Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Callback / event on DOM insertion #80

Open
soren-n opened this issue Apr 17, 2018 · 13 comments
Open

Callback / event on DOM insertion #80

soren-n opened this issue Apr 17, 2018 · 13 comments

Comments

@soren-n
Copy link
Contributor

soren-n commented Apr 17, 2018

This might turn out to be either a question or a feature request, sorry if opening an issue is not the right place to ask this (btw is there a project related forum/chat somewhere?).

I am trying to achieve auto-resizing of textareas. Resizing on value change is working fine by using something like the following:

external _auto_expand : Web.Node.t -> unit = "textarea_auto_expand" [@@bs.val]
event "input"
  (fun evt ->
    match Js.Undefined.toOption evt##target with
    | None -> None
    | Some target -> begin
       _auto_expand target;
       match Js.Undefined.toOption target##value with
       | None -> None
       | Some value -> Some (action value)
     end)

I am using an external to hack around certain missing fields on Web.Node.t such as scrollHeight and others (tbh I prefer it this way since auto resizing of textareas is a hack regardless):

const textarea_auto_expand = element => {
  const get_value = pixels => parseInt(pixels.substring(0, pixels.length - 2));
  const style = window.getComputedStyle(element);
  const min_height = get_value(style.getPropertyValue("min-height"));
  element.style.height = 0;
  element.style.height = Math.max(min_height, element.scrollHeight) + 'px';
};

This works great, but I also need to have _auto_expand be called when the element is inserted into the DOM. E.g. in React you would implement componentDidMount.

I have been trying to find examples in Elm where you get access to a DOM element on insertion/mount time, but have not found any.

So, the question / feature request is; is it possible to get access to an element at the moment after it is inserted into the document?

@OvermindDL1
Copy link
Owner

OvermindDL1 commented Apr 17, 2018

This might turn out to be either a question or a feature request, sorry if opening an issue is not the right place to ask this (btw is there a project related forum/chat somewhere?).

Nah this is perfect. :-)

If you want a forum, the elixirforums (of all places) is generally where it is talked about most though, otherwise here. :-)

I am trying to achieve auto-resizing of textareas.

Heh, I have the same thing in one of my apps!

I am using an external to hack around certain missing fields on Web.Node.t such as scrollHeight and others (tbh I prefer it this way since auto resizing of textareas is a hack regardless):

I actually parse it based on event callbacks and json-path branching, though adding it to node would be good (can you PR those in?).

This works great, but I also need to have _auto_expand be called when the element is inserted into the DOM. E.g. in React you would implement componentDidMount.

Yeah this would be the point of the custom node that I've not implemented yet, for now just wait a render tick by subscribing to a Tea.AnimationFrame.every or so for a single tick (so just add a flag on your model, set the flag when you create the textbox, then clear it when the event from the animationframe is returned). The animation frame 'ticks' when the VDom is re-rendered so it is the excellent place to do something when the vdom is rendered (I'm pretty sure it is called 'after' the vdom updates and not before, otherwise delay an extra tick, but I'm pretty sure it is 'after'). Then from your event that's given to the update from that then just call your auto_expand function. :-)

I should leave this open until I get custom nodes added in, I really need to get around to doing that VDom overhaul...

(EDIT: You can always listen to DOM added events from javascript too)

@OvermindDL1
Copy link
Owner

Hmm... it would be really easy to make an attribute that enqueues an event only when it is added (and optionally another on removal), this would be perfect for listening to specific element add/remove events... I should add that to the todo too...

@soren-n
Copy link
Contributor Author

soren-n commented Apr 18, 2018

Thank you for the quick reply @OvermindDL1 👍

I'll try out the animation frame tick idea as a temporary solution.

Being able to register for a custom event e.g. "mount" would follow the current logic well:

event "mount"
  (fun evt ->
    match Js.Undefined.toOption evt##target with
    | Some target -> _auto_expand target; None
    | None -> None)

I might add the additional fields to Web.Node.t and make a PR.

@OvermindDL1
Copy link
Owner

OvermindDL1 commented Apr 18, 2018

More things in the exported types is always nice, but yeah such an event or attribute would definitely be best. I wish the "load" event on the DOM worked on every element, it would be perfect if so.

@alfredfriedrich
Copy link
Contributor

Might be related..
I'm trying to attach an event listener to a serviceworker and the event has a serviceworker in target. I tried to create a subscription, but I failed to attach the callback on the serviceworker. Vdom.onCB only works with Web.Node.event. I'm also not sure where to register the event listener. In update I can return some Cmd msg, but I could not figure out how to register the callback on the serviceworker (which is not a Web.Node.t).
Any help regarding callbacks on various JS object would be appreciated.
I can make it to Js.log some message, but I want to be able to "send" a Cmd msg or similar from the callback. So basically I have: serviceworkerRegistration Js.Promise.t resolves, triggers an update call Registered of serviceworkerRegistration. In that Registered branch i want to register my callbacks on properties of the serviceworkerRegistration. And the callbacks in turn should "send" another Cmd msg.
@soren-n in your above code, how is the event external defined?

@soren-n
Copy link
Contributor Author

soren-n commented Apr 30, 2018

Hey @alfredfriedrich,

Yes, sorry for the slight psudo-code above. The definition of event is not an external, since all "interactions" with the actual DOM goes through the virtual DOM, registration of event handlers have to go through the virtual DOM as well. I don't know much about the how the virtual DOM is implemented, so I am not of much help in that regard.

However, this is taken from tea_html.ml, as an example of how to register for input events through the VDOM:

let onInputOpt ?(key="") msg =
  onCB "input" key
    (fun ev ->
       match Js.Undefined.toOption ev##target with
       | None -> None
       | Some target -> match Js.Undefined.toOption target##value with
         | None -> None
         | Some value -> msg value
)

let onInput ?(key="") msg = onInputOpt ~key:key (fun ev -> Some (msg ev))

You would use it to register in the following example:

type effect =
  | MyEffect of string
  [@@bs.deriving {accessors}]

let draw model =
  ...
  input' [ onInput myEffect ] []

By service workers do you mean this? (I am not that seasoned in browser app programming, so this is the first time I hear about it) It looks like it comes with a lot of custom API that you would need to wrap using externals, so the above code snippets might not be that relevant.

As an example of how to wrap external APIs try to have a look at web_xmlhttprequest.ml, it helped me a lot with some of the work I've been doing lately.

@alfredfriedrich
Copy link
Contributor

Yes, sorry for the slight psudo-code above.

no worries :)

The definition of event is not an external, since all "interactions" with the actual DOM goes through the virtual DOM, registration of event handlers have to go through the virtual DOM as well.

This is where I hit a wall, because ServiceWorkers have no DOM access.

By service workers do you mean this?

Yes, particulary onstatechange

It looks like it comes with a lot of custom API that you would need to wrap using externals

This is what I came up so far:

type notification

type serviceworker = <

  (* properties *)
  state : string;
  scriptURL : string;

  (* methods *)
  onstatechange : (serviceworker_event -> unit [@bs.meth]) [@bs.set]; 
> Js.t

and serviceworker_event = <
  target : serviceworker Js.Nullable.t [@bs.get]
> Js.t

and serviceworkerregistration = <
  (* methods *)
  unregister : unit -> bool Js.Promise.t [@bs.meth];

  (* properties *)
  scope : string;
  installing : serviceworker Js.Nullable.t;
  waiting : serviceworker Js.Nullable.t;
  active : serviceworker Js.Nullable.t;
  pushManager : pushmanager;
> Js.t

and pushmanager = <
  (* methods *)
  subscribe : subscribeoptions -> pushsubscription Js.Promise.t [@bs.meth]
> Js.t

and pushsubscription = <
  endpoint : string;
  expirationTime : string option;
> Js.t

and serviceworkercontainer = <
  (* properties *)
  controller : serviceworker Js.Nullable.t;
  ready : serviceworkerregistration Js.Promise.t;
  register : string -> serviceworkerregistration Js.Promise.t [@bs.meth];

> Js.t

external container : serviceworkercontainer = "serviceWorker" [@@bs.val] [@@bs.scope "navigator"]

As an example of how to wrap external APIs try to have a look at web_xmlhttprequest.ml, it helped me a lot with some of the work I've been doing lately.

Yep, reading it again and again :-) I feel like there is an answer for me in there, too.

@alfredfriedrich
Copy link
Contributor

never mind, got it somehow working:

    let onStateChange swOpt =
      match Js.Nullable.toOption swOpt with 
      | Some s -> 
        Cmd.call (fun callbacks ->
            let enqRes = fun ev ->
              match Js.Undefined.toOption ev##target with
              | None -> ()
              | Some target -> 
                let targetAsServiceworker = targetToServiceworker target in
                let msg = ServiceWorkerChanged targetAsServiceworker in
                (* let open Vdom in *)
                !callbacks.enqueue msg
            in
            s##onstatechange #= enqRes
          )
      | None -> Cmd.none
    in



    let cmds =
      [reg##installing; reg##waiting; reg##active]
      |> List.map onStateChange
    in (model, Cmd.batch cmds)

@OvermindDL1
Copy link
Owner

OvermindDL1 commented May 1, 2018

I'm trying to attach an event listener to a serviceworker and the event has a serviceworker in target. I tried to create a subscription, but I failed to attach the callback on the serviceworker. Vdom.onCB only works with Web.Node.event. I'm also not sure where to register the event listener. In update I can return some Cmd msg, but I could not figure out how to register the callback on the serviceworker (which is not a Web.Node.t).

Might need an external for that, I've not set up service workers yet (though if you come up with a great API then definitely PR it in! :-) ).

Any help regarding callbacks on various JS object would be appreciated.

What would be the javascript code of what you are trying to accomplish?

I can make it to Js.log some message, but I want to be able to "send" a Cmd msg or similar from the callback.

Lot's of ways to do that, from pushing a message manually to the main app itself to handling things in a Cmd or Subscription. :-)

However, this is taken from tea_html.ml, as an example of how to register for input events through the VDOM:

This is exactly right! Though for DOM events the event external should be what is used regardless, but you can use it in a ton of different ways!

By service workers do you mean this? (I am not that seasoned in browser app programming, so this is the first time I hear about it) It looks like it comes with a lot of custom API that you would need to wrap using externals, so the above code snippets might not be that relevant.

Similar for me, I've heard of service workers and know how they are used in PWA's, but I've never used them yet... ^.^;

This is where I hit a wall, because ServiceWorkers have no DOM access.

What would be the javascript code for using them? I'm thinking that a Subscription interface is what will suit them based on what little I know about them, but if you can show the JS for them and how to interact with them then I could come up with a better API for it. :-)

This is what I came up so far:

Ahh, it looks like a global object?

never mind, got it somehow working:

Oh hey cool! Don't suppose you want to PR that code? If you leave the PR open to commits from the main repo people then I can clean it up and get it merged in to main as a combination set of subscriptions and commands. :-)

@alfredfriedrich
Copy link
Contributor

What would be the javascript code for using them? I'm thinking that a Subscription interface is what will suit them based on what little I know about them, but if you can show the JS for them and how to interact with them then I could come up with a better API for it. :-)

The serviceworker itself is currently in JS:

self.addEventListener('install', function(event) {
  console.log('service worker is installing.')
});

self.addEventListener('activate', function(event) {
  console.log('service worker is activating.')
});

self.addEventListener('push', function(event) {
  if (event.data) {
    console.log('This push event has data: ', event.data.text());
    console.log(event)
  } else {
    console.log('This push event has no data.');
  }
});

I plan to try to write it in BS later.

Ahh, it looks like a global object?

Yes, e.g. to register a service worker:

if ('serviceWorker' in navigator) {
  window.addEventListener('load', function() {
    navigator.serviceWorker.register('/sw.js').then(function(registration) {
      // Registration was successful
      console.log('ServiceWorker registration successful with scope: ', registration.scope);
    }, function(err) {
      // registration failed :(
      console.log('ServiceWorker registration failed: ', err);
    });
  });
}

and later I can use the registration to show push notifications:

serviceWorkerRegistration.showNotification(title, options);

To play around, I wanted to register an event listener on the serviceworker and registration records, this would be the matching JS code

serviceWorkerRegistration.onupdatefound = function (event) {
    model.registration = event.target.registration
}
or
myActiveServiceworker.onstatechange = function (event) {
    $('#swindicator').text = event.target.state
}

Oh hey cool! Don't suppose you want to PR that code? If you leave the PR open to commits from the main repo people then I can clean it up and get it merged in to main as a combination set of subscriptions and commands. :-)

Currently, I'm rewriting it once more :-) Will try to create a PR if it looks good.

@OvermindDL1
Copy link
Owner

The serviceworker itself is currently in JS:

Hmm, so it's just a set of global event listeners it looks like? Yeah this would fit the subscription pattern absolutely perfectly then. :-)

I'm guessing the addEventListener on it has a corresponding removeEventListener as well, like on the DOM with the same args?

Yes, e.g. to register a service worker:

And the registration seems to just return a promise then? In that case it can be a Cmd, though perhaps combining the Cmd and promise handling into a single call would be easier for the user (and remove the need for another message head)...

and later I can use the registration to show push notifications:

Just a simple Cmd on that one too. :-)

To play around, I wanted to register an event listener on the serviceworker and registration records, this would be the matching JS code

These could be done via addEventListener too yes?

Currently, I'm rewriting it once more :-) Will try to create a PR if it looks good.

Awesome! But yeah just follow the patterns above, I'd make a single subscription that subscribes to a user-specified event on the serviceWorker that takes a function that takes the event argument and return a msg option type so they can return information from the function back to the event loop. If you need help just ask (I'm on IRC and Discord whatever you prefer) or I can do it if you need it done and out soon now that I have an idea of what the API should be. :-)

@alfredfriedrich
Copy link
Contributor

Hmm, so it's just a set of global event listeners it looks like? Yeah this would fit the subscription pattern absolutely perfectly then. :-)
I'm guessing the addEventListener on it has a corresponding removeEventListener as well, like on the DOM with the same args?

I did not see any examples where they used removeEventListener, because the serviceworker "stays" at the users, until it gets updated (onupdatefound on the registration gets triggerd) or unregistered / removed, but yeah I guess it's using the EventTarget interface a lot.

These could be done via addEventListener too yes?

Definitely, I use an external that uses addEventListener under the hood just for that.

Because the serviceworker / registration / container object "seem to appear somewhere in the DOM Tree" (navigator.serviceWorker.controller is not null after registeris called and the promise resolves), it seemd related to this issue and I responded to it, sorry for highjacking it.

On a side note, I tried to implement a global "mount/unmount" listener, rigged up a working Sub with a MutationObserver on documentand tried to filter for a specific node, but in the end, it only compared a "living" Dom node with a static copy in Javascript and this failed:

type mutationRecord = <
  _type : string;
  target : Web_node.t;
  addedNodes : Web_node.t list;
  removedNodes : Web_node.t list;
> Js.t 

type mutationObserver = <
  observe : Web_node.t -> mutationObserverInit -> unit [@bs.meth];
  diconnect : unit -> unit [@bs.meth];
> Js.t

external mutationObserver : (mutationRecord Js.Array.t -> unit) -> mutationObserver = "MutationObserver" [@@bs.new]

let observerOptions = [%bs.obj {
  childList = true;
  attributes = true;
  characterData = true;
  subtree = true;
  attributeOldValue = true;
  characterDataOldValue = true;
  (* attributeFilter = []; *)
}]

let global tagger =
  let open Vdom in
  let enableCall callbacks_base =
    let callbacks = ref callbacks_base in
    let handler = (fun (recs : mutationRecord Js.Array.t) ->
        (* let () = Js.log2 "In Global Observer" recs in *)
        match !observed with
        | None -> ()
        | Some vdom -> 
          let node = patchVNodesOnElems_CreateElement callbacks vdom in
          let filterForNode =
            List.filter (fun elem ->
                (* this is okay for the bs compiler, but in javascript world, elem.target is a living dom node and the node above is just a pumped up Vdom.t *)
                elem##_type == "childList" && elem##target == node
              ) in
          let foundNode = filterForNode (Array.to_list recs) in
          let () = Js.log2 "foundNode" foundNode in
          ()
          (* callbacks.enqueue (tagger recs) *)
      ) in
    let m = mutationObserver handler in
    let () = m##observe Web_node.document_node observerOptions in
    fun () -> m##diconnect ()
  in Tea_sub.registration "mutationobserver" enableCall



let register node =
  let () = observed := Some node in
  ()


let unregister =
  let () = observed := None in
  ()

@OvermindDL1
Copy link
Owner

Definitely, I use an external that uses addEventListener under the hood just for that.

Just making sure so the same calls can be used. :-)

Because the serviceworker / registration / container object "seem to appear somewhere in the DOM Tree" (navigator.serviceWorker.controller is not null after registeris called and the promise resolves), it seemd related to this issue and I responded to it, sorry for highjacking it.

All good, navigator is not really in the DOM tree anyway. :-)

On a side note, I tried to implement a global "mount/unmount" listener, rigged up a working Sub with a MutationObserver on documentand tried to filter for a specific node, but in the end, it only compared a "living" Dom node with a static copy in Javascript and this failed:

I thought I read somewhere that mutation observers ended up not getting added to the spec and thus can't be relied on? I need to look in to that again, might be thinking about something else...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants