Skip to content

Commit

Permalink
[FEATURE ember-contextual-components] Implement contextual component
Browse files Browse the repository at this point in the history
This feature moves previous component keyword implementation to
`element-component` and adds a `closure-component` that creates a cell
that closes over the component's path, positional parameters and hash
parameters.

These cells can be nested, merging the previous parameters with the
previous ones.
  • Loading branch information
Serabe committed Sep 18, 2015
1 parent 85f2cd4 commit 9e50864
Show file tree
Hide file tree
Showing 11 changed files with 677 additions and 88 deletions.
3 changes: 2 additions & 1 deletion features.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"ember-debug-handlers": true,
"ember-registry-container-reform": true,
"ember-routing-routable-components": null,
"ember-metal-ember-assign": null
"ember-metal-ember-assign": null,
"ember-contextual-components": null
}
}
2 changes: 2 additions & 0 deletions packages/ember-htmlbars/lib/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ import outlet from 'ember-htmlbars/keywords/outlet';
import unbound from 'ember-htmlbars/keywords/unbound';
import view from 'ember-htmlbars/keywords/view';
import componentKeyword from 'ember-htmlbars/keywords/component';
import elementComponent from 'ember-htmlbars/keywords/element-component';
import partial from 'ember-htmlbars/keywords/partial';
import input from 'ember-htmlbars/keywords/input';
import textarea from 'ember-htmlbars/keywords/textarea';
Expand All @@ -91,6 +92,7 @@ registerKeyword('with', withKeyword);
registerKeyword('outlet', outlet);
registerKeyword('unbound', unbound);
registerKeyword('component', componentKeyword);
registerKeyword('@element_component', elementComponent);
registerKeyword('partial', partial);
registerKeyword('input', input);
registerKeyword('textarea', textarea);
Expand Down
35 changes: 35 additions & 0 deletions packages/ember-htmlbars/lib/helpers/hash.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
@module ember
@submodule ember-templates
*/

/**
Use the `{{hash}}` helper to create a hash to pass as an option to your
components. This is specially useful for contextual components where you can
just yield a hash:
```handlebars
{{yield (hash
name='Sarah'
title=office
)}}
```
Would result in an object such as:
```js
{ name: 'Sarah', title: this.get('office') }
```
Where the `title` is bound to updates of the `office` property.
@method hash
@for Ember.Templates.helpers
@param {Object} options
@return {Object} Hash
@public
*/

export default function hashHelper(params, hash, options) {
return hash;
}
109 changes: 109 additions & 0 deletions packages/ember-htmlbars/lib/keywords/closure-component.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/**
@module ember
@submodule ember-templates
*/

import { symbol } from 'ember-metal/utils';
import BasicStream from 'ember-metal/streams/stream';
import { read } from 'ember-metal/streams/utils';
import { labelForSubexpr } from 'ember-htmlbars/hooks/subexpr';
import assign from 'ember-metal/assign';
import { processPositionalParams } from 'ember-htmlbars/utils/extract-positional-params';
import lookupComponent from 'ember-htmlbars/utils/lookup-component';

export const COMPONENT_REFERENCE = symbol('COMPONENT_REFERENCE');
export const COMPONENT_CELL = symbol('COMPONENT_CELL');
export const COMPONENT_PATH = symbol('COMPONENT_PATH');
export const COMPONENT_POSITIONAL_PARAMS = symbol('COMPONENT_POSITIONAL_PARAMS');
export const COMPONENT_HASH = symbol('COMPONENT_HASH');

let ClosureComponentStream = BasicStream.extend({
init(env, path, params, hash) {
this._env = env;
this._path = path;
this._params = params;
this._hash = hash;
this.label = labelForSubexpr([path, ...params], hash, 'component');
this[COMPONENT_REFERENCE] = true;
},
compute() {
return createClosureComponentCell(this._env, this._path, this._params, this._hash);
}
});

export default function closureComponent(env, [path, ...params], hash) {
let s = new ClosureComponentStream(env, path, params, hash);

s.addDependency(path);

// FIXME: If the stream invalidates on every params or hash change, then
// the {{component helper will be forces to rerender the whole component
// each time. Instead, these dependencies should not be required and the
// element component keyword should add the params and hash as dependencies
params.forEach(item => s.addDependency(item));
Object.keys(hash).forEach(key => s.addDependency(hash[key]));

return s;
}

function createClosureComponentCell(env, originalComponentPath, params, hash) {
let componentPath = read(originalComponentPath);

if (isComponentCell(componentPath)) {
return createNestedClosureComponentCell(componentPath, params, hash);
} else {
return createNewClosureComponentCell(env, componentPath, params, hash);
}
}

export function isComponentCell(component) {
return component && component[COMPONENT_CELL];
}

function createNestedClosureComponentCell(componentCell, params, hash) {
let positionalParams = componentCell[COMPONENT_POSITIONAL_PARAMS];

// This needs to be done in each nesting level to avoid raising assertions
processPositionalParams(null, positionalParams, params, hash);

return {
[COMPONENT_PATH]: componentCell[COMPONENT_PATH],
[COMPONENT_HASH]: mergeHash(componentCell[COMPONENT_HASH], hash),
[COMPONENT_POSITIONAL_PARAMS]: positionalParams,
[COMPONENT_CELL]: true
};
}

function createNewClosureComponentCell(env, componentPath, params, hash) {
let positionalParams = getPositionalParams(env.container, componentPath);

// This needs to be done in each nesting level to avoid raising assertions
processPositionalParams(null, positionalParams, params, hash);

return {
[COMPONENT_PATH]: componentPath,
[COMPONENT_HASH]: hash,
[COMPONENT_POSITIONAL_PARAMS]: positionalParams,
[COMPONENT_CELL]: true
};
}

/*
Returns the positional parameters for component `componentPath`.
If it has no positional parameters, it returns the empty array.
*/
function getPositionalParams(container, componentPath) {
if (!componentPath) { return []; }
let result = lookupComponent(container, componentPath);
let component = result.component;

if (component && component.positionalParams) {
return component.positionalParams;
} else {
return [];
}
}

export function mergeHash(original, updates) {
return assign(original, updates);
}
46 changes: 13 additions & 33 deletions packages/ember-htmlbars/lib/keywords/component.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
@submodule ember-templates
@public
*/
import assign from 'ember-metal/assign';
import Ember from 'ember-metal/core';
import { keyword } from 'htmlbars-runtime/hooks';
import closureComponent from 'ember-htmlbars/keywords/closure-component';

/**
The `{{component}}` helper lets you add instances of `Ember.Component` to a
Expand Down Expand Up @@ -52,38 +54,16 @@ import assign from 'ember-metal/assign';
@for Ember.Templates.helpers
@public
*/
export default {
setupState(lastState, env, scope, params, hash) {
let componentPath = env.hooks.getValue(params[0]);
return assign({}, lastState, { componentPath, isComponentHelper: true });
},

render(morph, ...rest) {
let state = morph.getState();

if (state.manager) {
state.manager.destroy();
export default function(morph, env, scope, params, hash, template, inverse, visitor) {
if (Ember.FEATURES.isEnabled('ember-contextual-components')) {
if (morph) {
keyword('@element_component', morph, env, scope, params, hash, template, inverse, visitor);
return true;
}

// Force the component hook to treat this as a first-time render,
// because normal components (`<foo-bar>`) cannot change at runtime,
// but the `{{component}}` helper can.
state.manager = null;

render(morph, ...rest);
},

rerender: render
};

function render(morph, env, scope, params, hash, template, inverse, visitor) {
let componentPath = morph.getState().componentPath;

// If the value passed to the {{component}} helper is undefined or null,
// don't create a new ComponentNode.
if (componentPath === undefined || componentPath === null) {
return;
return closureComponent(env, params, hash);
} else {
keyword('@element_component', morph, env, scope, params, hash, template, inverse, visitor);
return true;
}

env.hooks.component(morph, env, scope, componentPath, params, hash, { default: template, inverse }, visitor);
}

74 changes: 74 additions & 0 deletions packages/ember-htmlbars/lib/keywords/element-component.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import assign from 'ember-metal/assign';
import {
COMPONENT_PATH,
COMPONENT_POSITIONAL_PARAMS,
COMPONENT_HASH,
isComponentCell,
mergeHash,
} from './closure-component';
import { processPositionalParams } from 'ember-htmlbars/utils/extract-positional-params';

export default {
setupState(lastState, env, scope, params, hash) {
let componentPath = getComponentPath(params[0], env);
return assign({}, lastState, {
componentPath,
isComponentHelper: true
});
},

render(morph, ...rest) {
let state = morph.getState();

if (state.manager) {
state.manager.destroy();
}

// Force the component hook to treat this as a first-time render,
// because normal components (`<foo-bar>`) cannot change at runtime,
// but the `{{component}}` helper can.
state.manager = null;

render(morph, ...rest);
},

rerender: render
};

function getComponentPath(param, env) {
let path = env.hooks.getValue(param);
if (isComponentCell(path)) {
path = path[COMPONENT_PATH];
}
return path;
}

function render(morph, env, scope, [path, ...params], hash, template, inverse, visitor) {
let {
componentPath
} = morph.getState();

// If the value passed to the {{component}} helper is undefined or null,
// don't create a new ComponentNode.
if (componentPath === undefined || componentPath === null) {
return;
}

path = env.hooks.getValue(path);

if (isComponentCell(path)) {
let closureComponent = env.hooks.getValue(path);
let positionalParams = closureComponent[COMPONENT_POSITIONAL_PARAMS];

// This needs to be done in each nesting level to avoid raising assertions
processPositionalParams(null, positionalParams, params, hash);
params = [];
hash = mergeHash(closureComponent[COMPONENT_HASH], hash);
}

let templates = { default: template, inverse };
env.hooks.component(
morph, env, scope, componentPath,
params, hash, templates, visitor
);
}
5 changes: 5 additions & 0 deletions packages/ember-htmlbars/lib/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ import joinClassesHelper from 'ember-htmlbars/helpers/-join-classes';
import legacyEachWithControllerHelper from 'ember-htmlbars/helpers/-legacy-each-with-controller';
import legacyEachWithKeywordHelper from 'ember-htmlbars/helpers/-legacy-each-with-keyword';
import htmlSafeHelper from 'ember-htmlbars/helpers/-html-safe';
import hashHelper from 'ember-htmlbars/helpers/hash';
import DOMHelper from 'ember-htmlbars/system/dom-helper';
import Helper, { helper as makeHelper } from 'ember-htmlbars/helper';
import GlimmerComponent from 'ember-htmlbars/glimmer-component';
Expand All @@ -148,6 +149,10 @@ registerHelper('concat', concatHelper);
registerHelper('-join-classes', joinClassesHelper);
registerHelper('-html-safe', htmlSafeHelper);

if (Ember.FEATURES.isEnabled('ember-contextual-components')) {
registerHelper('hash', hashHelper);
}

if (Ember.ENV._ENABLE_LEGACY_VIEW_SUPPORT) {
registerHelper('-legacy-each-with-controller', legacyEachWithControllerHelper);
registerHelper('-legacy-each-with-keyword', legacyEachWithKeywordHelper);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ import { MUTABLE_CELL } from 'ember-views/compat/attrs-proxy';
import { instrument } from 'ember-htmlbars/system/instrumentation-support';
import LegacyEmberComponent from 'ember-views/components/component';
import GlimmerComponent from 'ember-htmlbars/glimmer-component';
import { Stream } from 'ember-metal/streams/stream';
import { readArray } from 'ember-metal/streams/utils';
import extractPositionalParams from 'ember-htmlbars/utils/extract-positional-params';
import { symbol } from 'ember-metal/utils';

// These symbols will be used to limit link-to's public API surface area.
Expand Down Expand Up @@ -110,58 +109,6 @@ ComponentNodeManager.create = function ComponentNodeManager_create(renderNode, e
return new ComponentNodeManager(component, isAngleBracket, parentScope, renderNode, attrs, results.block, results.createdElement);
};

function extractPositionalParams(renderNode, component, params, attrs) {
let positionalParams = component.positionalParams;

if (positionalParams) {
processPositionalParams(renderNode, positionalParams, params, attrs);
}
}

function processPositionalParams(renderNode, positionalParams, params, attrs) {
// 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
const isNamed = typeof positionalParams === 'string';

if (isNamed) {
processRestPositionalParameters(renderNode, positionalParams, params, attrs);
} else {
processNamedPositionalParameters(renderNode, positionalParams, params, attrs);
}
}

function processNamedPositionalParameters(renderNode, positionalParams, params, attrs) {
const paramsStartIndex = renderNode.getState().isComponentHelper ? 1 : 0;

for (let i = 0; i < positionalParams.length; i++) {
let param = params[paramsStartIndex + i];

assert(`You cannot specify both a positional param (at position ${i}) and the hash argument \`${positionalParams[i]}\`.`,
!(positionalParams[i] in attrs));

attrs[positionalParams[i]] = param;
}
}

function processRestPositionalParameters(renderNode, positionalParamsName, params, attrs) {
// If there is already an attribute for that variable, do nothing
assert(`You cannot specify positional parameters and the hash argument \`${positionalParamsName}\`.`,
!(positionalParamsName in attrs));

const paramsStartIndex = renderNode.getState().isComponentHelper ? 1 : 0;

let paramsStream = new Stream(() => {
return readArray(params.slice(paramsStartIndex));
}, 'params');

attrs[positionalParamsName] = paramsStream;

for (let i = paramsStartIndex; i < params.length; i++) {
let param = params[i];
paramsStream.addDependency(param);
}
}

function configureTagName(attrs, tagName, component, isAngleBracket, createOptions) {
if (isAngleBracket) {
Expand Down
Loading

0 comments on commit 9e50864

Please sign in to comment.