Hi all,

We have been discussing cross-orign use case and declarative syntax of web 
components internally at Apple, and here are our straw man proposal to amend 
the existing Web Components specifications to support it.

1. Modify HTML Imports to run scripts in the imported document itself
This allows the importee and the importer to not share the same script context, 
etc…

2. Add “importcomponents" content attribute on link element
It defines the list of custom element tag names to be imported from the 
imported HTML document.
e.g. <link rel="import" href="~" importcomponents="tag-1 tag-2"> will export 
custom elements of tag names "tag-1" and "tag-2" from ~.  Any name that didn't 
have a definition in the import document is ignored (i.e. if "tag-2" was not 
defined in ~, it would be skipped but "tag-1" will be still imported).

This mechanism prevents the imported document from defining arbitrary 
components in the host document.

3. Support "static" (write-once) binding of a HTML template
e.g.
<template id=cardTemplate>Name: {{name}}<br>Email:{{email}}</template>
<script>
document.body.appendChild(cardTemplate.instantiate({name: "Ryosuke Niwa", 
email:"rn...@webkit.org"}));
</script>

4. Add “interface" content attribute to template element
This content attribute specifies the name of the JavaScript constructor 
function to be created in the global scope. The UA creates one and will be used 
to instantiate a given custom element.  The author can then setup the prototype 
chain as needed:

<template defines="name-card" interface="NameCardElement">
Name: {{name}}<br>Email:{{email}}
</template>
<script>
NameCardElement.prototype.name = function () {...}
NameCardElement.prototype.email = function () {...}
</script>

This is similar to doing:
var NameCardElement = document.register(’name-card');

5. Add "defines" content attribute on HTML template element to define a custom 
element
This new attribute defines a custom element of the given name for the template 
content.
e.g. <template defines="nestedDiv"><div><div></div></div></template> will let 
you use <nestedDiv></nestedDiv>

We didn’t think having a separate custom element was useful because we couldn’t 
think of a use case where you wanted to define a custom element declaratively 
and not use template by default, and having to associate the first template 
element with the custom element seemed unnecessary complexity.

5.1. When a custom element is instantiated, automatically instantiate template 
inside a shadow root after statically binding the template with dataset
This allows statically declaring arguments to a component.
e.g.
<template defines="name-card">Name: {{name}}<br>Email:{{email}}</template>
<name-card data-name="Ryosuke Niwa" data-email="rn...@webkit.org”>

5.2. When a new custom element object is constructed, "created" callback is 
called with a shadow root
Unfortunately, we can't let the author define a constructor because the element 
hadn't been properly initialized with the right JS wrapper at the time of its 
construction.  So just like we can't do "new HTMLTitleElement", we're not going 
to let the author do an interesting things inside a custom element's 
constructor.  Instead, we're going to call "created" function on its prototype 
chain:

<template defines="name-card" interface="NameCardElement">
Name: {{name}}<br>Email:{{email}}
</template>
<script>
NameCardElement.prototype.name = function () {...}
NameCardElement.prototype.email = function () {...}
NameCardElement.prototype.created = function (shadowRoot) {
    ... // Initialize the shadowRoot here.
}
</script>

This is similar to the way document.register works in that document.register 
creates a constructor automatically.

6. The cross-origin component does not have access to the shadow host element, 
and the host document doesn’t have access to the element object.
When member functions of the element is called, “this” object will be 
undefined.  This is necessary because exposing the object to a cross-origin 
content will result in tricky security issues, forcing us to have proxy 
objects, etc…

Inside the document that imported a component, the element doesn’t use the 
prototype defined by the component as that exposes JS objects cross-origin.  
e.g. even if LikeButtonElement was defined in facebook.com/~/like-button.html, 
the document that uses this component wouldn’t see the prototype or the 
constructor.  It’ll be HTMLUnknownElement.  (We could create a new custom 
element type such as HTMLCrossOriginCustomElement if think that’s necessary).

7. Expose shadow host’s dataset on shadow root
This allows the component to communicate with the host document in a limited 
fashion without exposing the element directly.

This design allows us to have an iframe-like boundary between the shadow host 
(custom element itself) and the shadow root (implementation details), and 
address our cross-origin use case elegantly as follows:

rniwa.com/webkit.html
---------------------------------
<!DOCTYPE html>
<html>
<head>
<link rel=import href="https://webkit.org/components.html"; 
defines="share-button like-button">
</head>
<body>
<like-button data-url="https://build.webkit.org/";>Like 
build.webkit.org</like-button>
</body>
</html>

webkit.org/components.html
---------------------------------
<template defines="like-button" interface="LikeButtonElement">
<!-- implicitly does 
shadowRoot.appendChild(myTemplate.instantiate(shadowHost.dataset)); -->
<form ...>
<input type=hidden value="{{url}}">
<button type=submit>Like!</button>
</form>
<script>
LikeButtonElement.prototype.created = function (shadowRoot) {
    shadowRoot.query('form').onsubmit = function () {
        // ...
    }
}
</script>
</template>

- R. Niwa

Reply via email to