Skip to content

Commit

Permalink
Implements RFC #60 (Component Unification)
Browse files Browse the repository at this point in the history
emberjs/rfcs#60

This commit implements the proposed semantics for angle-bracket
components. The TLDR is that a component’s template represents its
“outerHTML” rather than its “innerHTML”, which makes it easier to
configure the element itself using templating constructs.

Some salient points:

1. If there is a single root element, the attributes from the invocation
   are copied onto the root element.
2. The invocation’s attributes “win out” over the attributes from the
   root element in the component’s layout.
3. Classes are merged. If the invocation says `class=“a”` and the root
   element says `class=“b”`, the result is `class=“a b”`. The rationale
   is that when you say `class=“a”`, you are really saying “add the `a`
   class to this element”.

A few sticky issues:

1. If the root element for the `my-foo` component is `<my-foo>`, we
   insert an element with tag name of `my-foo`. While this is intuitive,
   note that in all other cases, `<my-foo>` represents an invocation of
   the component. In the root position, that makes no sense, since it
   would inevitably produce a stack overflow.
   a. This “identity element” case makes it idiomatic to reflect the
      name of the component onto the DOM.
   b. Unfortunately, when we compile a template, we do not yet know
      what component that template is used for, and, indeed, whether it
      is even a template for a component at all.
   c. To capture the semantic differences, we do a bit of analysis at
      compile time (to determine *candidates* for top-level elements),
      and use runtime information (component invocation style and
      the name of the component looked up in the container) to
      disambiguate between a component’s element and an invocation of
      another component.
2. If the root element for the `my-foo` component is a regular HTML
   element, we use the `attachAttributes` functionality of HTMLBars to
   attach the attributes that the component was invoked with onto the
   root element.
3. In general, it is important that changes to attributes do not result
   in a change to the identity of the element that they are contained
   in. To achieve this, we reused much of the infrastructure built in
   `buildComponentTemplate`.

The consequence of (1) and (2) above is that the semantics are always
“a component’s layout represents its outerHTML”, even though the
implementation is quite different depending on whether the root element
is the same-named component or not.
  • Loading branch information
tomdale committed Jul 8, 2015
1 parent 1361d8a commit e7866ca
Show file tree
Hide file tree
Showing 10 changed files with 469 additions and 205 deletions.
50 changes: 26 additions & 24 deletions packages/ember-htmlbars/lib/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import lookupHelper from 'ember-htmlbars/hooks/lookup-helper';
import hasHelper from 'ember-htmlbars/hooks/has-helper';
import invokeHelper from 'ember-htmlbars/hooks/invoke-helper';
import element from 'ember-htmlbars/hooks/element';
import attributes from 'ember-htmlbars/hooks/attributes';

import helpers from 'ember-htmlbars/helpers';
import keywords, { registerKeyword } from 'ember-htmlbars/keywords';
Expand All @@ -38,30 +39,31 @@ var emberHooks = merge({}, hooks);
emberHooks.keywords = keywords;

merge(emberHooks, {
linkRenderNode: linkRenderNode,
createFreshScope: createFreshScope,
bindShadowScope: bindShadowScope,
bindSelf: bindSelf,
bindScope: bindScope,
bindLocal: bindLocal,
updateSelf: updateSelf,
getRoot: getRoot,
getChild: getChild,
getValue: getValue,
getCellOrValue: getCellOrValue,
subexpr: subexpr,
concat: concat,
cleanupRenderNode: cleanupRenderNode,
destroyRenderNode: destroyRenderNode,
willCleanupTree: willCleanupTree,
didCleanupTree: didCleanupTree,
didRenderNode: didRenderNode,
classify: classify,
component: component,
lookupHelper: lookupHelper,
hasHelper: hasHelper,
invokeHelper: invokeHelper,
element: element
linkRenderNode,
createFreshScope,
bindShadowScope,
bindSelf,
bindScope,
bindLocal,
updateSelf,
getRoot,
getChild,
getValue,
getCellOrValue,
subexpr,
concat,
cleanupRenderNode,
destroyRenderNode,
willCleanupTree,
didCleanupTree,
didRenderNode,
classify,
component,
lookupHelper,
hasHelper,
invokeHelper,
element,
attributes
});

import debuggerKeyword from 'ember-htmlbars/keywords/debugger';
Expand Down
50 changes: 50 additions & 0 deletions packages/ember-htmlbars/lib/hooks/attributes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { render, internal } from 'htmlbars-runtime';

export default function attributes(morph, env, scope, template, parentNode, visitor) {
let state = morph.state;
let block = state.block;

if (!block) {
let element = findRootElement(parentNode);
if (!element) { return; }

normalizeClassStatement(template.statements, element);

template.element = element;
block = morph.state.block = internal.blockFor(render, template, { scope });
}

block(env, [], undefined, morph, undefined, visitor);
}

function normalizeClassStatement(statements, element) {
let className = element.getAttribute('class');
if (!className) { return; }

for (let i=0, l=statements.length; i<l; i++) {
let statement = statements[i];

if (statement[1] === 'class') {
statement[2][2].unshift(className);
}
}
}

function findRootElement(parentNode) {
let node = parentNode.firstChild;
let found = null;

while (node) {
if (node.nodeType === 1) {
// found more than one top-level element, so there is no "root element"
if (found) { return null; }
found = node;
}
node = node.nextSibling;
}

let className = found && found.getAttribute('class');
if (!className || className.split(' ').indexOf('ember-view') === -1) {
return found;
}
}
50 changes: 36 additions & 14 deletions packages/ember-htmlbars/lib/hooks/component.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import ComponentNodeManager from 'ember-htmlbars/node-managers/component-node-manager';
import buildComponentTemplate from 'ember-views/system/build-component-template';

export default function componentHook(renderNode, env, scope, _tagName, params, attrs, templates, visitor) {
var state = renderNode.state;
Expand All @@ -11,25 +12,46 @@ export default function componentHook(renderNode, env, scope, _tagName, params,

let tagName = _tagName;
let isAngleBracket = false;
let isTopLevel;

if (tagName.charAt(0) === '<') {
tagName = tagName.slice(1, -1);
let angles = tagName.match(/^(@?)<(.*)>$/);

if (angles) {
tagName = angles[2];
isAngleBracket = true;
isTopLevel = !!angles[1];
}

var parentView = env.view;

var manager = ComponentNodeManager.create(renderNode, env, {
tagName,
params,
attrs,
parentView,
templates,
isAngleBracket,
parentScope: scope
});

state.manager = manager;
if (!isTopLevel || tagName !== env.view.tagName) {
var manager = ComponentNodeManager.create(renderNode, env, {
tagName,
params,
attrs,
parentView,
templates,
isAngleBracket,
isTopLevel,
parentScope: scope
});

state.manager = manager;
manager.render(env, visitor);
} else {
let component = env.view;
let templateOptions = {
component,
isAngleBracket: true,
isComponentElement: true,
outerAttrs: scope.attrs,
parentScope: scope
};

let contentOptions = { templates, scope };

let { block } = buildComponentTemplate(templateOptions, attrs, contentOptions);
block(env, [], undefined, renderNode, scope, visitor);
}

manager.render(env, visitor);
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ ComponentNodeManager.create = function(renderNode, env, options) {

extractPositionalParams(renderNode, component, params, attrs);

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

Expand Down
Loading

0 comments on commit e7866ca

Please sign in to comment.