Skip to content

Commit

Permalink
Implement angle-bracket components
Browse files Browse the repository at this point in the history
This commit adds support for angle-bracket components, including a
number of changes from curlies as discussed:

1. <my-component> is inserted with a tagName of `my-component` into the
   DOM. We plan to support an opt-out in the future (possibly along the
   line of the web-component `is=` feature).
2. Attributes specified as strings (with “quotation marks”) are inserted
   into the DOM as attributes. For the most part, this eliminates the
   need for `attributeBindings`.
3. Angle bracket components do not support attrs at the top-level (the
   entire attrs proxy functionality is disabled).
4. A number of other legacy behaviors are removed, such as `controller=`
   and string rendering.
5. Attributes are read-only values by default.

We plan to do a more aggressive disabling of legacy functionality; you
should assume that by the time Ember 1.13 ships, there will be no
deprecated functionality supported with angle bracket components.

From a high-level, angle bracket components are a coarse-grained opt-in
for Ember 2.0 functionality.
  • Loading branch information
Tom Dale and Yehuda Katz authored and rwjblue committed May 14, 2015
1 parent 9936dc4 commit 7194a5e
Show file tree
Hide file tree
Showing 8 changed files with 410 additions and 87 deletions.
11 changes: 10 additions & 1 deletion packages/ember-htmlbars/lib/hooks/component.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import ComponentNodeManager from "ember-htmlbars/node-managers/component-node-manager";

export default function componentHook(renderNode, env, scope, tagName, params, attrs, templates, visitor) {
export default function componentHook(renderNode, env, scope, _tagName, params, attrs, templates, visitor) {
var state = renderNode.state;

// Determine if this is an initial render or a re-render
Expand All @@ -9,6 +9,14 @@ export default function componentHook(renderNode, env, scope, tagName, params, a
return;
}

let tagName = _tagName;
let isAngleBracket = false;

if (tagName.charAt(0) === '<') {
tagName = tagName.slice(1, -1);
isAngleBracket = true;
}

var read = env.hooks.getValue;
var parentView = read(env.view);

Expand All @@ -18,6 +26,7 @@ export default function componentHook(renderNode, env, scope, tagName, params, a
attrs,
parentView,
templates,
isAngleBracket,
parentScope: scope
});

Expand Down
156 changes: 85 additions & 71 deletions packages/ember-htmlbars/lib/node-managers/component-node-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,17 @@ import setProperties from "ember-metal/set_properties";
import { MUTABLE_CELL } from "ember-views/compat/attrs-proxy";
import SafeString from "htmlbars-util/safe-string";
import { instrument } from "ember-htmlbars/system/instrumentation-support";
import EmberComponent from "ember-views/views/component";

// In theory this should come through the env, but it should
// be safe to import this until we make the hook system public
// and it gets actively used in addons or other downstream
// libraries.
import getValue from "ember-htmlbars/hooks/get-value";

function ComponentNodeManager(component, scope, renderNode, attrs, block, expectElement) {
function ComponentNodeManager(component, isAngleBracket, scope, renderNode, attrs, block, expectElement) {
this.component = component;
this.isAngleBracket = isAngleBracket;
this.scope = scope;
this.renderNode = renderNode;
this.attrs = attrs;
Expand All @@ -33,6 +35,7 @@ ComponentNodeManager.create = function(renderNode, env, options) {
attrs,
parentView,
parentScope,
isAngleBracket,
templates } = options;

attrs = attrs || {};
Expand All @@ -45,39 +48,40 @@ ComponentNodeManager.create = function(renderNode, env, options) {
return component || layout;
});

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

// Map passed attributes (e.g. <my-component id="foo">) to component
// properties ({ id: "foo" }).
configureCreateOptions(attrs, createOptions);
let createOptions = { parentView };

// 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);
}

// Instantiate the component
component = createComponent(component, createOptions, renderNode, env, attrs);
configureTagName(attrs, tagName, component, isAngleBracket, createOptions);

// 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;
// Map passed attributes (e.g. <my-component id="foo">) to component
// properties ({ id: "foo" }).
configureCreateOptions(attrs, createOptions);

extractPositionalParams(renderNode, component, params, attrs);
// 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);
}

var results = buildComponentTemplate({ layout: layout, component: component }, attrs, {
templates,
scope: parentScope
});
// Instantiate the component
component = createComponent(component, isAngleBracket, createOptions, renderNode, env, attrs);

// 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;

extractPositionalParams(renderNode, component, params, attrs);

var results = buildComponentTemplate(
{ layout, component, isAngleBracket }, attrs, { templates, scope: parentScope }
);

return new ComponentNodeManager(component, parentScope, renderNode, attrs, results.block, results.createdElement);
return new ComponentNodeManager(component, isAngleBracket, parentScope, renderNode, attrs, results.block, results.createdElement);
};

function extractPositionalParams(renderNode, component, params, attrs) {
Expand Down Expand Up @@ -132,12 +136,19 @@ function extractLegacyTemplate(_templates, componentTemplate) {
return templates;
}

function configureTagName(attrs, tagName, component, isAngleBracket, createOptions) {
if (isAngleBracket) {
createOptions.tagName = tagName;
} else if (attrs.tagName) {
createOptions.tagName = getValue(attrs.tagName);
}
}

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); }
}
Expand All @@ -148,32 +159,32 @@ ComponentNodeManager.prototype.render = function(_env, visitor) {
return instrument(component, function() {
let env = _env;

if (component) {
env = assign({ view: component }, env);
env = assign({ view: component }, env);

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

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

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

handleLegacyRender(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);
};

export function handleLegacyRender(component, element) {
if (!component.render) { return; }

Ember.assert("Legacy render functions are not supported with angle-bracket components", !component._isAngleBracket);

var content, node, lastChildIndex;
var buffer = [];
var renderNode = component._renderNode;
Expand All @@ -194,60 +205,63 @@ ComponentNodeManager.prototype.rerender = function(_env, attrs, visitor) {
return instrument(component, function() {
let env = _env;

if (component) {
env = assign({ view: component }, env);
env = assign({ view: component }, env);

var snapshot = takeSnapshot(attrs);
var snapshot = takeSnapshot(attrs);

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

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

component._renderNode.shouldReceiveAttrs = false;
}

// 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);
component._renderNode.shouldReceiveAttrs = false;
}

// 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(env, [], undefined, this.renderNode, this.scope, visitor);
}

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

return env;
}, this);
};


export function createComponent(_component, options, renderNode, env, attrs = {}) {
let snapshot = takeSnapshot(attrs);
let props = assign({}, options);
let hasSuppliedController = 'controller' in attrs; // 2.0TODO remove
export function createComponent(_component, isAngleBracket, _props, renderNode, env, attrs = {}) {
let props = assign({}, _props);

Ember.deprecate("controller= is deprecated", !hasSuppliedController);
if (!isAngleBracket) {
let hasSuppliedController = 'controller' in attrs; // 2.0TODO remove
Ember.deprecate("controller= is deprecated", !hasSuppliedController);

props.attrs = snapshot;
let snapshot = takeSnapshot(attrs);
props.attrs = snapshot;

// 2.0TODO deprecate and remove from angle components
let proto = _component.proto();
mergeBindings(props, shadowedAttrs(proto, snapshot));
let proto = _component.proto();
mergeBindings(props, shadowedAttrs(proto, snapshot));
} else {
props._isAngleBracket = true;
}

let component = _component.create(props);

if (options.parentView) {
options.parentView.appendChild(component);
// for the fallback case
component.container = component.container || env.container;

if (props.parentView) {
props.parentView.appendChild(component);

if (options.viewName) {
set(options.parentView, options.viewName, component);
if (props.viewName) {
set(props.parentView, props.viewName, component);
}
}

Expand Down
Loading

0 comments on commit 7194a5e

Please sign in to comment.