OvermindDL1 / bucklescript-tea

TEA for Bucklescript

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Suggestion with Web components?

mistyharsh opened this issue · comments

Thank you for the great library. It has been a great experience so far.

I am trying to wrap bucklescript-tea component into web components. The initial skeleton looks like this:

import { main } from './Demo.bs.js';

// Library function to register a custom element
export function define(name, mainFn) {

    const componentClass = class extends HTMLElement {

        constructor() {
            super();
            // Attach a shadow root to the element.
            let shadowRoot = this.attachShadow({ mode: 'open' });

            mainFn(shadowRoot);
        }
    };
    customElements.define(name, componentClass);
}



// Application code
define('hello-world', main);

I was wondering if you have any suggestions for observing props in a typesafe manner. Currently, I am thinking of manually accepting the list of prop string to be observed and adding setter for each prop. Something like this:

define('hello-world', main, ['prop1', 'prop2']);

The plan is to either raise a custom event or call pushMessage directly from the prop setter. I am not sure if this is the right approach. Any suggestions on how I can do interoperability with JavaScript; specifically in the context of custom elements?

Events is definitely the way I'd recommend, it's more standards compliant. I try to only use pushMessage for things that cannot really be done by events. I keep meaning to add some helpers for listening and pushing events but they are pretty easy to write yourself. Just make a new Cmd to 'send' an event out and make a new Sub type to 'listen' to global events or listen for directed events on the view itself. For a Custom Component wrapping as you are doing then pushMessage would be most useful for passing lifecycle and property update events into the system I'd think. I really should make a new Application type to handle this automatically (though PR's welcome! ^.^). :-)

Thank you @OvermindDL1. This is a good start. Yes, I am definitely looking to have a PR but it would take some time. Just starting out with OCaml after a long time with TypeScript based development.

@OvermindDL1, Is there any way I can get access to the actual DOM node in my custom command? Also, is Task.perform the right way to wrap to create a custom command?

@OvermindDL1, Is there any way I can get access to the actual DOM node in my custom command?

In the worst case you can always just go down to calling into the DOM itself.

However, a 'Cmd' isn't associated with a node as it is designed to be front-end agnostic for later running on the server-side without a DOM, so although it can be done that way it may not actually be the best place, hmm...

In that case the best method would really be something on the view. This is not something support by Elm either so it might be worth breaking the Elm API just a touch for this, but at least it can be done in a backwards compatible way. A new Program Type that accepts a view callback that returns a vdom and a vdom.cmd or something would be generic, but maybe it would be sufficient enough to just let it pass back an event to fire.

Although at that point it would probably be best just to make a WebComponent program type that abstracts all that out more properly. ^.^

At the very least to get going 'now' you can give your root vdom an id, make a new Cmd, and just use VDom commands to grab the element with that id and fire the event on it using direct DOM commands. You'd want to be sure to add a hoist inside it to delay it for a render loop perhaps if you want it to work 'at' program startup (since the DOM elements don't exist until a browser re-render event happens), but it would be far more simple to ignore that part if you don't need that functionality.

Also, is Task.perform the right way to wrap to create a custom command?

You can but that's more useful for chaining things rather than pushing back into the main event loop (I prefer main event loop, but either way works fine). If you want to just make a normal Cmd you can basically copy this file into you app and just make a change to fire the event instead of acquire focus:
https://github.com/OvermindDL1/bucklescript-tea/blob/master/src-ocaml/tea_html_cmds.ml
(And you could PR it into the same file too if you want! :-))
The focus bit waits a render frame (or two) though, so that method works even at program startup, however that also delays the firing of the event by up to 32ms or so as well, which you may not want, if not then don't do all the Web.Window.requestAnimationFrame stuff and instead just do:

let focus id =
  Tea_cmd.call
    (fun _enqueue  ->
       let ecb _ =
         match Js.Nullable.toOption (Web.Document.getElementById id) with
         | None  ->
           Js.log ("Attempted to fire event on a non-existant element of: ", id)
         | Some elem -> <do-your-code-here, construct the Event then `dispatchEvent`>
       in
       ignore (ecb ());
       ())

The Web API built in exists before the one bucklescript had so you can just use bucklescript's API instead as it's more complete. You can use the API in this project by either adding the proper external's in your code to expose the Event constructor (or docuemtn.createEvent if you want to work with IE11) and the dispatchEvent calls, or feel free to PR them in too (or if needed I can add them here and push a new version if that would be easier, might be able to do so later today if needed?).

Thank you for your reply. I was able to get most of the things done. The idea of vdom.cmd and WebComponent program type seems really cool but it will be too early to tell. Right now, I have found a way to make TEA talk to JS Class and vice-a-versa. Before I propose a solution, I am experimenting with more.

At a glance, I am piggybacking a dispatcher object having a trigger method as part of initial data supplied to init() function. It looks like this:

// Trust simple dumb JS Closures
const dispatcher = {
    trigger: (name, /* extra data */) => {
        // this here refers to the Custom Element
        this.dispatchEvent(new CustomEvent(name));
    }
};

// Initiate TEA event loop
this._mainLoop = main(this[shadowRoot], dispatcher);

The only cons is to currently maintain to this dispatcher object as part of the Model. I believe WebComponent program type should be able to abstract this away, leaving developers back with traditional update function.

I believe WebComponent program type should be able to abstract this away, leaving developers back with traditional update function.

Specifically a webcomponent program type would listen for registrations via normal Subscriptions, and fire events via a Cmd. :-)

But yeah, a bouncer JS object is perfectly sufficient, basically what it will be doing in the background anyway. :-)