Drab: remote controlled frontend framework for Phoenix

Hi folks,
Few months ago I have announced the proof-of-concept of the library to manipulate the browsers DOM objects directly from Elixir (here). Finally, first beta has been released!

And of course it is available as a hex package.


In the meantime I’ve changed the API, so it is - I believe - easier to remember. If you think about webpage as a database which contains DOM objects, it is natural to query:

  • select(:text, from: "p:first") to get text of the first paragraph or
  • select(attr: :href, from: "a") for all links in the document,
    update some object(s) with
  • update(css: :border, set: "3px solid red", on: this(sender)),
    add new nodes or attributes
  • insert("<b>IMPORTANT</b>", before: "p:first"),
    or remove something with
  • delete(class: "btn", from: "#the_button"),
    and more, like synchronous modal which - launched on the server side - waits for user input.

Any suggestions, criticism, ideas welcome!


Grych

64 Likes

Do I understand it well if I say that this allows you to basically ‘remote control’ what a user sees on their webpage? :open_mouth:

This is very interesting, as I feel myself writing similar javascript over and over again to manage these kinds of tasks.

If this would be combined with a JS library managing a virtual DOM, we might even be able to write nearly all browser-UI management on the server, FRP-style!

:thumbsup: awesome stuff!

1 Like

Yes, it does exactly this. You can manipulate the user interface in the real-time, from Elixir/Phoenix.


Grych

1 Like

Good job! It’s awesome for all projects that do not care about support as many connections as possible as fast as possible :smile:
What do you think about shourtcut method to toggle actions (delete or insert something or update with keep prevoous value)?

I foresee a wonderful symbiosis between Drab and GenFRP :heart_eyes:

6 Likes

Yes, this is a good idea, thanks! We can extend update/3 function like:

update(:val, set: ["View mode", "Edit mode"], on: "#button")
update(:css, set: ["border: none", "border: 1px solid red"], on: "#button")

So in case when value to set is a list, it toggles between the array values (and it doesn’t have to be only 2 values: we can cycle!)

Interesting, thanks for sharing the idea!

Nice! Your API looks great. I want to see something like this in pure JavaScript.

You could add also shortcut for data- attributes:

update(socket, data: "something", set: "width: 100%", on: ".progress-bar")
# instead of:
update(socket, attr: "data-something", set: "width: 100%", on: ".progress-bar")

and allow to fetch dataset like:

select(socket, :dataset, from: "#name")

You can also move h3 outside ul at bottom of your home page :smile:

2 Likes

Thanks! I’ll add it to my todo list!.

This looks fascinating and could simplify code! A question though, how does it work with a shadow DOM (which makes custom elements internal parts invisible to normal DOM methods) as the site I work on is almost entirely Shadow Dom (which is faked on Firefox and such but a real shadow DOM on Chrome)?

1 Like

Awesome.
I was thinking about using intercooler.js mainly to get partial-page-rendering (alongside pushstate), but this seems even better.

2 Likes

One more idea style shortcut:

update(socket, style: "font-size", set: "6px", on: "#button")
# or:
update(socket, style: "font-size", set: 6, unit: :px, on: "#button")
2 Likes

It will not work - on the browser side it uses normal DOM methods from jQuery library.

Would it be possible to generalize drab to call a particular function on javascript land? That would allow us to have a callback say in a polymer component or a react one and let them be called from elixir via drab. I just found about your library yesterday, but I’m willing to play with it today, and will explore this, as I’d be interested in using it to command an existing react-native app (where no jquery is available, so I’d really like to call a generic js function, perhaps use my app tree state as a queriable thing, as drab treats the client side as a database).

2 Likes

It is already done - in fact, all the query functions just build the javascript and call it on the client side by running execjs/2 or broadcastjs/2. This functions just run any JS you give. At the beginning there were private, but I decided to open it to public as they may be useful in cases like yours :slight_smile:

https://hexdocs.pm/drab/Drab.Query.html#execjs/2
https://hexdocs.pm/drab/Drab.Query.html#broadcastjs/2

2 Likes

Nice ! so I guess drab is not actually tied to having a DOM, that was my biggest concern about using with react-native. Thanks for the links, and for drab, it looks just awesome! :heart:

1 Like

But it still requires jQuery on the client-side, as it was easier for me to develop it in early sandbox/proof-of-concept stage. Removing jQuery is on the todo list, but not on the top priority for now.

4 Likes

Yay, I was able to connect and call some random javascript at my app:

Still not commanding react properly but was able to execute a console log and a random js.

Here’s what I did

  • created a bare-bones react native app
  • copied just the essential parts of your drab.js template into it (removed most of the modal, and other stuff)
    called it DrabNative perhaps DrabHeadless could be better name, as it should be generic. Just included
    the parts for connecting and sending events with payload.
  • bundled phoenix.js in it
  • removed the Phoenix.Token validation and hardcoded my demo to Elixir.DrabPoc.PageController#index
  • created another method in the commander:
  def native_uppercase(socket, sender) do
    socket |> console("Hey DrabNative, this is PageCommander from the server side!")
    socket |> execjs("console.log(1+1)")
  end
  • on my app index.ios.js I did
export default class Drab extends Component {
  constructor() {
    super()
    const drab = DrabNative().run()
    this.state = {message: "Welcome to Drab Native"};
    this.upcase = drab('native', {event_handler_function: 'native_uppercase'})
  }

  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.welcome}>
          {this.state.message}
        </Text>
        <Button onPress={this.upcase} title="Press me" />
      </View>
    )
  }
}

That was my afternoon fun for today :slight_smile:

4 Likes

Maybe it would be a good idea to extract the Drab core (so functions execjs and broadcastjs) and treat jquery/DOM part as a kind of a plugin? Thus you could build your own non-DOM add-on, like helper functions for React or whatever you want :wink:

use Drab.Commander, plugins: [Drab.Query, Drab.Call]
4 Likes

Actually it is already done, by setting up :css property:

update(socket, css: "font-size", set: "6px", on: "#button")
1 Like