Skip to content

Commit

Permalink
Refine component lifecycle hooks
Browse files Browse the repository at this point in the history
This commit refines the lifecycle hooks to more completely support the
use-cases.

On first render (in order):

* `didInitAttrs` runs after `attrs` is guaranteed to be up to date. This
  is more reliable than trying to ensure that `attrs` are always
  available on `init`.
* `didReceiveAttrs` runs after `didInitAttrs` (it also runs on
  subsequent re-renders, which is useful for logic that is the same
  on all renders).
* `willRender` runs before the template is rendered. It runs when the
  template is updated for any reason (both initial and re-render, and
  regardless of whether the change was caused by an attrs change or
  re-render).
* `didInsertElement` runs after the template has rendered and the
  element is in the DOM.
* `didRender` runs after `didInsertElement` (it also runs on subsequent
  re-renders).

On re-render (in order):

* `didUpdateAttrs` runs when the attributes of a component have changed
  (but not when the component is re-rendered, via `component.rerender`,
  `component.set`, or changes in models or services used by the
  template).
* `didReceiveAttrs`, same as above.
* `willUpdate` runs when the component is re-rendering for any reason,
  including `component.rerender()`, `component.set()` or changes in
  models or services used by the template.
* `willRender`, same as above
* `didUpdate` runs after the template has re-rendered and the DOM is
  now up to date.
* `didRender`, same as above.

Note that a component is re-rendered whenever:

1. any of its attributes change
2. `component.set()` is called
3. `component.rerender()` is called
4. a property on a model or service used by the template has changed
   (including through computed properties).

Because of the Glimmer engine, these re-renders are fast, and avoid
unnecessary work.
  • Loading branch information
Tom Dale and Yehuda Katz authored and tilde-engineering committed May 12, 2015
1 parent 6af6fa4 commit 84c2875
Show file tree
Hide file tree
Showing 7 changed files with 262 additions and 228 deletions.
9 changes: 8 additions & 1 deletion packages/ember-htmlbars/lib/morphs/morph.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,16 @@ function EmberMorph(DOMHelper, contextualElement) {
this.HTMLBarsMorph$constructor(DOMHelper, contextualElement);

this.emberView = null;
this.emberComponent = null;
this.emberToDestroy = null;
this.streamUnsubscribers = null;

// A component can become dirty either because one of its
// attributes changed, or because it was re-rendered. If any part
// of the component's template changes through observation, it has
// re-rendered from the perpsective of the programming model. This
// flag is set to true whenever a component becomes dirty because
// one of its attributes changed, which also triggers the attribute
// update flag (didUpdateAttrs).
this.shouldReceiveAttrs = false;
}

Expand Down
197 changes: 108 additions & 89 deletions packages/ember-htmlbars/lib/node-managers/component-node-manager.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import merge from "ember-metal/merge";
import Ember from "ember-metal/core";
import { assign } from "ember-metal/merge";
import buildComponentTemplate from "ember-views/system/build-component-template";
import lookupComponent from "ember-htmlbars/utils/lookup-component";
import getCellOrValue from "ember-htmlbars/hooks/get-cell-or-value";
import { get } from "ember-metal/property_get";
import { set } from "ember-metal/property_set";
import setProperties from "ember-metal/set_properties";
import View from "ember-views/views/view";
import { MUTABLE_CELL } from "ember-views/compat/attrs-proxy";
import { instrument } from "ember-htmlbars/system/instrumentation-support";

Expand Down Expand Up @@ -45,63 +44,31 @@ ComponentNodeManager.create = function(renderNode, env, options) {
return component || layout;
});

//var componentInfo = { layout: found.layout };

if (component) {
let createOptions = { parentView };

// Some attrs are special and need to be set as properties on the component
// instance. Make sure we use getValue() to get them from `attrs` since
// they are still streams.
if (attrs.id) { createOptions.elementId = getValue(attrs.id); }
if (attrs.tagName) { createOptions.tagName = getValue(attrs.tagName); }
if (attrs._defaultTagName) { createOptions._defaultTagName = getValue(attrs._defaultTagName); }
if (attrs.viewName) { createOptions.viewName = getValue(attrs.viewName); }

if (component.create && parentScope && parentScope.self) {
createOptions._context = getValue(parentScope.self);
}
// Map passed attributes (e.g. <my-component id="foo">) to component
// properties ({ id: "foo" }).
configureCreateOptions(attrs, createOptions);

// If there is a controller on the scope, pluck it off and save it on the
// component. This allows the component to target actions sent via
// `sendAction` correctly.
if (parentScope.locals.controller) {
createOptions._controller = getValue(parentScope.locals.controller);
}

component = createOrUpdateComponent(component, createOptions, renderNode, env, attrs);

// Even though we looked up a layout from the container earlier, the
// component may specify a `layout` property that overrides that.
// The component may also provide a `template` property we should
// respect (though this behavior is deprecated).
let componentLayout = get(component, 'layout');
let componentTemplate = get(component, 'template');

if (componentLayout) {
layout = componentLayout;
// Instantiate the component
component = createComponent(component, createOptions, renderNode, env, attrs);

// There is no block template provided but the component has a
// `template` property.
if ((!templates || !templates.default) && componentTemplate) {
Ember.deprecate("Using deprecated `template` property on a Component.");
templates = { default: componentTemplate.raw };
}
} else if (componentTemplate) {
// If the component has a `template` but no `layout`, use the template
// as the layout.
layout = componentTemplate;
}

renderNode.emberView = component;
// If the component specifies its template via the `layout` or `template`
// properties instead of using the template looked up in the container, get
// them now that we have the component instance.
let result = extractComponentTemplates(component, templates);
layout = result.layout || layout;
templates = result.templates || templates;

if (component.positionalParams) {
// if the component is rendered via {{component}} helper, the first
// element of `params` is the name of the component, so we need to
// skip that when the positional parameters are constructed
let paramsStartIndex = renderNode.state.isComponentHelper ? 1 : 0;
let pp = component.positionalParams;
for (let i=0; i<pp.length; i++) {
attrs[pp[i]] = params[paramsStartIndex + i];
}
}
extractPositionalParams(renderNode, component, params, attrs);
}

var results = buildComponentTemplate({ layout: layout, component: component }, attrs, {
Expand All @@ -112,97 +79,150 @@ ComponentNodeManager.create = function(renderNode, env, options) {
return new ComponentNodeManager(component, parentScope, renderNode, attrs, results.block, results.createdElement);
};

ComponentNodeManager.prototype.render = function(env, visitor) {
function extractPositionalParams(renderNode, component, params, attrs) {
if (component.positionalParams) {
// if the component is rendered via {{component}} helper, the first
// element of `params` is the name of the component, so we need to
// skip that when the positional parameters are constructed
let paramsStartIndex = renderNode.state.isComponentHelper ? 1 : 0;
let pp = component.positionalParams;
for (let i=0; i<pp.length; i++) {
attrs[pp[i]] = params[paramsStartIndex + i];
}
}
}

function extractComponentTemplates(component, _templates) {
// Even though we looked up a layout from the container earlier, the
// component may specify a `layout` property that overrides that.
// The component may also provide a `template` property we should
// respect (though this behavior is deprecated).
let componentLayout = get(component, 'layout');
let componentTemplate = get(component, 'template');
let layout, templates;

if (componentLayout) {
layout = componentLayout;
templates = extractLegacyTemplate(_templates, componentTemplate);
} else if (componentTemplate) {
// If the component has a `template` but no `layout`, use the template
// as the layout.
layout = componentTemplate;
templates = _templates;
Ember.deprecate("Using deprecated `template` property on a Component.");
}

return { layout, templates };
}

// 2.0TODO: Remove legacy behavior
function extractLegacyTemplate(_templates, componentTemplate) {
let templates;

// There is no block template provided but the component has a
// `template` property.
if ((!templates || !templates.default) && componentTemplate) {
Ember.deprecate("Using deprecated `template` property on a Component.");
templates = { default: componentTemplate.raw };
} else {
templates = _templates;
}

return templates;
}

function configureCreateOptions(attrs, createOptions) {
// Some attrs are special and need to be set as properties on the component
// instance. Make sure we use getValue() to get them from `attrs` since
// they are still streams.
if (attrs.id) { createOptions.elementId = getValue(attrs.id); }
if (attrs.tagName) { createOptions.tagName = getValue(attrs.tagName); }
if (attrs._defaultTagName) { createOptions._defaultTagName = getValue(attrs._defaultTagName); }
if (attrs.viewName) { createOptions.viewName = getValue(attrs.viewName); }
}

ComponentNodeManager.prototype.render = function(_env, visitor) {
var { component, attrs } = this;

return instrument(component, function() {
let env = _env;

var newEnv = env;
if (component) {
newEnv = merge({}, env);
newEnv.view = component;
}
env = assign({ view: component }, env);

if (component) {
var snapshot = takeSnapshot(attrs);
env.renderer.setAttrs(this.component, snapshot);
env.renderer.willCreateElement(component);
env.renderer.willRender(component);
env.renderer.componentInitAttrs(this.component, snapshot);
env.renderer.componentWillRender(component);
env.renderedViews.push(component.elementId);
}

if (this.block) {
this.block(newEnv, [], undefined, this.renderNode, this.scope, visitor);
this.block(env, [], undefined, this.renderNode, this.scope, visitor);
}

if (component) {
var element = this.expectElement && this.renderNode.firstNode;
env.renderer.didCreateElement(component, element); // 2.0TODO: Remove legacy hooks.
env.renderer.willInsertElement(component, element);
env.renderer.didCreateElement(component, element);
env.renderer.willInsertElement(component, element); // 2.0TODO remove legacy hook
env.lifecycleHooks.push({ type: 'didInsertElement', view: component });
}
}, this);
};

ComponentNodeManager.prototype.rerender = function(env, attrs, visitor) {
ComponentNodeManager.prototype.rerender = function(_env, attrs, visitor) {
var component = this.component;

return instrument(component, function() {
let env = _env;

var newEnv = env;
if (component) {
newEnv = merge({}, env);
newEnv.view = component;
env = assign({ view: component }, env);

var snapshot = takeSnapshot(attrs);

// Notify component that it has become dirty and is about to change.
env.renderer.willUpdate(component, snapshot);

if (component._renderNode.shouldReceiveAttrs) {
env.renderer.updateAttrs(component, snapshot);
env.renderer.componentUpdateAttrs(component, component.attrs, snapshot);

// 2.0TODO: remove legacy semantics for angle-bracket semantics
setProperties(component, mergeBindings({}, shadowedAttrs(component, snapshot)));

component._renderNode.shouldReceiveAttrs = false;
}

env.renderer.willRender(component);
// Notify component that it has become dirty and is about to change.
env.renderer.componentWillUpdate(component, snapshot);
env.renderer.componentWillRender(component);

env.renderedViews.push(component.elementId);
}

if (this.block) {
this.block(newEnv, [], undefined, this.renderNode, this.scope, visitor);
this.block(env, [], undefined, this.renderNode, this.scope, visitor);
}

if (component) {
env.lifecycleHooks.push({ type: 'didUpdate', view: component });
}

return newEnv;
return env;
}, this);
};


export function createOrUpdateComponent(component, options, renderNode, env, attrs = {}) {
export function createComponent(_component, options, renderNode, env, attrs = {}) {
let snapshot = takeSnapshot(attrs);
let props = merge({}, options);
let defaultController = View.proto().controller;
let hasSuppliedController = 'controller' in attrs;
let props = assign({}, options);
let hasSuppliedController = 'controller' in attrs; // 2.0TODO remove

Ember.deprecate("controller= is deprecated", !hasSuppliedController);

props.attrs = snapshot;
if (component.create) {
let proto = component.proto();
mergeBindings(props, shadowedAttrs(proto, snapshot));
props.container = options.parentView ? options.parentView.container : env.container;

if (proto.controller !== defaultController || hasSuppliedController) {
delete props._context;
}
// 2.0TODO deprecate and remove from angle components
let proto = _component.proto();
mergeBindings(props, shadowedAttrs(proto, snapshot));

component = component.create(props);
} else {
mergeBindings(props, shadowedAttrs(component, snapshot));
setProperties(component, props);
}
let component = _component.create(props);

if (options.parentView) {
options.parentView.appendChild(component);
Expand All @@ -213,7 +233,6 @@ export function createOrUpdateComponent(component, options, renderNode, env, att
}

component._renderNode = renderNode;
renderNode.emberComponent = component;
renderNode.emberView = component;
return component;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,6 @@ export function createOrUpdateComponent(component, options, createOptions, rende
}

component._renderNode = renderNode;
renderNode.emberComponent = component;
renderNode.emberView = component;
return component;
}
Expand Down
5 changes: 5 additions & 0 deletions packages/ember-htmlbars/lib/utils/subscribe.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ export default function subscribe(node, scope, stream) {
unsubscribers.push(stream.subscribe(function() {
node.isDirty = true;

// Whenever a render node directly inside a component becomes
// dirty, we want to invoke the willRenderElement and
// didRenderElement lifecycle hooks. From the perspective of the
// programming model, whenever anything in the DOM changes, a
// "re-render" has occured.
if (component && component._renderNode) {
component._renderNode.isDirty = true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,11 @@ QUnit.test('non-block with properties on attrs and component class', function()

QUnit.test('rerendering component with attrs from parent', function() {
var willUpdate = 0;
var willReceiveAttrs = 0;
var didReceiveAttrs = 0;

registry.register('component:non-block', Component.extend({
willReceiveAttrs() {
willReceiveAttrs++;
didReceiveAttrs() {
didReceiveAttrs++;
},

willUpdate() {
Expand All @@ -109,20 +109,22 @@ QUnit.test('rerendering component with attrs from parent', function() {

runAppend(view);

equal(didReceiveAttrs, 1, "The didReceiveAttrs hook fired");

equal(jQuery('#qunit-fixture').text(), 'In layout - someProp: wycats');

run(function() {
view.set('someProp', 'tomdale');
});

equal(jQuery('#qunit-fixture').text(), 'In layout - someProp: tomdale');
equal(willReceiveAttrs, 1, "The willReceiveAttrs hook fired");
equal(didReceiveAttrs, 2, "The didReceiveAttrs hook fired again");
equal(willUpdate, 1, "The willUpdate hook fired once");

Ember.run(view, 'rerender');

equal(jQuery('#qunit-fixture').text(), 'In layout - someProp: tomdale');
equal(willReceiveAttrs, 2, "The willReceiveAttrs hook fired again");
equal(didReceiveAttrs, 3, "The didReceiveAttrs hook fired again");
equal(willUpdate, 2, "The willUpdate hook fired again");
});

Expand Down
Loading

0 comments on commit 84c2875

Please sign in to comment.