A user account is required in order to edit this wiki, but we've had to disable public user registrations due to spam.
To request an account, ask an autoconfirmed user on Chat (such as one of these permanent autoconfirmed members).
Component Model
Here's a good starting point for learning about the component model spec, which is currently under development (also see periodically updating gh-pages).
Introduction
The Component Model is an effort to formalize the concept of loosely coupled, highly coherent units of behavior in the Web platform.
Approach
- Capture requirements as use cases;
- Study use cases and extract a set of desired properties for these use cases;
- Come with a design that satisfies all desired properties;
- Extrude building blocks out of the design;
- Iterate until success criteria is reached.
Success criteria
- The use cases:
- reasonably cover a set of component model needs on the Web.
- The properties:
- completely describe the set of use cases;
- are orthogonal to each other.
- The building blocks:
- satisfy all desired properties;
- follow the component model design principles.
Design Principles
- Reuse existing mechanisms as much as possible, gently extending them if necessary;
- Study popular patterns in existing Web frameworks and pave the cowpaths when appropriate;
- Encourage each building block to stand on its own merits.
Properties
The component model aims to satisfy several key properties. These properties were extracted from the use cases. This section explains component model basics by studying each property.
Extensibility
The component model enables creation of new types of DOM elements by allowing the use of existing DOM elements in Javascript prototype chain. For example, here's how you would create a new sub-type of HTMLElement:
function LayoutManagerPanel() {
HTMLElement.call(this);
}
LayoutManagerPanel.prototype = Object.create(HTMLElement.prototype);
// ...
// 1) Will not be able to add instances of LayoutManagerPanel to document without this call.
// 2) First parameter specifies the tagName of the element being registered and must be a string prefixed with "x-".
Element.register("x-layout-manager-panel", LayoutManagerPanel);
// ...
var panel = new LayoutManagerPanel();
document.body.appendChild(panel);
// or
document.body.innerHTML = "<x-layout-manager-panel></x-layout-manager-panel>";
The resulting panel
instance is a Javascript object that is a valid DOM element, which can be added to the DOM tree. You can then extend this object using standard Javascript prototype inheritance.
Required Building Blocks:
Consistency
Because components are just DOM objects, they inherently share the same traversal and manipulation APIs, as defined by the DOM Core. The authors of components can extend these APIs by adding custom methods and properties on DOM objects, using standard Javascript inheritance:
Widget.prototype = Object.create(HTMLElement.prototype, {
update: {
value: function() { /* ... */ }
},
value: {
get: function() { /* ... */ },
set: function() { /* ... */ }
},
// ...
});
The common API surface and the ability to extend it serves as a natural API boundary for framework authors, enabling interoperability.
Required Building Blocks:
Encapsulation
Encapsulation refers to ability of the component to hide its implementation details and state from the document. To enable hiding of the implementation details, the component model provides a way to build a DOM tree that is not accessible from the document DOM tree, but is rendered as part of the document. This DOM tree, associated with the component is the shadow DOM. The boundary between the document DOM tree and shadow DOM tree provides complete encapsulation, and ensures that no information about the shadow DOM tree crosses this boundary. Additionally, the boundary serves as a convenient style application lever, giving the component a choice of letting (or not letting) document CSS affect the shadow DOM tree.
Every DOM element instance may only have (or host) one shadow DOM tree. A shadow tree is instantiated by creating an instance of the ShadowRoot
object, which takes a reference to the hosting element as a parameter. Attempting to create more than one instance for the same element throws an exception:
function LayoutManagerPanel() {
HTMLElement.call(this);
var shadow = new ShadowRoot(this);
var shadow2 = new ShadowRoot(this); // throws an exception.
// ...
}
Required Building Blocks:
Composability
Being DOM objects, components fit naturally into the document DOM tree and support all of its composition properties. In addition, the content
element allows controlling interaction between shadow and document DOM trees. A content
element specifies places where immediate document tree children of the component are rendered inside the shadow tree.
There can be more than one content
element in the shadow tree. The includes
attribute provides a convenient way to sort element's children by CSS selector. For example, a DockLayoutPanel
component could be used like this in the document DOM tree:
<x-dock-layout-panel>
<h1 class="north">On Gardens</h1>
<ul class="west">
<li>Automatic Gardens</li>
<li>Gardening on minefields</li>
</ul>
<p>I love gardening.</p>
<div class="south">Written by Chauncey Gardiner.</div>
</x-dock-layout-panel>
Provided that its shadow DOM tree looks like this:
<#shadow-root>
<div class="north">
<content includes=".north">
</div>
<div>
<div class="west">
<content includes=".west">
</div>
<div class="east">
<content>
</div>
</div>
<div class="south">
<content includes=".south">
</div>
<#shadow-root>
The document DOM tree children on of
x-dock-layout-panel
will be rendered as if composed from this tree:
<x-dock-layout-panel>
<div class="north">
<h1 class="north">On Gardens</h1>
</div>
<div>
<div class="west">
<ul class="west">
<li>Automatic Gardens</li>
<li>Gardening on minefields</li>
</ul>
</div>
<div class="east">
<p>I love gardening.</p>
</div>
</div>
<div class="south">
<div class="south">Written by Avid Gardener.</div>
</div>
</x-dock-layout-panel>
Required Building Blocks:
Desugaring
The component model also explains pretty well how the native HTML elements are built and dispels some of the magic associated with the "DOM object vs. Javascript object" juxtaposition.
Allowing DOM elements to participate in the Javascript inheritance chain makes DOM elements more approachable and easier to work with.
Complex DOM elements that are rendered with more than one CSS box (and aren't specified in terms of CSS, like lists) are just components that have shadow DOM. Coincidentally, this also explains why you can't add shadow DOM subtrees to input
or details
elements -- their ShadowRoot
s are claimed by HTMLInputElement
and HTMLDetailsElement
constructors, respectively.
Required Building Blocks:
Performance
Considering the way Web works, the component model must allow decoupling of the instantiation and the declaration of the components in order to provide reasonable performance characteristics. It's an unfortunate, but necessary requirement. In other words, we must be able to handle situations where a component instance is created before it is declared. Here's an simple example:
// somewhere in view.js
...
document.body.innerHTML = '<div class="awesome"><x-layout><x-spring-panel>...</x-spring-panel></x-layout>';
...
// somewhere in layout.js
Element.register('x-layout', Layout);
Element.register('x-spring-panel', SpringPanel);
In this situation, there is no room for error: view.js
must wait for layout.js
to load before executing. You can't load layout.js
lazily or in any different order, since it defines the components that are used in view.js
. Given that asynchronous, deferred or lazy-loading of script are all common performance techniques in today's Web, we must do better than block or throw an exception in such cases.
The component model offers this solution:
When unknown DOM element with an "x-"-prefixed tagName
is encountered, we put an placeholder HTMLUnknownElement
instance in its place. As soon as the element is defined, all placeholder instances are replaced (think replaceChild with the extra work of transferring children, attributes, and children) with the new, proper DOM element.
Required Building Blocks:
Confinement
Confinement refers to the document protecting its implementation details and state from the component and can be viewed as the inverse of encapsulation.
Required Building Blocks:
Building Blocks
The component model is comprised of the following building blocks.
Shadow DOM
Any DOM element can host a shadow DOM subtree. The shadow DOM subtree originates at ShadowRoot
, which is coupled the hosting element at the time of its construction. You don't need any other building blocks in order to take advantage of the shadow DOM:
var element = document.createElement("div");
var shadow = new ShadowRoot(element);
shadow.appendChild(document.createElement("p")).textContent = "weee!!';
A ShadowRoot
instance is a Node, and acts as the root of the element's shadow DOM subtree. The ShadowRoot
itself is never rendered, nor has styles. In this regard, it's similar to the DocumentFragment. It has two properties:
applyAuthorSheets
, which is either true (that is, apply author style sheets from the document), or false (don't);shadowHost
, which points to the hosting element.
Content Element
The content
element is used with Shadow DOM and provides a mechanism for distributing hosting element's children inside of its shadow subtree. To preserve the encapsulation property, the content
elements act as insertion points and do not leak any information about hosting element's children to the shadow DOM subtree or vise versa.
Constructable DOM Types
The inability to construct DOM element using new
(with some exceptions) or use them in the prototype inheritance chain had confounded many Web developers since the beginning of DOM. This building block intends to rectify at least the latter by allowing HTMLElement.call
invocation and thus enabling creation of Javascript objects with DOM elements in the prototype chain.
Registering Elements
Working in conjunction with Constructable DOM Types, this building block makes the list of valid markup tag names extensible by exposing Element.register
:
[Supplemental]
interface Element {
static void register(in String tagName, in Function constructor);
}
It is possible to envisage a milder (though less elegant) version of element registration by keeping DOM element construction magical (thus decoupling it from Constructable DOM Types) and making Element.register
use a callback, rather than constructor
as parameter. The callback would be invoked with an already-constructed DOM object with the specified tagName
, leaving the work of setting up properties on this object to the callback.
Confinement Primitives
The API surface of the component model lends itself well to proper confinement. Here's an approach that could be used to provide it (very early brainstorming):
- Confinement is not tied to the component model. Instead, it's a new twist on the method of loading scripts. A script could be loaded as usual or it could be confined, or loaded into its own context.
- When confined, a script has its own global and document objects. These objects only reveal some safe limited subset of the actual document objects. Think of it as a same-origin iframe with restrictions on document and window.
- You can communicate with the main document using
window.postMessage
.
- The confined document DOM tree is always empty. Attempting to insert into it throws exceptions.
- ... except when you append to elements in shadow DOM. That's right, you can do
Element.register
andnew ShadowRoot
in the confined document.
- Whenever you register an element, it registers in the main document as well. Creating an instance of a component from an confined document produces a functional DOM element shell that proxies to the actual element in the confined document.
Proposed API: introduce a new confined
attribute to the script
element. Presence of this attribute triggers loading scripts in the confined context.
Confinement of script execution could be useful outside of the component model and also could be related to Content Security Policy.
Implementation Sequence
Since the component model is composed of several building blocks, it is possible to implement it incrementally, gaining more insight with each step. Here's one possible sequence (tabulated against the use cases and approximate percentage of satisfying them):
Step | Layout Manager | Widget Mix-and-Match | SVG Controls | Contacts Widget | Like/+1 Button |
---|---|---|---|---|---|
Shadow DOM | 25% | - | 34% | 100% | 25% |
Content Element | 25% | - | - | - | - |
Constructable DOM Types | 25% | 50% | 33% | - | 25% |
Registering Elements | 25% | 50% | 33% | - | 25% |
Confinement Primitives | - | - | - | - | 25% |