knockout/knockout — pull request #1385 by SteveSanderson (2014-06-11 12:34:38+00:00)
Title: Custom elements design spec

We've implemented components - hooray! Where they really shine, from my point of view, is when consumed as custom elements.

Having done this on a few projects, for example http://triage.knockoutjs.com/, I'm pretty confident that custom elements are a very satisfactory and natural way to consume components. It's short and easy for us to implement, and having this as a native feature helps maintain KO as a modern and fluent front-end library. Of course, it isn't (and shouldn't be) mandatory to use them, though!

A full implementation of all this is *much* shorter than the spec - probably 40-80 lines of JS :)

### Syntax

I've been considering and trying out two possible syntaxes:

**(A) With named attributes and Handlebars-style dynamic expressions:**

This is clean and natural, but it only makes sense if we are introducing [text interpolation](https://github.com/knockout/knockout/issues/1273) throughout templates more broadly.

**(B) With a `params` attribute and comma-separated expressions:**

This relies on no new syntax, and obviously parallels KO's existing `data-bind` syntax (and would make use of the same parsing code). It's perhaps a bit more explicit, since no dasherised/camelCase conversions are needed in parameter names.

From prototyping, I think either syntax works fine. Option A looks better at first glance, though B has fewer caveats and special cases.

### Parameter passing

In syntax A, the parameter names are taken from the attribute names, converted from dasherised to camel case (e.g., `some-param` is passed as `someParam`). This is a solution to the issue that HTML attribute names are case insensitive, and JavaScript object keys are annoying if they contain dashes. This is consistent with how other front-end frameworks deal with custom elements. Unrelated attributes (e.g., `id` or `class`) would be passed as string-literal params to the component, but they can just be ignored.

In syntax B, no parameter name conversions are needed, because the parameter names remain case-sensitive throughout. Unrelated attributes are not passed as parameters.

As for parameter values, there are a few cases:

1. Literal values (string, number, bool, etc.)

2. Dynamic expressions

2a. Some of which involve evaluating observables/computeds

2b. Some of which do not (e.g., supplying an observable instance directly, without unwrapping it)

With syntax A, case 1 is slightly prone to confusion, because it might not be obvious that you need to write `some-switch="{{ true }}"` instead of `some-switch="true"`, so the value arrives as a bool and not a string. But people would get used to it. With syntax B there's no such caveat.

In case 2a, there is the possibility that the value will observably change in the future. For perf and avoiding flicker, it's desirable *not* to tear down and rebuild the component whenever this happens. Custom elements presents us a perfect opportunity to solve this: in this case, the value we supply is a read-only `ko.computed` whose evaluator is the supplied expression, so the receiving component can react to its changes.

In case 2b, which most commonly happens when you supply a `ko.observable` instance as the value for a param, it's desirable *not* to wrap it in a further `ko.computed`, but instead to pass through the value directly. This avoids confusion and any need for double-unwrapping, and means the receiving component can choose to write changes back to the observable.

We can easily distinguish cases 2a and 2b by evaluating the expression inside a `ko.computed`, and then checking its `isActive()` result.

I've been really pleased with how perfectly this distinction between cases 2a and 2b maps to all the scenarios I've encountered. It means the developer controls whether or not any observables they pass can be written back to, and provides an easy technique to react to changes in the output from an arbitrary expression without having to construct intermediate computeds manually.

### Interface with binding system

How do we know which elements represent components (and which components they represent)? The most obvious default behavior is that an element is a component if and only if its name matches a preregistered component. This can be determined in `O(1)` time (merely by looking for the element `tagName` in the dictionary of components) so is not a perf concern for the binding system.

However, there's a good case for making this extensible without resorting to a custom binding provider. People who have custom component loaders, or people who want to impose rules on which components can be invoked as custom elements (or where on the page) will want an easy place to plug in this logic.

So, I propose the following extensibility point:

ko.components.getComponentNameForNode = function(node) {
// Default behavior just looks for node.tagName in preregistered components
// and returns it if found, else returns null. Override it if desired.
}

This also means you can disable custom elements completely if you want:

ko.components.getComponentNameForNode = null; // Or a function that always returns false

However I'm not sure many people will be putting `` in their markup if they didn't intend to use it.

### Browser compatibility

Implementing all this for HTML5-era browsers (in IE's case, that's IE9+) is pretty much trivial - about 30 lines of code.

For IE6-8, there's an annoyance to work around: in the main `document`, and in any document fragment, it will only parse `` in the markup if you've previously called `thatDocument.createElement('my-custom-elem')`. This is the same issue that you face when using HTML5 elements on IE6-8. We can make it all work transparently by detecting IE<9, and in that case,

* Whenever you call `ko.components.register(...)`, we automatically call `document.createElement(thatName)`. Then all preregistered components can appear in subsequently-parsed markup. Developers who aren't preregistering components, who also want to support IE<9 (probably quite a small minority) will need to take their own steps to call `document.createElement` for whatever custom elements they want to use.
* Also we'd wrap `document.createDocumentFragment` so that all newly-created doc frags also have support for the same set of custom elements. This is needed so that, for example, jQuery's HTML parsing logic (which works on a temporary doc frag) supports your custom elements.

There's the further option for KO core's custom elements feature to only support IE9+, with the IE<9 workarounds packaged as a separate plugin. That would be a significant decision for us to make, as so far we have 100% support (bugs excepted) for IE 6+ in core, so we shouldn't make such a decision lightly. Since the IE6-8 workarounds are not particularly difficult, and the code wouldn't even run on newer browsers, there isn't much drawback in including them. We could include notes in the docs that, if you target IE<9, it's your job to ensure custom components are registered *before* the browser parses any markup containing corresponding custom elements.

### Implementation and Prototypes

There's a prototype implementation in [`knockout-customElements.js`](https://github.com/SteveSanderson/knockout-triage/blob/custom-elems-use-params-attribs/src/js/lib/knockout-customElements.js) in the [knockout-triage sources](https://github.com/stevesanderson/knockout-triage/tree/master). See the version in the `custom-elems-use-params-attribs` branch for the most recent cut.

Note that the majority of the code in that prototype wouldn't be needed if it was simply part of KO core, as a lot of it deals with plugging into AMD, detecting IE version, wrapping the binding provider, etc.