max-mapper / yo-yo

A tiny library for building modular UI components using DOM diffing and ES6 tagged template literals

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Element lifecycle; calling .focus()

callum opened this issue · comments

I'm currently trying to work out how to call .focus() on an element.

const yo = require('yo-yo')

function textarea (value) {
  const el = yo`<textarea>${value}</textarea>`
  el.focus()
  return el
}

const foo = textarea('foo')
document.body.appendChild(foo)
yo.update(foo, textarea('bar'))

There are a couple problems with the above:

  1. updating is asynchronous; the textarea function doesn't know when its return value is added to the page
  2. element references are inconsistent; el represents a new element, not what's in the page

Any ideas how to achieve this? I've tried a few things to no avail.

Unfortunately elements don't natively have an onload and onunload events. I use the on-load library atm, which uses a MutationObserver to detect when the element is loaded/unloaded on the body. But would eventually like to figure out a more robust lifecycle implementation.

For element references, if you need the actual element in the dom and lost the reference to it, you can always use document.querySelector() to find it.

The "target" element foo will always hold the reference to the actual element. So another strategy if an element needs to update itself, let that element control the update, rather than a parent. But it really depends on what you're building on how high up the yo.updates should occur.

commented

@shama ooh, on-load looks heaps good.

I think virtual-dom's widgets got the events down to a minimum:

  • init: when an event is first rendered
  • update: when an event receives new properties
  • destroy: when an event is removed from the dom

I think between on-load and functions receiving data, this model is already in place for bel. Perhaps yo-yo could integrate on-load, but I don't think there's would be a need for more than these events. Thoughts?

@yoshuawuyts I had an implementation that looked like:

var el = yo`<div onload=${function () {}} onunload=${function () {}}>
  add me
</div>`

To be consistent with existing events and those events are the ones I wish elements already had natively.

But opted to keep the elements pure to make implementing yo-yoify easier (and one of the reasons dom-css was removed too). I'd be curious to see how it would affect yo-yoify if on-load was integrated.

I wanted to look into how web components shim lifecycle support too. Might be a good model to borrow from too.

Oh and on-load doesn't support older browsers but we could implement a polling backup to the MutationObserver... but I wanted to dive into more solutions to ensure the solution is the best option we currently have to implement it.

Looks like webcomponents are using MutationObservers as well for their lifecycles too: https://github.com/webcomponents/webcomponentsjs/blob/master/src/CustomElements/observe.js

commented

Yeah, I think on-load pretty much covers all our bases; perhaps improving the syntax on the yo level would be neat. I quite like the idea of using onload=${function}, it looks heaps neat.

As far as webcomponent events are concerned, I believe they expose the following:

.createdCallback()                         // after element was created
.attachedCallback()                        // after element was attached to DOM
.detachedCallback()                        // after element was detached from dom
.attributeChangedCallback(attr, old, nw)   // on element attribute change

.attached() / .detached() seem similar to what on-load implements now. I believe .created() is like an init function; I reckon it shouldn't be that hard to emulate if we keep track of state a bit (perhaps extend on-load? Or should we leave it up to the consumer to implement?) I don't think .attributeChangedCallback() would be needed.

morphdom does an OK job of keeping track of lifecycle. One approach I tried was adding onBeforeNodeDiscarded and onBeforeMorphEl handlers that emitted custom events on elements. That only partially solved my issue though. on-load seems like a pretty good approach. What would an ideal solution be?

My pull request for more detailed lifecycle handling in morphdom has just been released: patrick-steele-idem/morphdom#46

I'm considering creating a module that wraps the morphdom function to give you lifecycle hooks. This could be implemented as custom events on elements, or even something callback-based. Example:

const yo = require('yo')
const hooks = require('morphdom-hooks')(yo.update)

function textarea (value) {
  const el = yo`<textarea>${value}</textarea>`
  hooks.onAdded(el, function (el) {
    el.focus()
  })
  // hooks.onUpdated
  // hooks.onDiscarded
  return el
}

const foo = textarea('foo')
document.body.appendChild(foo)
yo.update(foo, textarea('bar'))

What this wouldn't have is a way to handle the initial insertion to the document - events would only occur when the DOM is morphed.

The simplest I could come up with: https://github.com/callum/morphdom-hooks

Thoughts @shama? @yoshuawuyts?

commented

@callum @jmeas @shama

yo.transform.js

function has(object, path) {
  return object != null && hasOwnProperty.call(object, path);
}

var morphdomHooks = ["onBeforeNodeAdded", 
  "onNodeAdded", 
  "onBeforeElUpdated", 
  "onElUpdated", 
  "onBeforeNodeDiscarded", 
  "onNodeDiscarded", 
  "onBeforeElChildrenUpdated"];

module.exports = yo.transform = function(el, newEl, opts) {
  if(typeof opts !== 'object')
    opts = {};

  for (var hook = 0; hook < morphdomHooks.length; hook++)
    if(has(el, morphdomHooks[hook]))
      opts[morphdomHooks[hook]] = el[morphdomHooks[hook]]

  return this.update(el, newEl, opts);
}

smaller version (without 'has'):

var morphdomHooks = ["onBeforeNodeAdded", 
  "onNodeAdded", 
  "onBeforeElUpdated", 
  "onElUpdated", 
  "onBeforeNodeDiscarded", 
  "onNodeDiscarded", 
  "onBeforeElChildrenUpdated"];

module.exports = yo.transform = function(el, newEl, opts) {
  if(typeof opts !== 'object') opts = {};

  for (var hook = 0; hook < morphdomHooks.length; hook++)
    opts[morphdomHooks[hook]] = el[morphdomHooks[hook]]

  return this.update(el, newEl, opts);
}

Usage:

function Button (content) {
   return yo`<button>${content}</button>`
}

var button = Button("My Button")

button.onElUpdated = function (node) {
   console.log('updated!');
}

yo.transform(button, Button("My New Button"))

document.body.appendChild(button)

A more modular approach, thoughts? It may be slightly slower due to the testing and binding of the element every time it's updated. But I would assume it's minimal processing. If you want to bypass this, you can just use yo.update. But yo.transform would be used if you have morphdom hooks.

As well the morphdomHooks can be decreased reducing functionality, but increasing processing speed.

**Note, doesn't seem to work on requirebin, but I think that's to do with morphdom. Works locally.

@SilentCicero is that different to https://github.com/callum/morphdom-hooks? It's made to be used with yo-yo:

var yo = require('yo-yo')
var hooks = require('morphdom-hooks')
var update = hooks(yo.update)

update(nodeA, nodeB, {})
commented

For anyone experimenting with MutationObserver:
https://developer.mozilla.org/en/docs/Web/API/MutationObserver

pretty interesting.

window.listeners = [];
var doc = window.document,
MutationObserver = window.MutationObserver || window.WebKitMutationObserver,
observer;

function ready(selector, fn){
    // Store the selector and callback to be monitored
    listeners.push({
        selector: selector,
        fn: fn
    });
    if(!observer){
        // Watch for changes in the document
        observer = new MutationObserver(check);
        observer.observe(doc.documentElement, {
            childList: true,
            subtree: true
        });
    }

    console.log(listeners);

    // Check if the element is currently in the DOM
    check();
}

function check(){
    // Check the DOM for elements matching a stored selector
    for(var i = 0, len = window.listeners.length, listener, elements; i < len; i++){
        listener = window.listeners[i];
        // Query for elements matching the specified selector
        elements = doc.querySelectorAll(listener.selector);
        for(var j = 0, jLen = elements.length, element; j < jLen; j++){
            element = elements[j];
            // Make sure the callback isn't invoked with the
            // same element more than once
            if(!element.ready){
                element.ready = true;
                // Invoke the callback with the element
                listener.fn.call(element, element);
            }
        }
    }
}
commented

Just left a note of some trouble I'm having with the on-load package, and yoyo: shama/on-load#1

For focus specifically, you can set a boolean html property autofocus when you trigger an update.

commented

@substack see my latest life cycle package: https://github.com/SilentCicero/throw-down

I tried out morphdom-hooks but found that only the parent nodes got the add and discard events.

Also on the initial render the events weren't called.

I ended up with https://gist.github.com/JamesKyburz/f0d8536c6ad7f144989984655dadf226

@callum Great thanks!

commented

on-load seems to be working brilliantly https://github.com/shama/on-load/issues - looking like a winner. The question now is: should this be part of yo-yo or bel in some way, or would this be a separate thing?

commented

Ah choojs/nanohtml#32 is out; this means it'll be part of bel which means that once it lands and we update the version of bel here this issue will be resolved ✨