Unpoly - a framework like Turbolinks

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

Thanks @cpgo @OvermindDL1 @PragTob for the code simples. This should me to get started.

4 Likes

I’m sure if you guys work on it a bit and make it more compatible with Phoenix it will rise very high in popularity. My friend who is Laravel user said that Vue.js become famous because Laravel team praised it on many occasions and Jeffrey way and Taylor Otwel recommended it with Laravel, otherwise it was very hard for a new framework to get so much successful in the presence of Angular and React which are backed by tech giants.

1 Like

It is already fully compatible with Phoenix.

You only have to write a hand full of code, if you want to support the server protocol – which is optional. If you write an application with elixir/phoenix the hand full additional code should not add much complexity.

2 Likes

I’m sorry that I failed to explain what I meant.

I mean something which can be installed through a package, like Turbolinks in Rails.

I think if React needs extra work at the front-end, it’s already visible, so better to install it through webpack or whatever bundler at the front-end, but if Turbolinks is invisible and don’t need extra work, so there should be an option to install it through the Gemfile, which is exactly the case in Rails (you can install it as a gem (default) or you can install it through webpack etc).
Similarly if there is the option of installing Unpoly though webpack etc, and also as a hex package if someone wanna use it that way, it will make the library so popular in Elixir community, and Elixir community is growing very quickly, so it will be great for the library’s future.

1 Like

I think we’re all in violent agreement :slight_smile:

While Unpoly is completely language agnostic, the concerns at the top of this thread make it clear that we could communicate this better. Also the optionality of the server protocol confuses a lot of people. Offering a small Elixir package would go along way ensuring people that they won’t have to jump through hoops.

7 Likes

Of course we all agree that it’s language or framework agnostic but,

it will make it easier for novices like me.

and Thank You @triskweline for this awesome library!

1 Like

You already can, just npm install --save-dependency unpoly (or whatever the save command was called), then just link it in as normal. :slight_smile:

1 Like

In life there are things which look more scary than as dangerous they are in real, npm install --something is one of those things for a non-JavaScript programmer. :slight_smile:

Anyway I’ll take your word for unpoly and will use it before it becomes a package, through the npm command.

2 Likes

You know you can just turbolinks now right? it has an npm package