WebReflection / document-register-element

A stand-alone working lightweight version of the W3C Custom Elements specification
ISC License
1.13k stars 116 forks source link

Implement WebComponentsReady event #5

Closed goldoraf closed 10 years ago

goldoraf commented 10 years ago

Hi, and thanks for this excellent polyfill ;)

As @einarq reported (https://github.com/WebReflection/document-register-element/issues/4), async upgrade of custom elements makes a bit hard to work with, but adding a 'WebComponentsReady' would help.

I'll try to look into the code next week in order to provide a patch if you're OK with that.

WebReflection commented 10 years ago

I'm not sure that's part of the spec plus components can be defined at any time so I'm not sure that makes sense at all and it won't give us nothing more than what createdCallback can already. What do you think?

WebReflection commented 10 years ago

OK, you haven't come back here but again, I think there's nothing like that in CustomEvents specifications, neither in registerElement.

That is probably some specific thing provided by polymer but it does not belong to this polyfill, IMO.

I'll keep this open for other two days, feel free to close it if you believe there's nothing to do or tell me where is the specification about such event regarding Custom Elements specs.

Thank you

goldoraf commented 10 years ago

Sorry for the delay, I'm currently in vacation. You 're right, there is no such event in the spec. But the async nature of the polyfill make it imho necessary to have. At least, the polyfill could fire custom events at key points in its lifecycle, so that users could deal with the problem.

What do you think ?

WebReflection commented 10 years ago

This is a polyfill for Custom Elements, if I start doing things the way I think should be I'll make this nothing different from Polymer, a non standard library based on modern standards.

This is very much outside of this scope.

Moreover, like I've said, there is nothing better about having such listener: you define prototypes, the moment an element is ready is the moment createdCallback is invoked for such element, and that is your entry point for such notification.

I hope you don't mind if I close this.

goldoraf commented 10 years ago

I fully understand your concern, you want to follow the spec as closely as possible, so do I : I'm the author of Bosonic, and my goal is to provide an alternative platform & components lib, more closer to the spec than Polymer. I'm currently rewriting the platform using your polyfill and a lightweight ShadowDOM polyfill I wrote (it will not try to simulate encapsulation though, because Composition is the goal, and it's the main reason why Polymer's polyfill is so heavy). I plan to release this new version in the following weeks. So you see we are in the same boat ;)

The problem, which didn't have to be covered by the spec because it will not present with native impl., is : how to enable people to deal with the async nature of the polyfill ? Because createdCallback is good for components authors, but not for components users. Imagine a user that want to access the API of a custom element on DOMContentLoaded : he can't, the element will not have time to upgrade. It can be tricky too with unit testing : with Karma for example, you will have to add setTimeouts in each test to let the elements upgrade.

So I understand your point of conformance to the spec, but it'll be a pity if this forbids people to use your polyfill, and they go to Polymer instead (Polymer is already way too much present in the Web Components ecosystem...).

That's why I proposed to have some hooks (custom events ?) of some sort in your polyfill that would permit users to extend it in order to deal with the issue. If you don't want to follow the Polymer route with WebComponentsReady, no problem. But firing a few custom events is not necessarily a big deal imho.

I hope this clarifies my point of view (sorry for my bad english, I'm french unfortunately...), and I hope we will work together to offer a good alternative to Polymer for HTML fans ;)

WebReflection commented 10 years ago

in case you are interested, I've partially naively solved the encapsulation problem with restyle … back to the topic, this polyfill is a polyfill, not a platform or an alternative to Polymer … this is Custom Element polyfill.

If specs do not say a thing about such event, yes, you gonna end up polling elements on DOMContentLoaded waiting for element.created to be true.

I understand this is problematic but I also believe this is not the place to discuss about this.

What I can do, as components developer, is that the moment I want users to be able to be notified I'd rather give them such possibility putting a basic snippet inside the createdCallback:

createdCallback: function () {
  document.dispatchEvent(
    new CustomEvent(
      this.nodeName.toLowerCase() + ':created',
      {detail: this}
    )
  );
}

This will allow any user to put a listener on the document and be notified before the element is available but this must be done only if the component hasn't been created yet.

Unfortunately it's not clear to me if .created property should be true after the createCallback has been invoked, but if ('created' in node) {/* already registered, YEAH! */} should do the trick.

What do you think?

WebReflection commented 10 years ago

a listener example:

document.addEventListener(
  'my-custom-elem:created',
  function (e) {
    var node = e.detail;
    // do stuff
  }
);

On top of this I believe this would be an excellent case for .when() method proposed in eddy.js, where everything would be as easy as this:

createdCallback: function () {
  document.trigger(
    this.nodeName.toLowerCase() + ':created',
    this
  );
}

and everyone, at any time, even if that call already happened:

document.when(
   'my-custom-elem:created',
  function (e) {
    var node = e.details;
    // do stuff
  }
);

I wish eddy had more penetration online, still no idea what developers are waiting for to use it daily and drop other old libraries (or use both, they'll probably forget other libraries soon anyway :D)

WebReflection commented 10 years ago

actually forget about the eddy example, that should be confined in the node, not the document, or it won't work … damn it, we actually need some concrete Promise like behavior through events on the DOM - I still don't see how Polymer event would have solved this, specially with lazy loaded components.

kentaromiura commented 10 years ago

iirc polymer has 2 custom events, 1 in the custom element polyfill (WebComponentsReady) and one in Polymer itself (polymer-ready)

Basically what polymer custom element polyfill does is to find all the custom-elements in the page that need upgrades and when all of them has been upgraded it will fire that event.

http://stackoverflow.com/questions/21763690/polymer-and-webcomponentsready-event

Unfortunately as @WebReflection already said here this is not specced. It probably should, but this is not a topic for this, I had this problem myself (especially for testing) and I ended up writing https://github.com/kentaromiura/custom-element/blob/master/utils/onElementReady.js for this, as I didn't trust the ready property (as there's the remotely possibility that it can be an expando)

WebReflection commented 10 years ago

you might have over engineered that method, here a simplified one:

document.onElementReady = function onElementReady(el, callback) {
  if (el instanceof HTMLUnknownElement) {
    return setTimeout(onElementReady, 100, el, callback);
  }
  callback.call(el);
};

No reason to have privileged/prioritized delays because in the DOM-tree the order matter, let the registerElement resolve in the right order for you or you might have problems.

Also, Object.getPrototypeOf is in every browser tested against this polyfill, IE8 isn't part of this list.

That bend said … to use this you need to own the node already so why don't you simply fallback via document listener if not initialized yet ?

This seems a cleaner approach to me:

document.onElementReady = function onElementReady(el, callback) {
  if (el instanceof HTMLUnknownElement) {
    var type = el.nodeName.toLowerCase() + ':created';
    return document.addEventListener(
      type,
      function created(e) {
        if (e.detail === el) {
          document.removeEventListener(type, created);
          callback.call(el);
        }
      }
    );
  }
  callback.call(el);
};

And notify, as shown before, in the element prototype, whenever you want to expose it to the users or not.

Thoughts?

kentaromiura commented 10 years ago

That will work as long as you're the creator of all the elements you're using which is not always the case and since you cannot enforce all the custom elements creator to fire that custom element you cannot really rely on it.

WebReflection commented 10 years ago

You cannot force them to not use HTMLUnknownElement as prototype neither …

kentaromiura commented 10 years ago

:-/

WebReflection commented 10 years ago

I think this is going to work, if only W3C would ever consider it.

HTMLElement.prototype.hasNotifiedCreation = false;

HTMLElement.prototype.notifyCreation = function () {
  if (!this.hasNotifiedCreation) {
    Object.defineProperty(
      this, 'hasNotifiedCreation', {value: true}
    );
    document.dispatchEvent(
      new CustomEvent(
        this.nodeName.toLowerCase() + ':created',
        {detail: this}
      )
    );
  }
};

HTMLElement.prototype.onceCreated = function (callback) {
  if (this.hasNotifiedCreation) {
    // allow bound callbacks, pass element as first param
    callback.call(this, this);
  } else {
    var
      self = this,
      type = self.nodeName.toLowerCase() + ':created',
      created = function (e) {
        if (e.detail === self) {
          document.removeEventListener(type, created);
          callback.call(self, self);
        }
      }
    ;
    document.addEventListener(type, created);
  }
};

It gives control over Custom Elements and libraries too, it's pretty simple to use, it's going to work in all cases, either the node was there or it has been created later on.

var XE = document.registerElement(
  'x-e',
  {
    prototype: Object.create(
      HTMLElement.prototype, {
      createdCallback: {value: function() {
        // prepare the node ... then
        this.notifyCreation();
      }}
    })
  }
);

A basic workflow would be:

var xe = new XE;
// we can do this later on
xe.onceCreated(function(){
  console.log('here I am');
});

The concept is very similar to Promise one, and the approach in my opinion way cleaner than a random WebComponentReady that does not mean much in my opinion.

This could semantically scale with onceAttached or onceDetached but having creation first is probably the most crucial one.