-
Notifications
You must be signed in to change notification settings - Fork 12
Convert to custom elements spec v1 #30
base: master
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for the delay - I've now got lots of time all of this week, so I should be able to get back to you with more notes much much quicker.
I've written up a whole bunch of comments. My largest concern is the extends-domino bits, which suggest strange things are going on somewhere, and separating the polyfill from our wrapper code.
Wrapping this all together and getting it out is still going to be a bunch of work, and I'm assuming you'd rather cover these tweaks yourself, but let me know if you'd like me to jump in and start making patches on this too.
All that said, this is a huge chunk of work though, the thrust is great, and I'm really glad you're pushing this forwards. Thanks so much for taking this on!
|
||
Creates a returns a new custom HTML element prototype, extending the HTMLElement prototype. | ||
|
||
Note that this does *not* register the element. To do that, call `components.registerElement` with an element name, and options (typically including the prototype returned here as your 'prototype' value). | ||
|
||
This is broadly equivalent to `Object.create(HTMLElement.prototype)` in browser land, and exactly equivalent here to `Object.create(components.dom.HTMLElement.prototype)`. You can call that yourself instead if you like, but it's a bit of a mouthful. | ||
|
||
#### `components.registerElement(componentName, options)` | ||
#### `components.customElements.define(componentName, Constructor)` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Server Components is more or less an implementation of custom elements. I think there's a pretty good argument for not separately namespacing the custom elements API with this as here, and just exposing it at the top-level. I.e. components.define(componentName, Constructor).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True, but the library also provides renderPage
and the like, which isn't conceptually a part of customElements
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True. It keeps it simpler and cleaner to have a single entry point though rather than nesting, so I'd prefer the API to be on the one object.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Although it's simpler, I argue it's not cleaner. The customElements
object would include extra, unrelated methods, the most notable of which being HTMLElement
. This will matter when isometric elements come into the picture.
I pushed the update to a different branch; see the readme to view the difference.
Another benefit is being able to destructure imports. Example:
var { customElements, HTMLElement } = require('server-components');
// OR
import { customElements, HTMLElement } from 'server-components';
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I'd still rather put them on the one object. customElements is just three methods (define, get, whenDefined), almost all users will use define
, and namespacing that separately from the rest of this API is annoying. A lot of users will really only be using define
and renderPage
, and they should be up front. Everybody else seems to do the same thing - SkateJS is the closest similar library, and uses skate.define()
and even Polymer has moved their [define-equivalent]((https://www.polymer-project.org/1.0/docs/devguide/registering-elements) to the top-level of the API. It's how people expect APIs to work - the key methods should be easily accessible and obvious.
On that branch, I don't think you've mapped your changes to exactly how my isometric version works, which might be part of the difference here. Your changes there have renamed the import, seemingly to try and emulate window.customElements. I don't think we want to do that (i.e. you can keep the import as components
everywhere). Any component that works with this library has to be aware of it first (by not using the global document object or DOM methods). It's fine to make that explicit and clear, and if we don't then we're shadowing the real customElements
, which potentially very bad behaviour (what if they have other non-server-component compatible web components on their page, and we break them?).
Given that, I don't think it affects the isometric version substantially (we have to map our API to the real methods, and it's easy to do this for the client-side: components = { define: window.customElements.define, HTMLElement: window.HTMLElement }
+ a v1 polyfill), and I'm fine with the resulting destructuring approach too - this way people don't need to understand the custom elements namespacing and they can just import define directly import { define, HTMLElement } from 'server-components'
.
I get that it doesn't match up exactly to reality, but I do think the end result is much nicer as a library interface. Does that difference in how we're looking at isometric elements help explain this?
|
||
Called when an element is created. | ||
Called when an element is attached to the faux DOM. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not a faux DOM, it's a real DOM.
Domino's a real DOM implementation, just one that doesn't happen to be tied to a layout engine. More practically, this API should hopefully be the same in server-components-for-web, where this same callback will be called after attachment to a completely normal browser DOM, and it'd be nice to be consistent.
@@ -131,9 +131,9 @@ These methods are methods you can implement on your component prototype (as retu | |||
|
|||
Any methods that are implemented, from this selection or otherwise, will be exposed on your element in the DOM during rendering. I.e. you can call `document.querySelector("my-element").setTitle("New Title")` and to call the `setTitle` method on your object, which can then potentially change how your component is rendered. | |||
|
|||
#### `yourComponent.createdCallback(document)` | |||
#### `yourComponentConstructor.prototype.createdCallback(document)` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be connectedCallback
, right?
@@ -27,19 +27,19 @@ You can take the same ideas (and standards), apply them directly server side, to | |||
var components = require("server-components"); | |||
|
|||
// Get the prototype for a new element |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment is sort-of still true, but doesn't really line up with what's happening here. "Define the class..." is much clearer.
"after": false, | ||
"afterEach": false, | ||
"expect": false | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting. This makes perfect sense, and I'm not sure why been it's passing without it all this time! Any idea? Right now, it seems to pass fine on my machine and in CI without this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding "use strict" to the top of the test files caused the linter to start complaining.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, that'll do it. 👍
// | ||
// Helpers | ||
// | ||
function map (arrayLike, fn) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's neater to just convert the array-like into a real array, and then use real map, rather than reimplementing map and any other functions we need all from scratch. function asArray(arrayLike) { return [].slice.apply(arrayLike) }
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's neater, but also creates an two extra arrays (an empty one and a copy for the actual mapping).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can do Array.prototype.slice instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alright, updated.
}; | ||
var task = visitedNodes.has(currentNode) ? undefined : callback(currentNode); | ||
|
||
visitedNodes.add(currentNode); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is visitedNodes
here? It's only useful if we've got loops in the tree, but this is a DOM, so afaik there's no way we can have loops. Is there a case where this is necessary?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was in the original polyfill. I believe it's possible if a custom element decides to move itself around within the DOM.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, ok. Yes, that makes perfect sense.
const definition = customElements.getDefinition(element.localName); | ||
|
||
if (definition) { | ||
if ( upgradedNodes.has(element[_upgradedProp]) ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think (from looking at upgradeElement) element[_upgradedProp]
is always either undefined or true. That means this test always fails, because the only thing we ever put in upgradedNodes
is elements. Am I missing something?
Feels like we're mixing two upgrade trackers here: a list of elements and a property on the elements. In fact, can't we drop both of them, and just consider an element upgraded if it already has the correct prototype set?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I must have mistranslated this from the polyfill. You're right in that there is excessive tracking going on here.
My guess on why they opted for setting a property is that comparing prototypes might be more expensive. But that's just a guess.
CustomElementRegistry.instance()._setNewInstance(element); | ||
new (definition.constructor)(); | ||
element[_upgradedProp] = true; | ||
console.assert(CustomElementRegistry.instance()._newInstance === null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we have this assert in-line? Should probably either be removed or become a test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was there from the polyfill. We can probably remove it.
@@ -0,0 +1,296 @@ | |||
"use strict"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How much of this is the original real polyfill, and how much is extensions on top of it?
If at all possible (and I do get that it might not be) I'd like to keep them separate. It's going to be way easier to maintain this if there's a polyfill file that we can trust (and upgrade) independently, and then a separate bunch of code making the any tweaks required to hook it into Domino, and/or wrapping the polyfill to change the interface.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getDefinition
and undefine
are extensions. The rest of the changes were to make it compatible with domino instead of the actual DOM.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It'd be useful to keep all those changes separate from the core polyfill though, rather than mixing them all in together. If we can. If you've got examples where we can't possibly make the changes separately then that's ok too, but we should document those, so we can work out what's going on here in future.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But this sort of polyfill modifies the DOM environment. Normally it would modify window and window.document, but since there is no such globals, it modifies domino instead. In other words, modifying domino is a primary concern of the polyfill.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's fine that it modifies the DOM, my concern is that we're modifying the polyfill from within it, rather than externally, which makes this codebase harder to manage.
What I'm aiming to separate are the modifications to the DOM that any custom elements polyfill would do for any DOM, from both the modifications to Domino we make to get the polyfill working and the modifications to the polyfill we make to provide this library's API.
Some practical reasons:
- I'd like to be able to replace the polyfill file in future, to upgrade it without having to worry about recreating changes we've made within it.
- At some point hopefully Domino will get built-in custom elements support - I'll need to be able to remove this polyfill completely, and easily see what the extra parts we've built on top are (i.e. getDefinition) so they can be ported to build on top of Domino's implementation instead.
- It makes it much easier to review and manage. I shouldn't try and review the polyfill code here now - I should review extensions we're making on top. In future this remains true: if there's a bug in the polyfill, it should be fixed upstream, but if there's a bug in our polyfill wrapper, it should be fixed it here.
It's easier to do all that if we can keep the two as separate as possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The changes to the original polyfill were things like this:
const origHTMLElement = win.HTMLElement;
// TO
const origHTMLElement = domino.impl.HTMLElement;
which were contained in the same file. Are you suggesting we move this to a separate file? Sorry if I'm still misunderstanding.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Focusing on the use cases is the key part of this I think. Given the change you have there, if I wanted to pull in a new version of the polyfill, I'd have to recreate that change, and right now I'd have to do that totally by hand from scratch. That'll be painful, and I'd like to avoid this.
At the very least, some documentation of exactly what we've changed (even perhaps just as git history) would be a start, but separating our changes from the polyfill source entirely is probably doable, I hope. For example, there's other things we can do to wrap a library and inject our own window
into it, so you don't need to change the lines as you have there. See https://github.com/pimterry/leaflet-map-server-component/blob/master/src/leaflet-for-server.js, which injects window
, document
and navigator
globals into leaflet, to wrap it without changing the core leaflet code.
This might be a bit fiddly. Have a go, but if you really don't want to dig into this, feel free to commit an unchanged version of the polyfill followed by your changed version, and I'll extract the diff out as much as possible. That also conveniently lets me review the changes we're making to the polyfill, which is an important part of this too.
@@ -1,7 +1,6 @@ | |||
"use strict"; | |||
|
|||
var domino = require("domino"); | |||
var validateElementName = require("validate-element-name"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just spotted this - we should remove the dependency if we're not using this any more.
@@ -6,9 +6,11 @@ var components = require("../src/index.js"); | |||
describe("Programmatic usage", () => { | |||
|
|||
// Pending until we decide what we want from this | |||
xit("returns the element constructor from the registration call", () => { | |||
it("returns the element constructor from the registration call", () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test changes look great, but the name needs updating too.
}; | ||
var task = visitedNodes.has(currentNode) ? undefined : callback(currentNode); | ||
|
||
visitedNodes.add(currentNode); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, ok. Yes, that makes perfect sense.
Just an update to keep track of where we are. Outstanding issues are:
I think all the other comments are covered or dealt with (but let me know if I've missed something). |
1 and 2 are complete. For 3, I now understand what you mean by separating out the polyfill. However, server-components expands on the spec by looking at the return value of |
On part 1, in your latest commit we've still got the changes I mentioned here. We shouldn't be renaming the import to customElements everywhere. It should just be: var components = import("server-components");
class X extends components.HTMLElement { ... }
components.define(...) We're building a new API that's extremely easy to migrate to from custom elements, but we're definitely not emulating custom elements exactly (because that's not possible), and we shouldn't give that impression. |
On your part 3 comment:
There's ways we can do this from outside the polyfill. For example, we can easily externally override just _addElement on the registry to define our behaviour for that part instead: polyfill-extensions.js CustomElementRegistry.prototype._addElement = [do same thing, but think about promises too] Alternatively, we could add behavior inside our That way looks nicer actually: we use exactly the same polyfill logic during upgrade and element creation, but we automatically insert hooks into the callback interface to track their status when defining them. Conceptually tidy. Either way, changes like this makes it clear that we haven't changed the rest of the polyfill, makes it fairly obvious which changes we have made on top (i.e. everything is the same, but we use different behaviour at add-element time, or in component callbacks), and makes it easier to review this and change it in future. I think this probably works best with one component registry per render, which might actually be a good idea generally. I'm a bit cautious of sharing registries between renders, since there's lots of potential for weird interactions, and it's not really what any of this polyfill code was designed to do (whereas rendering a single document is exactly what it was designed for). Again, this is tricky! Let me know if you rather I dived in myself. |
Thank you for the explanation, I think I now understand what you mean. The original polyfill also had MutationObservers. Do you want to strip those out, provide a mock implementation, or wait for / make a PR for domino to implement them? |
Good point. I've actually got an open issue with domino looking at adding mutation observers, and they're interested, if we provide the implementation. This will be a bigger chunk of work, and I'll get on it this week, but it's certainly not going to be done today. If you assume for your changes here that MutationObserver works though, I'll try and get an implementation put together at least by the start of next week. Hope that doesn't block you too much in the meantime! |
A quick update on the status of this again. I think we're down to:
|
Hi I'm trying to follow the thread of where this PR is at. I take it the "Fix the extend-domino code" task is complete as per this merge? Does that just leave the "updating to properly use the real polyfill code" task? Is there anything I can help out with? |
I'm not maintaining this PR any more; I've found what I was looking for in Marko.js's upcoming v4 release. Feel free to take over. |
Closes #27
The implementation is almost a copy-paste of polymer's polyfill. The main differences is that I removed some unnecessary reactions and hooked in Domino.
If approved, a squash merge would probably be most appropriate.