Skip to content

Commit

Permalink
Call parser and attachedCallback() / detachedCallback() automatically
Browse files Browse the repository at this point in the history
on initial page load and as DOM nodes added/removed from the document.

For performance reasons, does not monitor the nodes created by
widgets themselves (or at least, not when widgets are instantiated via
parse()).  Therefore, the delite/Template code continues to propagate
attachedCallback() and detachedCallback() calls to subwidgets.

Since parsing and attachedCallback() / detachedCallback() happens asynchronously,
register has a new method call deliver() that will parse new widgets synchronously.
This is used in the tests to make sure parsing is complete before testing starts.
It can also be used by applications when the application needs to run
some code that depends on widgets being instantiated.

Fixes #392.
  • Loading branch information
wkeese committed Apr 1, 2015
1 parent 35fb04f commit e1b289c
Show file tree
Hide file tree
Showing 20 changed files with 406 additions and 138 deletions.
26 changes: 3 additions & 23 deletions CustomElement.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ define([
"./register"
], function (advise, dcl, Observable, Destroyable, Stateful, has, register) {

function nop() {}

/**
* Dispatched after the CustomElement has been attached.
* This is useful to be notified when an HTMLElement has been upgraded to a
Expand Down Expand Up @@ -226,10 +224,7 @@ define([
attached: false,

/**
* Called when the element is added to the document, after `createdCallback()` completes.
* Note though that for programatically created custom elements, the app must manually call
* this method.
*
* Called automatically when the element is added to the document, after `createdCallback()` completes.
* This method is automatically chained, so subclasses generally do not need to use `dcl.superCall()`,
* `dcl.advise()`, etc.
* @method
Expand All @@ -241,13 +236,6 @@ define([
// Do this in attachedCallback() rather than createdCallback() to avoid calling refreshRendering() etc.
// prematurely in the programmatic case (i.e. calling it before user parameters have been applied).
this.deliver();

// Protect against repeated calls.
this._realAttachedCallback = this.attachedCallback;
this.attachedCallback = nop;
if (this._realDetachedCallback) {
this.detachedCallback = this._realDetachedCallback;
}
},
after: function () {
this.attached = true;
Expand All @@ -260,20 +248,12 @@ define([
}),

/**
* Called when the element is removed the document. Note that the app must manually call this method.
*
* Called when the element is removed the document.
* This method is automatically chained, so subclasses generally do not need to use `dcl.superCall()`,
* `dcl.advise()`, etc.
*/
detachedCallback: function () {
if (this.attached) {
this.attached = false;

// Protect against repeated calls.
this._realDetachedCallback = this.detachedCallback;
this.detachedCallback = nop;
this.attachedCallback = this._realAttachedCallback;
}
this.attached = false;
},

/**
Expand Down
11 changes: 1 addition & 10 deletions docs/CustomElement.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,7 @@ myWidget.numProp = 123;
The initialization methods in `delite/CustomElement` correspond to the function names from the
Custom Elements specification, specifically `createdCallback()` and `attachedCallback()`.

When a custom element is instantiated via `register.parse()`, `createdCallback()` and `attachedCallback()` are
automatically called.
When a custom element is instantiated programatically, `createdCallback()` is automatically called,
but the application must call `attachedCallback()` manually.
Alternately, if you extend [`delite/Widget`](Widget.md), you can use the `placeAt()`
method, which will attach the element to the specified DOM node and also call `attachedCallback()`.
The requirement to manually call `attachedCallback()` is because, for performance reasons,
delite does not set up document level listeners for DOM nodes being attached / removed from the document.

Also, `delite/CustomElement` does not provide the `attributeChangedCallback()`, but you can
`delite/CustomElement` does not provide the `attributeChangedCallback()`, but you can
find out when properties change by declaring the properties in your element's prototype, and then reacting to changes
in `refreshRendering()`.

Expand Down
9 changes: 4 additions & 5 deletions docs/Widget.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,23 +33,22 @@ Programmatic creation is:
custom setters.
6. `attachedCallback()` callback.

`attachedCallback()` will be called automatically in the declarative case, and
when the widget was created programatically, then it can be triggered by calling
`Widget#placeAt(document.body)` (or specify any parent DOM node).
`attachedCallback()` will be called automatically, although asynchronously.

As mentioned above, there are currently five lifecycle methods which can be extended on the widget:
There are currently five lifecycle methods which can be extended on the widget:

1. `preRender()`
2. `render()`
3. `postRender()`
4. `attachedCallback()`
5. `destroy()`
5. `detachedCallback()`

Note that all of these methods except `render()` are automatically chained,
so you don't need to worry about setting up code to call the superclasses' methods.

Also, note that widget authors don't typically extend `render()` directly, but rather
specify the `template` property. See the [`handlebars!`](handlebars.md) documentation for more details.

## Placement

Delite widgets are DOM Custom Elements. That means they can be placed and manipulated just like other DOM elements.
Expand Down
14 changes: 8 additions & 6 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,14 @@ Custom Elements extend [`decor/Stateful`](/decor/docs/0.5.0/Stateful.html).
See the decor [design documentation](/decor/docs/0.5.0/architecture.html) for details about how that class avoids
polling / dirty checking for property changes.

Also, we intentionally don't set up page level listeners for custom element creation/deletion.
The listeners could be a bottleneck for applications that create thousands of DOM nodes on the fly.
Think of applications drawing charts in SVG, or quickly paging/scrolling through a table with
lots of data. As a consequence to this, you must call `.parse()` on page load.

Another decision decision was to not shim shadow DOM. While shadow DOM a nice concept, it takes lots of code to shim,
Although we set up page level listeners for custom elements being attached/detached from the document, the listeners are
disabled as widgets are being instantiated. This prevents a performance issue for widgets that internally
create lots of elements, like charts.
Therefore, custom elements that create other custom elements are responsible for creating those
custom elements via javascript (`new MyWidget(...)`), and then calling `attachedCallback()` at the appropriate time.
Note however that this is handled automatically for widgets in templates.

Another decision was to not shim shadow DOM. While shadow DOM a nice concept, it takes lots of code to shim,
and we felt the download cost outweighed the benefit.

## register() implementation details
Expand Down
25 changes: 11 additions & 14 deletions docs/customElements101.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,26 +91,23 @@ to the property's type.

### Parsing

In order for declarative custom elements to be instantiated on platforms without native custom element support,
you must call the parser:
"Parsing" refers to scanning the document for custom element usages (ex: `<my-widget></my-widget>`), and upgrading
those plain HTML elements to be proper custom elements (i.e. setting up the prototype chain, and calling
`createdCallback()` and `attachedCallback()`).

```js
require(["delite/register", "requirejs-domready/domReady!"], function (register) {
register.parse();
});
```
When the document has finished loading, delite will do an initial parse.
Afterwards, if new custom elements are defined, delite will scan the document for any additional nodes that need to
be upgraded.

Note that on platforms *with* custom element support, the custom elements will be instantiated before
the call to `register.parse()`, and without any guaranteed order. Therefore, if your custom elements
depend on a global variable, like in the example above, you should make sure it is available before
the custom element is loaded. Therefore, you may need code like this:
So, custom elements will be instantiated without any guaranteed order, and without any guaranteed timing relative to
other javascript code running.
Therefore, if your custom elements depend on a global variable, like in the example above,
you should make sure it is available before the custom element is loaded. So you may need code like this:

```js
require(["dstore/Memory"], function (Memory) {
myGlobalVar = new Memory();
require(["delite/register", "requirejs-domready/domReady!"], function (register) {
register.parse();
});
require(["deliteful/List"]);
});
```
### Declarative Events
Expand Down
2 changes: 1 addition & 1 deletion docs/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ title: delite/migration
1. In markup, widgets look like `<d-star-rating foo=bar>` rather than
`<div data-dojo-type=delite/StarRating data-dojo-props="foo: bar">`.
For widgets that enhance an existing tag, syntax is `<button is="d-button">`.
2. Use `register.parse()` rather than `dojo/parser.parse()`. There's no `parseOnLoad:true` or auto-loading
2. Widgets are parsed automatically without needing a `parseOnLoad:true` flag. But there's no auto-loading
or `data-dojo-mixins`.
3. Since each widget defines and loads its own CSS, you don't need to manually include dijit.css or claro.css;
also, the theme is determined automatically so you don't need to add `class="claro"` to the `<body>` node.
Expand Down
5 changes: 0 additions & 5 deletions docs/register.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,11 +143,6 @@ open: register.after(function(){
})
```

## Parsing

If you've declared widgets in markup, then you need to instantiate them by calling `register.parse()`.

Eventually browsers will support custom elements natively, and then this step will not be necessary.

## Standards

Expand Down
6 changes: 2 additions & 4 deletions docs/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,10 @@ Using the source form is as simple as requiring the needed AMD modules using Req
require.config({
baseUrl: "bower_components"
});
require(["delite/register", "requirejs-domready/domReady!"], function (register) {
require(["delite/register"], function (register) {
register("my-element", [HTMLElement, Widget], {
//...
});
register.parse();
//...
});
```

Expand All @@ -48,7 +46,7 @@ corresponding layer and then the AMD modules as follows:
baseUrl: "bower_components"
});
require(["delite/layer"], function() {
require(["delite/register", "requirejs-domready/domReady!"], function (register) {
require(["delite/register"], function (register) {
//...
});
});
Expand Down
25 changes: 7 additions & 18 deletions docs/tutorial/beginner.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,14 +129,10 @@ by convention we define one custom element per module, and name them similarly.
If we view the generated sample `./samples/BlogPost.html`, we see the following JavaScript:

```js
require(["delite/register", "blogging-package/BlogPost"], function (register) {
register.parse();
require(["blogging-package/BlogPost"], function () {

This comment has been minimized.

Copy link
@lbod

lbod Apr 1, 2015

Member

I realise you've done lots of work here (obviously parse isn't needed now) and just noticed the doc changes.
I only noticed because I'm working on splitting up the tutorial and saw the merge conflict. Any way to better notify people of these changes? I did see the issue but didn't see any comments about docs or expected usage changing

This comment has been minimized.

Copy link
@wkeese

wkeese Apr 1, 2015

Author Member

Hmm, I thought I had covered it in the checkin comment, but we could write something more in the release notes. The short version is that generally you shouldn't call parse() manually anymore, and you only need to call deliver() in special cases. So normally you don't even need to require() delite/register anymore.

This comment has been minimized.

Copy link
@lbod

lbod Apr 2, 2015

Member

Yeh thats fine, it's just the fact the generator-delite-element will need to be updated and then i'll need to update all the branches in deliteful-tutorial
@cjolif @clmath thinking about this more, does generator-delite-element need to be updated first for 0.7 (before 0.7 is released) i.e. for https://github.com/ibm-js/generator-delite-element/blob/master/app/templates/_bower.json ?

Or is it safe to assume generator-delite-element will be updated and I work against 0.7.0-beta.3 delite in the meantime. Mainly I'm thinking about the runnable code in delite-tutorial and that ideally that should be run against 0.7 when I run the build.
It seems to me I need to update the runnable code once 0.7 is released?

This comment has been minimized.

Copy link
@cjolif

cjolif Apr 3, 2015

Contributor

generator-delite-element will definitely be updated. We might however have to delay a bit more 0.7 to let time for that.

This comment has been minimized.

Copy link
@lbod

lbod Apr 3, 2015

Member

@cjolif do you want me to update it?

This comment has been minimized.

Copy link
@wkeese

wkeese Apr 3, 2015

Author Member

FYI, I filed ibm-js/generator-delite-element#7 to track that. I assigned it to @cjolif but I'm sure he would appreciate it if you fixed it.

});
```

Declarative widget instances (those created via markup in the page) need to be parsed in order to kick off the lifecycle of creating the widget.


###Template
If we look at the template Yeoman just created `./BlogPost/BlogPost.html` we can see the following:

Expand Down Expand Up @@ -318,16 +314,14 @@ If you wanted to programmatically create a widget and also set the arbitrary HTM
`./samples/BlogPost.html` sample from:

```js
require(["delite/register", "blogging-package/BlogPost"], function (register) {
register.parse();
require(["blogging-package/BlogPost"], function () {
});
```

to the following:

```js
require(["delite/register", "blogging-package/BlogPost"], function (register, BlogPost) {
register.parse();
require(["blogging-package/BlogPost"], function (BlogPost) {
var anotherCustomElement = new BlogPost({value : 'The day after', publishDate : 'Nov 28th 2014', author : "My good self"});
anotherCustomElement.placeAt(document.body, 'last');
var containerNodeContent = "<b>boooooo</b> it's the day after, back to work soon :(" +
Expand All @@ -337,7 +331,6 @@ require(["delite/register", "blogging-package/BlogPost"], function (register, Bl
```
A helper function is provided by `delite/Widget` to place it somewhere in the DOM named `placeAt()`
(see the [documentation](https://github.com/ibm-js/delite/blob/master/docs/Widget.md#placement) for it's usage).
If you don't call `placeAt()` then programmatically created widget instances should call `attachedCallback()`.

If you refresh the page you can see how we've added this HTML to the `containerNode` of our widget programmatically.

Expand Down Expand Up @@ -388,8 +381,7 @@ shouldn't need to create anymore theme folders (the default bootstrap theme will
Update our existing `./samples/BlogPost.html` JavaScript content from:

```js
require(["delite/register", "blogging-package/BlogPost"], function (register, BlogPost) {
register.parse();
require(["blogging-package/BlogPost"], function (BlogPost) {
var anotherCustomElement = new BlogPost({value : 'The day after', publishDate : 'Nov 28th 2014', author : "My good self"});
anotherCustomElement.placeAt(document.body, 'last');
var containerNodeContent = "<b>boooooo</b> it's the day after, back to work soon :(" +
Expand All @@ -401,8 +393,7 @@ require(["delite/register", "blogging-package/BlogPost"], function (register, Bl
to:

```js
require(["delite/register", "blogging-package/BlogPost", "delite/theme!delite/themes/{{theme}}/global.css"], function (register, BlogPost) {
register.parse();
require(["blogging-package/BlogPost", "delite/theme!delite/themes/{{theme}}/global.css"], function (BlogPost) {
var anotherCustomElement = new BlogPost({value : 'The day after', publishDate : 'Nov 28th 2014', author : "My good self"});
anotherCustomElement.placeAt(document.body, 'last');
var containerNodeContent = "<b>boooooo</b> it's the day after, back to work soon :(" +
Expand Down Expand Up @@ -534,15 +525,13 @@ refreshRendering: function (props) {
Also let's update the `./samples/TitleWidget.html` JavaScript from:

```js
require(["delite/register", "title-package/TitleWidget"], function (register, TitleWidget) {
register.parse();
require(["title-package/TitleWidget"], function (TitleWidget) {
});
```
to add a programmatically created widget:

```js
require(["delite/register", "title-package/TitleWidget"], function (register, TitleWidget) {
register.parse();
require(["title-package/TitleWidget"], function (TitleWidget) {
var anotherTitleWidget = new TitleWidget({value : 'another custom element title'});
anotherTitleWidget.placeAt(document.body, 'last');
});
Expand Down
23 changes: 23 additions & 0 deletions features.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,29 @@ define(["requirejs-dplugins/has"], function (has) {
// Does platform have native support for document.registerElement() or a polyfill to simulate it?
has.add("document-register-element", typeof document !== "undefined" && !!document.registerElement);

// Test for how to monitor DOM nodes being inserted and removed from the document.
// For DOMNodeInserted events, there are two variations:
// "root" - just notified about the root of each tree added to the document
// "all" - notified about all nodes added to the document
has.add("MutationObserver", window.MutationObserver ? "MutationObserver" : window.WebKitMutationObserver ?
"WebKitMutationObserver" : "");
has.add("DOMNodeInserted", function () {
var root = document.createElement("div"),
child = document.createElement("div"),
sawRoot, sawChild;
root.id = "root";
child.id = "child";
function listener(event) {
if (event.target.id === "root") { sawRoot = true; }
if (event.target.id === "child") { sawChild = true; }
}
document.body.addEventListener("DOMNodeInserted", listener);
document.body.appendChild(root);
document.body.removeChild(root);
document.body.removeEventListener("DOMNodeInserted", listener);
return sawChild ? "all" : sawRoot ? "root" : "";
});

// Can we use __proto__ to reset the prototype of DOMNodes?
// It's not available on IE<11, and even on IE11 it makes the node's attributes
// (ex: node.attributes, node.textContent) disappear, so disabling it on IE11 too.
Expand Down
Loading

0 comments on commit e1b289c

Please sign in to comment.