Unpoly - a framework like Turbolinks

What about http://unpoly.com

The unobtrusive JavaScript framework for server-side applications
Unpoly enables fast and flexible frontends with minimal changes to your server-side code.

8 Likes

That looks nice - I wonder how it differs from Turbolinks and what each of their strengths are.

1 Like

Iā€™ve tried it very recently. Unpolly is neat. It comes with dialogs and partial updates support. It does out of the box slightly more than Turbolinks.

Having said that, you can do everything with Turbolinks and some addon, or just writing some of your own code.

I like unpolly, it seems well-thought and functional out of the box, so it would be my choice, however.

6 Likes

Are there any github projects that demonstrate the use of unpoly with Phoenix/Elixir? It does look cool and it looks like itā€™d be valuable for me to do some cool things thatā€™d take me much longer if I tried to puzzle them out myself. But Iā€™d appreciate examples with Phoenix.

2 Likes

I implemented once this inside a Phoenix application:

4 Likes

Thanks for that, while I didnā€™t use pjax the information on how to make these frameworks work with disqus is very useful.

Is there any github example project available which demonstrates using Unpoly.js with Phoenix?

1 Like

What exactly would you be looking for? Itā€™s entirely a front-end library so there is no real overlap?

4 Likes

When I looked at the Unpolyā€™s source at github, it have things like Gemfile, Gemfile.lock, unpoly-rails.gemspec, .ruby-version etc. Also the project has 6.2% ruby in the source. Is it designed to work specifically with Rails, or it can be used with Phoenix?

1 Like

it can be used with any server framework, as it is purely a javascript framework. Have you visited the https://unpoly.com/ website?

there it says:

3 Likes

The folks at Basecamp have recently released a small library called StimulusJS. Itā€™s meant to work alongside Turbolinks and it automatically connects HTML elements to javascript code through data-attributes.

It is a small lib though. There is no modals/popups/animations/forms validationsā€¦

Also read The origin of Stimulus

6 Likes

It includes a simple minimal Ruby library to make handling headers and such easier, but that is trivial to do in stock Phoenix anyway that Iā€™ve never bothered. ^.^

2 Likes

unpoly is made by my friends at makandra or more specifically by Henning the agency part of their company does mainly Ruby so having some very lightweight Ruby/Rails integration onboard is certainly in their best interest.

Itā€™s designed to be agnostic to whatever backend it is running on/with so itā€™s definitely good to go with Phoenix.

I also wouldnā€™t call it ā€œlike turbolinksā€ - itā€™s way different in my mind or maybe I just donā€™t know turbolinks enough. The thing which most got me when henning introduced it was something along the lines of ā€œWe were asking the question what would an unobtrusive way to add JavaScript enhancements to your HTML in a new standard look likeā€ - and thatā€™s what they did, most of it works through additional attributes etc.

Itā€™s a lovely design imo Iā€™'ve never played with it myself so far though.

2 Likes

Unpoly author here. Happy to answer any questions you might have!

Unpoly has no dependency on Ruby or Rails. There is a completely optional server protocol that improves a few edge cases like detecting redirects. You can implement that in a few lines of Elixir or just use Unpoly without it. E.g. unpoly.com is a static website that uses Unpoly and does not even have a server-side app behind it. Itā€™s just a folder of HTML files.

That said, I have noticed that Unpoly seems popular in the Elixir community and I would really like to offer better support for Elixir. Iā€™m currently a little lost where to start implementing something like unpoly-rails (server protocol for Rails) for Elixir/Phoenix. I guess this would be a ā€¦ plug? How would I unit test and package it? And so on. If anyone has some pointers or example code, I would be grateful.

15 Likes

Whooo welcome! Didnā€™t expect you here. ^.^

Iā€¦may push it around a lot here, I really find itā€™s API to be well designed (though I do wish I could feed it through my rollup packaging system, it doesnā€™t follow the JS module spec). ^.^;

Yep plugs and an api for querying and setting things. Iā€™ve made enough of what Iā€™ve needed in my own project. Iā€™ve intended to pull it out for a while now and document it, but it is so super easy for anyone to make theirselves that Iā€™ve not really gotten around to it yetā€¦ ^.^;

My file as it stands, I donā€™t use ā€˜allā€™ of it so I cannot claim it all to be tested, and of course itā€™s missing a lot of obvious helpers that I otherwise do randomly in my code (bad form I know) so it does need some work on it (itā€™s an internal fileā€¦), but this is what I have so far in an unpoly.ex file:

defmodule Unpoly do


  def up?(conn), do: target(conn) !== nil
  # defdelegate unpoly?, to: up?


  def target(conn), do: List.keyfind(conn.req_headers, "x-up-target", 0, nil)


  def target?(conn, tested_target) when is_binary(tested_target) do
    if up?(conn) do
      case target(conn) do
        ^tested_target -> true
        "html" -> true
        "body" -> not (tested_target in ["head", "title", "meta"])
        _ -> false
      end
    else
      true
    end
  end


  def validate?(conn), do: validate_name(conn) !== nil


  def validate_name(conn), do: List.keyfind(conn.req_headers, "x-up-validate", 0, nil)


  def set_title(conn, new_title) when is_binary(new_title), do: Plug.Conn.put_resp_header(conn, "x-up-title", new_title)


  defmodule Plugs do
    defmodule RequestMethodCookie do
      @cookie_name "_up_method"

      def init(options), do: options

      def call(conn, params) do
        case conn.method do
          "GET" -> Plug.Conn.delete_resp_cookie(conn, @cookie_name, List.wrap(params[:cookie_opts]))
          method -> Plug.Conn.put_resp_cookie(conn, @cookie_name, method, List.wrap(params[:cookie_opts]))
        end
      end
    end


    defmodule RequestEchoHeaders do
      def init(options), do: options

      def call(conn, params) do
        conn
        # |> Plug.Conn.put_resp_header("x-up-location", conn.request_path)
        # |> Plug.Conn.put_resp_header("x-up-method", conn.method)
      end
    end


    defmodule AddRedirectHeaderAfter do
      def init(options), do: options

      def call(conn, params) do
        case Plug.Conn.get_resp_header(conn, "location") do
          location when is_binary(location) -> Plug.Conn.put_resp_header(conn, "x-up-location", location)
        end
      end
    end
  end

end

To package it you just run mix new unpoly and follow the steps, the replace the pre-built lib/unpoly.ex with the above (edited to add documentation and all such too of course).

For testing you would want to inlcude the Plug library as a testing only dependency and use itā€™s test helpers to test that things go through properly as expected either in doctests, or the dedicated test file(s) in the test/* directory. Iā€™m sure someone here could create a whole scaffold project if you want. :slight_smile:

Once itā€™s made youā€™ll want to include ex_doc as a dev-only dependency then youā€™ll be able to publish it to the hex package system with documentation and all (and of course you can keep the whole project directory inside the unpoly project if you preferred too). :slight_smile:

15 Likes

@OvermindDL1 laid it out pretty neatly, other than that @triskweline - next time we see each other (you make it to Berlin or should I make it to the south againā€¦) happy to assist with/pair on creating a hex package with a plug. Unless @OvermindDL1 finds the time before and just creates it :smiley:

5 Likes

Thanks @OvermindDL1 for the sample code and also for spreading word about Unpoly.

Is there an example somewhere for a plug test? In the Rails world, testing code that works with requests and responses requires a lot of setup. Iā€™m guessing it should be easier in a functional language, but Iā€™d still need to dispatch and then inspect a request somehow.

4 Likes

I belive you can do something like this

# define your plug
defmodule Plugtest do
  import Plug.Conn

  def init(opts), do: opts

  def call(conn, _) do
    conn
    |> put_resp_header("x-custom-header", "Blarg")
  end
end

# define a dummy router
defmodule Plugtest.Router do
  use Plug.Router
  
  plug(Plugtest)
  plug(:match)
  plug(:dispatch)

  get("/", do: send_resp(conn, 200, "Welcome"))
end

# write the test
defmodule PlugtestTest do
  use ExUnit.Case
  use Plug.Test
  alias Plugtest.Router

  @opts Router.init([])

  test "has header" do
    conn = conn(:get, "/", "")
    |> Router.call(@opts)
    assert Enum.member?(conn.resp_headers, {"x-custom-header", "Blarg"})
    assert conn.status == 200
    assert conn.state == :sent
  end
end

I dont know if this is the best approach, but this is what I got by reading this guide

1 Like

@cpgoā€™s example is the traditional way as it sets up a request and all such, but that is usually for testing an entire pipeline.

For testing just a single plug, well Iā€™d just call it. :slight_smile:

First how a plug works, the plug call itself (for the given Unpoly.Plugs.AddRedirectHeaderAfter in my code above), it would be added to a pipeline via the plug command like:

plug Unpoly.Plugs.AddRedirectHeaderAfter

If it took options (it doesnā€™t, but if it did) then it would be like:

plug Unpoly.Plugs.AddRedirectHeaderAfter, blah: :blorp

That basically compiles into:

# Option-less:
Unpoly.Plugs.AddRedirectHeaderAfter.call(conn, unquote(Macro.escape(Unpoly.Plugs.AddRedirectHeaderAfter.init([]), __ENV__)))
# With the option
Unpoly.Plugs.AddRedirectHeaderAfter.call(conn, unquote(Macro.escape(Unpoly.Plugs.AddRedirectHeaderAfter.init([blah: :blorp]), __ENV__)))

In essence it calls the init/1 callback at compile-time and inlines the result as the second argument in the call function, then when the pipeline is run the call function is run with the connection and the baked-in parameters that is returned from the init function.

This means that if you are just testing a plug that it is really easy to do so, basically you can just do this:

# Put this outside the function so it becomes static, just to make sure you don't return something unserializable like a ref or so
import Plug
use Plug.Test
alias Unpoly.Plugs.AddRedirectHeaderAfter
@params AddRedirectHeaderAfter.init([])

# And in the testing functions
conn = put_resp_header(conn(:get, "/"), "location", "/")
assert %Plug.Conn{} = conn = AddRedirectHeaderAfter.call(conn)
assert "/" = get_resp_header(conn, "location")

You can see this at the bottom of:
https://hexdocs.pm/plug/readme.html

2 Likes

plug is pretty much like rack. I.e. just calling it is often good enough as mentioned above. For a full runnable exampe test from our bugsnex plug.

Or from the plug repo itself (the plugs it ships with) sample1 - sample2

1 Like