diff --git a/features.json b/features.json index 8fdafc357b7..4d28a577677 100644 --- a/features.json +++ b/features.json @@ -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 } } diff --git a/packages/ember-htmlbars/lib/env.js b/packages/ember-htmlbars/lib/env.js index fc5ab4787c2..c3343a34e14 100644 --- a/packages/ember-htmlbars/lib/env.js +++ b/packages/ember-htmlbars/lib/env.js @@ -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'; @@ -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); diff --git a/packages/ember-htmlbars/lib/helpers/hash.js b/packages/ember-htmlbars/lib/helpers/hash.js new file mode 100644 index 00000000000..b464d8ac51d --- /dev/null +++ b/packages/ember-htmlbars/lib/helpers/hash.js @@ -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; +} diff --git a/packages/ember-htmlbars/lib/keywords/closure-component.js b/packages/ember-htmlbars/lib/keywords/closure-component.js new file mode 100644 index 00000000000..f1357b6c569 --- /dev/null +++ b/packages/ember-htmlbars/lib/keywords/closure-component.js @@ -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); +} diff --git a/packages/ember-htmlbars/lib/keywords/component.js b/packages/ember-htmlbars/lib/keywords/component.js index 675e93a4adf..81d6606bc02 100644 --- a/packages/ember-htmlbars/lib/keywords/component.js +++ b/packages/ember-htmlbars/lib/keywords/component.js @@ -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 @@ -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 (``) 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); } + diff --git a/packages/ember-htmlbars/lib/keywords/element-component.js b/packages/ember-htmlbars/lib/keywords/element-component.js new file mode 100644 index 00000000000..ebfcf5d7c84 --- /dev/null +++ b/packages/ember-htmlbars/lib/keywords/element-component.js @@ -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 (``) 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 + ); +} diff --git a/packages/ember-htmlbars/lib/main.js b/packages/ember-htmlbars/lib/main.js index 9bb1a3b98ca..b8123ffe5ac 100644 --- a/packages/ember-htmlbars/lib/main.js +++ b/packages/ember-htmlbars/lib/main.js @@ -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'; @@ -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); diff --git a/packages/ember-htmlbars/lib/node-managers/component-node-manager.js b/packages/ember-htmlbars/lib/node-managers/component-node-manager.js index b080199e125..aec3fa6228a 100644 --- a/packages/ember-htmlbars/lib/node-managers/component-node-manager.js +++ b/packages/ember-htmlbars/lib/node-managers/component-node-manager.js @@ -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. @@ -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) { diff --git a/packages/ember-htmlbars/lib/utils/extract-positional-params.js b/packages/ember-htmlbars/lib/utils/extract-positional-params.js new file mode 100644 index 00000000000..06d3698a7fe --- /dev/null +++ b/packages/ember-htmlbars/lib/utils/extract-positional-params.js @@ -0,0 +1,51 @@ +import { assert } from 'ember-metal/debug'; +import { Stream } from 'ember-metal/streams/stream'; +import { readArray } from 'ember-metal/streams/utils'; + +export default function extractPositionalParams(renderNode, component, params, attrs) { + let positionalParams = component.positionalParams; + + if (positionalParams) { + processPositionalParams(renderNode, positionalParams, params, attrs); + } +} + +export function processPositionalParams(renderNode, positionalParams, params, attrs) { + const isNamed = typeof positionalParams === 'string'; + + if (isNamed) { + processRestPositionalParameters(renderNode, positionalParams, params, attrs); + } else { + processNamedPositionalParameters(renderNode, positionalParams, params, attrs); + } +} + +function processNamedPositionalParameters(renderNode, positionalParams, params, attrs) { + let limit = Math.min(params.length, positionalParams.length); + + for (let i = 0; i < limit; i++) { + let param = params[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)); + + let paramsStream = new Stream(() => { + return readArray(params.slice(0)); + }, 'params'); + + attrs[positionalParamsName] = paramsStream; + + for (let i = 0; i < params.length; i++) { + let param = params[i]; + paramsStream.addDependency(param); + } +} diff --git a/packages/ember-htmlbars/tests/helpers/closure_component_test.js b/packages/ember-htmlbars/tests/helpers/closure_component_test.js new file mode 100644 index 00000000000..0de60191132 --- /dev/null +++ b/packages/ember-htmlbars/tests/helpers/closure_component_test.js @@ -0,0 +1,312 @@ +import Ember from 'ember-metal/core'; +import Registry from 'container/registry'; +import { runAppend, runDestroy } from 'ember-runtime/tests/utils'; +import ComponentLookup from 'ember-views/component_lookup'; +import Component from 'ember-views/components/component'; +import compile from 'ember-template-compiler/system/compile'; +import run from 'ember-metal/run_loop'; + +let component, registry, container; + +if (Ember.FEATURES.isEnabled('ember-contextual-components')) { + QUnit.module('ember-htmlbars: closure component helper', { + setup() { + registry = new Registry(); + container = registry.container(); + + registry.optionsForType('template', { instantiate: false }); + registry.register('component-lookup:main', ComponentLookup); + }, + + teardown() { + runDestroy(component); + runDestroy(container); + registry = container = component = null; + } + }); + + QUnit.test('renders with component helper', function() { + let expectedText = 'Hodi'; + registry.register( + 'template:components/-looked-up', + compile(expectedText) + ); + + let template = compile('{{component (component "-looked-up")}}'); + component = Component.extend({ container, template }).create(); + + runAppend(component); + equal(component.$().text(), expectedText, '-looked-up component rendered'); + }); + + QUnit.test('renders with component helper with invocation params, hash', function() { + let LookedUp = Component.extend(); + LookedUp.reopenClass({ + positionalParams: ['name'] + }); + registry.register( + 'component:-looked-up', + LookedUp + ); + registry.register( + 'template:components/-looked-up', + compile(`{{greeting}} {{name}}`) + ); + + let template = compile( + `{{component (component "-looked-up") "Hodari" greeting="Hodi"}}` + ); + component = Component.extend({ container, template }).create(); + + runAppend(component); + equal(component.$().text(), 'Hodi Hodari', '-looked-up component rendered'); + }); + + QUnit.test('renders with component helper with curried params, hash', function() { + let LookedUp = Component.extend(); + LookedUp.reopenClass({ + positionalParams: ['name'] + }); + registry.register( + 'component:-looked-up', + LookedUp + ); + registry.register( + 'template:components/-looked-up', + compile(`{{greeting}} {{name}}`) + ); + + let template = compile( + `{{component (component "-looked-up" "Hodari" greeting="Hodi") greeting="Hola"}}` + ); + component = Component.extend({ container, template }).create(); + + runAppend(component); + equal(component.$().text(), 'Hola Hodari', '-looked-up component rendered'); + }); + + QUnit.test('updates when component path is bound', function() { + let Mandarin = Component.extend(); + registry.register( + 'component:-mandarin', + Mandarin + ); + registry.register( + 'template:components/-mandarin', + compile(`ni hao`) + ); + registry.register( + 'template:components/-hindi', + compile(`Namaste`) + ); + + let template = compile('{{component (component lookupComponent)}}'); + component = Component.extend({ container, template }).create(); + + runAppend(component); + equal(component.$().text(), ``, 'undefined lookupComponent does not render'); + run(() => { + component.set('lookupComponent', '-mandarin'); + }); + equal(component.$().text(), `ni hao`, + 'mandarin lookupComponent renders greeting'); + run(() => { + component.set('lookupComponent', '-hindi'); + }); + equal(component.$().text(), `Namaste`, + 'hindi lookupComponent renders greeting'); + }); + + QUnit.test('updates when curried hash argument is bound', function() { + registry.register( + 'template:components/-looked-up', + compile(`{{greeting}}`) + ); + + let template = compile( + `{{component (component "-looked-up" greeting=greeting)}}` + ); + component = Component.extend({ container, template }).create(); + + runAppend(component); + equal(component.$().text(), '', '-looked-up component rendered'); + run(() => { + component.set('greeting', 'Hodi'); + }); + equal(component.$().text(), `Hodi`, + 'greeting is bound'); + }); + + QUnit.test('nested components overwrites named positional parameters', function() { + let LookedUp = Component.extend(); + LookedUp.reopenClass({ + positionalParams: ['name', 'age'] + }); + registry.register( + 'component:-looked-up', + LookedUp + ); + registry.register( + 'template:components/-looked-up', + compile(`{{name}} {{age}}`) + ); + + let template = compile( + `{{component + (component (component "-looked-up" "Sergio" 28) + "Marvin" 21) + "Hodari"}}` + ); + component = Component.extend({ container, template }).create(); + + runAppend(component); + equal(component.$().text(), 'Hodari 21', '-looked-up component rendered'); + }); + + QUnit.test('nested components overwrites hash parameters', function() { + registry.register( + 'template:components/-looked-up', + compile(`{{greeting}} {{name}} {{age}}`) + ); + + let template = compile( + `{{component (component (component "-looked-up" + greeting="Hola" name="Dolores" age=33) + greeting="Hej" name="Sigmundur") + greeting=greeting}}` + ); + component = Component.extend({ container, template, greeting: 'Hodi' }).create(); + + runAppend(component); + + equal(component.$().text(), 'Hodi Sigmundur 33', '-looked-up component rendered'); + }); + + QUnit.test('bound outer named parameters get updated in the right scope', function() { + let InnerComponent = Component.extend(); + InnerComponent.reopenClass({ + positionalParams: ['comp'] + }); + registry.register( + 'component:-inner-component', + InnerComponent + ); + registry.register( + 'template:components/-inner-component', + compile(`{{component comp "Inner"}}`) + ); + + let LookedUp = Component.extend(); + LookedUp.reopenClass({ + positionalParams: ['name', 'age'] + }); + registry.register( + 'component:-looked-up', + LookedUp + ); + registry.register( + 'template:components/-looked-up', + compile(`{{name}} {{age}}`) + ); + + let template = compile( + `{{component "-inner-component" (component "-looked-up" outerName outerAge)}}` + ); + component = Component.extend({ + container, + template, + outerName: 'Outer', + outerAge: 28 + }).create(); + + runAppend(component); + equal(component.$().text(), 'Inner 28', '-looked-up component rendered'); + }); + + QUnit.test('bound outer hash parameters get updated in the right scope', function() { + let InnerComponent = Component.extend(); + InnerComponent.reopenClass({ + positionalParams: ['comp'] + }); + registry.register( + 'component:-inner-component', + InnerComponent + ); + registry.register( + 'template:components/-inner-component', + compile(`{{component comp name="Inner"}}`) + ); + + let LookedUp = Component.extend(); + LookedUp.reopenClass({ + }); + registry.register( + 'component:-looked-up', + LookedUp + ); + registry.register( + 'template:components/-looked-up', + compile(`{{name}} {{age}}`) + ); + + let template = compile( + `{{component "-inner-component" (component "-looked-up" name=outerName age=outerAge)}}` + ); + component = Component.extend({ + container, + template, + outerName: 'Outer', + outerAge: 28 + }).create(); + + runAppend(component); + equal(component.$().text(), 'Inner 28', '-looked-up component rendered'); + }); + + QUnit.test('conflicting positional and hash parameters raise and assertion if in the same closure', function() { + let LookedUp = Component.extend(); + LookedUp.reopenClass({ + positionalParams: ['name'] + }); + registry.register( + 'component:-looked-up', + LookedUp + ); + registry.register( + 'template:components/-looked-up', + compile(`{{greeting}} {{name}}`) + ); + + let template = compile( + `{{component (component "-looked-up" "Hodari" name="Sergio") "Hodari" greeting="Hodi"}}` + ); + component = Component.extend({ container, template }).create(); + + expectAssertion(function() { + runAppend(component); + }, `You cannot specify both a positional param (at position 0) and the hash argument \`name\`.`); + }); + + QUnit.test('conflicting positional and hash parameters does not raise and assertion if in the different closure', function() { + let LookedUp = Component.extend(); + LookedUp.reopenClass({ + positionalParams: ['name'] + }); + registry.register( + 'component:-looked-up', + LookedUp + ); + registry.register( + 'template:components/-looked-up', + compile(`{{greeting}} {{name}}`) + ); + + let template = compile( + `{{component (component "-looked-up" "Hodari") name="Sergio" greeting="Hodi"}}` + ); + component = Component.extend({ container, template }).create(); + + runAppend(component); + equal(component.$().text(), 'Hodi Sergio', 'component is rendered'); + }); +} diff --git a/packages/ember-htmlbars/tests/helpers/hash_test.js b/packages/ember-htmlbars/tests/helpers/hash_test.js new file mode 100644 index 00000000000..3d1c3e80d34 --- /dev/null +++ b/packages/ember-htmlbars/tests/helpers/hash_test.js @@ -0,0 +1,73 @@ +import EmberView from 'ember-views/views/view'; +import isEnabled from 'ember-metal/features'; +import run from 'ember-metal/run_loop'; +import { set } from 'ember-metal/property_set'; +import compile from 'ember-template-compiler/system/compile'; +import { runAppend, runDestroy } from 'ember-runtime/tests/utils'; + +var view; + +if (isEnabled('ember-contextual-components')) { + QUnit.module('hash helper', { + setup() { + }, + + teardown() { + runDestroy(view); + } + }); + + QUnit.test('returns a hash with the right key-value', function() { + view = EmberView.create({ + template: compile('{{#with (hash name="Sergio") as |person|}}{{person.name}}{{/with}}') + }); + + runAppend(view); + + equal(view.$().text(), 'Sergio', 'shows literal value'); + }); + + QUnit.test('can have more than one key-value', function() { + view = EmberView.create({ + template: compile('{{#with (hash name="Sergio" lastName="Arbeo") as |person|}}{{person.name}} {{person.lastName}}{{/with}}') + }); + + runAppend(view); + + equal(view.$().text(), 'Sergio Arbeo', 'shows both literal values'); + }); + + QUnit.test('binds values when variables are used', function() { + view = EmberView.create({ + template: compile('{{#with (hash name=firstName lastName="Arbeo") as |person|}}{{person.name}} {{person.lastName}}{{/with}}'), + + context: { + firstName: 'Marisa' + } + }); + + runAppend(view); + + // Hello, mom + equal(view.$().text(), 'Marisa Arbeo', 'shows original variable value'); + + run(function() { + set(view, 'context.firstName', 'Sergio'); + }); + + equal(view.$().text(), 'Sergio Arbeo', 'shows new variable value'); + }); + + QUnit.test('hash helpers can be nested', function() { + view = EmberView.create({ + template: compile('{{#with (hash person=(hash name=firstName)) as |ctx|}}{{ctx.person.name}}{{/with}}'), + context: { + firstName: 'Balint' + } + }); + + runAppend(view); + + equal(view.$().text(), 'Balint', 'it gets the value from a nested hash'); + }); +}