diff --git a/packages/ember-htmlbars/lib/env.js b/packages/ember-htmlbars/lib/env.js index e0fd6fb1646..74c5bbd07d2 100644 --- a/packages/ember-htmlbars/lib/env.js +++ b/packages/ember-htmlbars/lib/env.js @@ -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'; @@ -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'; diff --git a/packages/ember-htmlbars/lib/hooks/attributes.js b/packages/ember-htmlbars/lib/hooks/attributes.js new file mode 100644 index 00000000000..c622eaa3a82 --- /dev/null +++ b/packages/ember-htmlbars/lib/hooks/attributes.js @@ -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$/); + + 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); } 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 5c680a85d61..cce197107fa 100644 --- a/packages/ember-htmlbars/lib/node-managers/component-node-manager.js +++ b/packages/ember-htmlbars/lib/node-managers/component-node-manager.js @@ -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 } ); diff --git a/packages/ember-htmlbars/tests/integration/component_invocation_test.js b/packages/ember-htmlbars/tests/integration/component_invocation_test.js index 826abdfb119..9c3ab497c88 100644 --- a/packages/ember-htmlbars/tests/integration/component_invocation_test.js +++ b/packages/ember-htmlbars/tests/integration/component_invocation_test.js @@ -906,229 +906,318 @@ QUnit.test('non-block with each rendering child components', function() { if (isEnabled('ember-htmlbars-component-generation')) { QUnit.module('component - invocation (angle brackets)', { - setup() { - commonSetup(); - }, + setup() { + commonSetup(); + }, - teardown() { - commonTeardown(); - } -}); + teardown() { + commonTeardown(); + } + }); - QUnit.test('non-block without properties', function() { - registry.register('template:components/non-block', compile('In layout')); + QUnit.test('non-block without properties replaced with a fragment when the content is just text', function() { + registry.register('template:components/non-block', compile('In layout')); - view = appendViewFor(''); + view = appendViewFor(''); - equal(view.$().text(), 'In layout'); - ok(view.$('non-block.ember-view').length === 1, 'The non-block tag name was used'); -}); + equal(view.$().html(), 'In layout', 'Just the fragment was used'); + }); - QUnit.test('block without properties', function() { - registry.register('template:components/with-block', compile('In layout - {{yield}}')); + QUnit.test('non-block without properties replaced with a fragment when the content is multiple elements', function() { + registry.register('template:components/non-block', compile('
This is a
fragment
')); - view = appendViewFor('In template'); + view = appendViewFor(''); - equal(view.$('with-block.ember-view').text(), 'In layout - In template', 'Both the layout and template are rendered'); -}); + equal(view.$().html(), '
This is a
fragment
', 'Just the fragment was used'); + }); - QUnit.test('non-block with properties on attrs', function() { - registry.register('template:components/non-block', compile('In layout')); + QUnit.test('non-block without properties replaced with a div', function() { + // The whitespace is added intentionally to verify that the heuristic is not "a single node" but + // rather "a single non-whitespace, non-comment node" + registry.register('template:components/non-block', compile('
In layout
')); - view = appendViewFor('', { - dynamic: 'dynamic' + view = appendViewFor(''); + + equal(view.$().text(), ' In layout '); + ok(view.$().html().match(/^
In layout<\/div> $/), 'The root element has gotten the default class and ids'); + ok(view.$('div.ember-view[id]').length === 1, 'The div became an Ember view'); + + run(view, 'rerender'); + + equal(view.$().text(), ' In layout '); + ok(view.$().html().match(/^
In layout<\/div> $/), 'The root element has gotten the default class and ids'); + ok(view.$('div.ember-view[id]').length === 1, 'The non-block tag name was used'); }); - let el = view.$('non-block.ember-view'); - ok(el, 'precond - the view was rendered'); - equal(el.attr('static-prop'), 'static text'); - equal(el.attr('concat-prop'), 'dynamic text'); - equal(el.attr('dynamic-prop'), undefined); + QUnit.test('non-block without properties replaced with identity element', function() { + registry.register('template:components/non-block', compile('In layout')); - //equal(jQuery('#qunit-fixture').text(), 'In layout - someProp: something here'); -}); + view = appendViewFor('', { + stability: 'stability' + }); - QUnit.test('attributes are not installed on the top level', function() { - let component; + let node = view.$()[0]; + equal(view.$().text(), 'In layout'); + ok(view.$().html().match(/^In layout<\/non-block>$/), 'The root element has gotten the default class and ids'); + ok(view.$('non-block.ember-view[id][such=stability]').length === 1, 'The non-block tag name was used'); - registry.register('template:components/non-block', compile('In layout - {{attrs.text}}')); - registry.register('component:non-block', Component.extend({ - text: null, - dynamic: null, + run(() => view.set('stability', 'stability!')); - didInitAttrs() { - component = this; - } - })); + strictEqual(view.$()[0], node, 'the DOM node has remained stable'); + equal(view.$().text(), 'In layout'); + ok(view.$().html().match(/^In layout<\/non-block>$/), 'The root element has gotten the default class and ids'); + }); + + QUnit.test('non-block with class replaced with a div merges classes', function() { + registry.register('template:components/non-block', compile('
')); - view = appendViewFor('', { - dynamic: 'dynamic' + view = appendViewFor('', { + outer: 'outer' + }); + + equal(view.$('div').attr('class'), 'inner-class outer ember-view', 'the classes are merged'); + + run(() => view.set('outer', 'new-outer')); + + equal(view.$('div').attr('class'), 'inner-class new-outer ember-view', 'the classes are merged'); }); - let el = view.$('non-block.ember-view'); - ok(el, 'precond - the view was rendered'); + QUnit.test('non-block with class replaced with a identity element merges classes', function() { + registry.register('template:components/non-block', compile('')); - equal(el.text(), 'In layout - texting'); - equal(component.attrs.text, 'texting'); - equal(component.attrs.dynamic, 'dynamic'); - strictEqual(get(component, 'text'), null); - strictEqual(get(component, 'dynamic'), null); + view = appendViewFor('', { + outer: 'outer' + }); - run(() => view.rerender()); + equal(view.$('non-block').attr('class'), 'inner-class outer ember-view', 'the classes are merged'); - equal(el.text(), 'In layout - texting'); - equal(component.attrs.text, 'texting'); - equal(component.attrs.dynamic, 'dynamic'); - strictEqual(get(component, 'text'), null); - strictEqual(get(component, 'dynamic'), null); -}); + run(() => view.set('outer', 'new-outer')); - QUnit.test('non-block with properties on attrs and component class', function() { - registry.register('component:non-block', Component.extend()); - registry.register('template:components/non-block', compile('In layout - someProp: {{attrs.someProp}}')); + equal(view.$('non-block').attr('class'), 'inner-class new-outer ember-view', 'the classes are merged'); + }); - view = appendViewFor(''); + QUnit.test('non-block rendering a fragment', function() { + registry.register('template:components/non-block', compile('

{{attrs.first}}

{{attrs.second}}

')); - equal(jQuery('#qunit-fixture').text(), 'In layout - someProp: something here'); -}); + view = appendViewFor('', { + first: 'first1', + second: 'second1' + }); - QUnit.test('rerendering component with attrs from parent', function() { - var willUpdate = 0; - var didReceiveAttrs = 0; + equal(view.$().html(), '

first1

second1

', 'No wrapping element was created'); - registry.register('component:non-block', Component.extend({ - didReceiveAttrs() { - didReceiveAttrs++; - }, + run(view, 'setProperties', { + first: 'first2', + second: 'second2' + }); - willUpdate() { - willUpdate++; - } - })); + equal(view.$().html(), '

first2

second2

', 'The fragment was updated'); + }); - registry.register('template:components/non-block', compile('In layout - someProp: {{attrs.someProp}}')); + QUnit.test('block without properties', function() { + registry.register('template:components/with-block', compile('In layout - {{yield}}')); - view = appendViewFor('', { - someProp: 'wycats' + view = appendViewFor('In template'); + + equal(view.$('with-block.ember-view').text(), 'In layout - In template', 'Both the layout and template are rendered'); }); - equal(didReceiveAttrs, 1, 'The didReceiveAttrs hook fired'); + QUnit.test('non-block with properties on attrs', function() { + registry.register('template:components/non-block', compile('In layout')); - equal(jQuery('#qunit-fixture').text(), 'In layout - someProp: wycats'); + view = appendViewFor('', { + dynamic: 'dynamic' + }); - run(function() { - view.set('someProp', 'tomdale'); + let el = view.$('non-block.ember-view'); + ok(el, 'precond - the view was rendered'); + equal(el.attr('static-prop'), 'static text'); + equal(el.attr('concat-prop'), 'dynamic text'); + equal(el.attr('dynamic-prop'), undefined); + + //equal(jQuery('#qunit-fixture').text(), 'In layout - someProp: something here'); }); - equal(jQuery('#qunit-fixture').text(), 'In layout - someProp: tomdale'); - equal(didReceiveAttrs, 2, 'The didReceiveAttrs hook fired again'); - equal(willUpdate, 1, 'The willUpdate hook fired once'); + QUnit.test('attributes are not installed on the top level', function() { + let component; - Ember.run(view, 'rerender'); + registry.register('template:components/non-block', compile('In layout - {{attrs.text}}')); + registry.register('component:non-block', Component.extend({ + text: null, + dynamic: null, - equal(jQuery('#qunit-fixture').text(), 'In layout - someProp: tomdale'); - equal(didReceiveAttrs, 3, 'The didReceiveAttrs hook fired again'); - equal(willUpdate, 2, 'The willUpdate hook fired again'); -}); + didInitAttrs() { + component = this; + } + })); + + view = appendViewFor('', { + dynamic: 'dynamic' + }); + + let el = view.$('non-block.ember-view'); + ok(el, 'precond - the view was rendered'); + + equal(el.text(), 'In layout - texting'); + equal(component.attrs.text, 'texting'); + equal(component.attrs.dynamic, 'dynamic'); + strictEqual(get(component, 'text'), null); + strictEqual(get(component, 'dynamic'), null); + + run(() => view.rerender()); + + equal(el.text(), 'In layout - texting'); + equal(component.attrs.text, 'texting'); + equal(component.attrs.dynamic, 'dynamic'); + strictEqual(get(component, 'text'), null); + strictEqual(get(component, 'dynamic'), null); + }); + + QUnit.test('non-block with properties on attrs and component class', function() { + registry.register('component:non-block', Component.extend()); + registry.register('template:components/non-block', compile('In layout - someProp: {{attrs.someProp}}')); + + view = appendViewFor(''); + + equal(jQuery('#qunit-fixture').text(), 'In layout - someProp: something here'); + }); + + QUnit.test('rerendering component with attrs from parent', function() { + var willUpdate = 0; + var didReceiveAttrs = 0; + + registry.register('component:non-block', Component.extend({ + didReceiveAttrs() { + didReceiveAttrs++; + }, + + willUpdate() { + willUpdate++; + } + })); + + registry.register('template:components/non-block', compile('In layout - someProp: {{attrs.someProp}}')); + + view = appendViewFor('', { + someProp: 'wycats' + }); + + 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(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(didReceiveAttrs, 3, 'The didReceiveAttrs hook fired again'); + equal(willUpdate, 2, 'The willUpdate hook fired again'); + }); QUnit.test('block with properties on attrs', function() { - registry.register('template:components/with-block', compile('In layout - someProp: {{attrs.someProp}} - {{yield}}')); + registry.register('template:components/with-block', compile('In layout - someProp: {{attrs.someProp}} - {{yield}}')); - view = appendViewFor('In template'); + view = appendViewFor('In template'); - equal(jQuery('#qunit-fixture').text(), 'In layout - someProp: something here - In template'); -}); + equal(jQuery('#qunit-fixture').text(), 'In layout - someProp: something here - In template'); + }); QUnit.test('moduleName is available on _renderNode when a layout is present', function() { - expect(1); + expect(1); - var layoutModuleName = 'my-app-name/templates/components/sample-component'; - var sampleComponentLayout = compile('Sample Component - {{yield}}', { - moduleName: layoutModuleName - }); - registry.register('template:components/sample-component', sampleComponentLayout); - registry.register('component:sample-component', Component.extend({ - didInsertElement: function() { - equal(this._renderNode.lastResult.template.meta.moduleName, layoutModuleName); - } - })); + var layoutModuleName = 'my-app-name/templates/components/sample-component'; + var sampleComponentLayout = compile('Sample Component - {{yield}}', { + moduleName: layoutModuleName + }); + registry.register('template:components/sample-component', sampleComponentLayout); + registry.register('component:sample-component', Component.extend({ + didInsertElement: function() { + equal(this._renderNode.lastResult.template.meta.moduleName, layoutModuleName); + } + })); - view = EmberView.extend({ - layout: compile(''), - container - }).create(); + view = EmberView.extend({ + layout: compile(''), + container + }).create(); - runAppend(view); -}); + runAppend(view); + }); QUnit.test('moduleName is available on _renderNode when no layout is present', function() { - expect(1); + expect(1); - var templateModuleName = 'my-app-name/templates/application'; - registry.register('component:sample-component', Component.extend({ - didInsertElement: function() { - equal(this._renderNode.lastResult.template.meta.moduleName, templateModuleName); - } - })); + var templateModuleName = 'my-app-name/templates/application'; + registry.register('component:sample-component', Component.extend({ + didInsertElement: function() { + equal(this._renderNode.lastResult.template.meta.moduleName, templateModuleName); + } + })); - view = EmberView.extend({ - layout: compile('{{#sample-component}}Derp{{/sample-component}}', { - moduleName: templateModuleName - }), - container - }).create(); + view = EmberView.extend({ + layout: compile('{{#sample-component}}Derp{{/sample-component}}', { + moduleName: templateModuleName + }), + container + }).create(); - runAppend(view); -}); + runAppend(view); + }); QUnit.test('parameterized hasBlock default', function() { - registry.register('template:components/check-block', compile('{{#if (hasBlock)}}Yes{{else}}No{{/if}}')); + registry.register('template:components/check-block', compile('{{#if (hasBlock)}}Yes{{else}}No{{/if}}')); - view = appendViewFor(' '); + view = appendViewFor(' '); - equal(view.$('#expect-yes-1').text(), 'Yes'); - equal(view.$('#expect-yes-2').text(), 'Yes'); -}); + equal(view.$('#expect-yes-1').text(), 'Yes'); + equal(view.$('#expect-yes-2').text(), 'Yes'); + }); QUnit.test('non-expression hasBlock ', function() { - registry.register('template:components/check-block', compile('{{#if hasBlock}}Yes{{else}}No{{/if}}')); + registry.register('template:components/check-block', compile('{{#if hasBlock}}Yes{{else}}No{{/if}}')); - view = appendViewFor(' '); + view = appendViewFor(' '); - equal(view.$('#expect-yes-1').text(), 'Yes'); - equal(view.$('#expect-yes-2').text(), 'Yes'); -}); + equal(view.$('#expect-yes-1').text(), 'Yes'); + equal(view.$('#expect-yes-2').text(), 'Yes'); + }); QUnit.test('parameterized hasBlockParams', function() { - registry.register('template:components/check-params', compile('{{#if (hasBlockParams)}}Yes{{else}}No{{/if}}')); + registry.register('template:components/check-params', compile('{{#if (hasBlockParams)}}Yes{{else}}No{{/if}}')); - view = appendViewFor(' '); + view = appendViewFor(' '); - equal(view.$('#expect-no').text(), 'No'); - equal(view.$('#expect-yes').text(), 'Yes'); -}); + equal(view.$('#expect-no').text(), 'No'); + equal(view.$('#expect-yes').text(), 'Yes'); + }); QUnit.test('non-expression hasBlockParams', function() { - registry.register('template:components/check-params', compile('{{#if hasBlockParams}}Yes{{else}}No{{/if}}')); + registry.register('template:components/check-params', compile('{{#if hasBlockParams}}Yes{{else}}No{{/if}}')); - view = appendViewFor(' '); + view = appendViewFor(' '); - equal(view.$('#expect-no').text(), 'No'); - equal(view.$('#expect-yes').text(), 'Yes'); -}); + equal(view.$('#expect-no').text(), 'No'); + equal(view.$('#expect-yes').text(), 'Yes'); + }); QUnit.test('implementing `render` allows pushing into a string buffer', function() { - expect(1); + expect(1); - registry.register('component:non-block', Component.extend({ - render(buffer) { - buffer.push('Whoop!'); - } - })); + registry.register('component:non-block', Component.extend({ + render(buffer) { + buffer.push('Whoop!'); + } + })); - expectAssertion(function() { - appendViewFor(''); + expectAssertion(function() { + appendViewFor(''); + }); }); -}); - } diff --git a/packages/ember-template-compiler/lib/main.js b/packages/ember-template-compiler/lib/main.js index 113f8d617a7..b8fafc725cf 100644 --- a/packages/ember-template-compiler/lib/main.js +++ b/packages/ember-template-compiler/lib/main.js @@ -17,6 +17,7 @@ import TransformComponentCurlyToReadonly from 'ember-template-compiler/plugins/t import TransformAngleBracketComponents from 'ember-template-compiler/plugins/transform-angle-bracket-components'; import TransformInputOnToOnEvent from 'ember-template-compiler/plugins/transform-input-on-to-onEvent'; import DeprecateViewAndControllerPaths from 'ember-template-compiler/plugins/deprecate-view-and-controller-paths'; +import TransformTopLevelComponents from 'ember-template-compiler/plugins/transform-top-level-components'; import DeprecateViewHelper from 'ember-template-compiler/plugins/deprecate-view-helper'; // used for adding Ember.Handlebars.compile for backwards compat @@ -34,6 +35,7 @@ registerPlugin('ast', TransformComponentAttrsIntoMut); registerPlugin('ast', TransformComponentCurlyToReadonly); registerPlugin('ast', TransformAngleBracketComponents); registerPlugin('ast', TransformInputOnToOnEvent); +registerPlugin('ast', TransformTopLevelComponents); registerPlugin('ast', DeprecateViewAndControllerPaths); registerPlugin('ast', DeprecateViewHelper); diff --git a/packages/ember-template-compiler/lib/plugins/transform-top-level-components.js b/packages/ember-template-compiler/lib/plugins/transform-top-level-components.js new file mode 100644 index 00000000000..399cafdd601 --- /dev/null +++ b/packages/ember-template-compiler/lib/plugins/transform-top-level-components.js @@ -0,0 +1,46 @@ +function TransformTopLevelComponents() { + // set later within HTMLBars to the syntax package + this.syntax = null; +} + +/** + @private + @method transform + @param {AST} The AST to be transformed. +*/ +TransformTopLevelComponents.prototype.transform = function TransformTopLevelComponents_transform(ast) { + hasSingleComponentNode(ast.body, component => { + component.tag = `@${component.tag}`; + }); + + return ast; +}; + +function hasSingleComponentNode(body, callback) { + let lastComponentNode; + let lastIndex; + let nodeCount = 0; + + for (let i=0, l=body.length; i 0) { return false; } + + if (curr.type === 'ComponentNode' || curr.type === 'ElementNode') { + lastComponentNode = curr; + lastIndex = i; + } + } + + if (!lastComponentNode) { return; } + + if (lastComponentNode.type === 'ComponentNode') { + callback(lastComponentNode); + } +} + +export default TransformTopLevelComponents; diff --git a/packages/ember-template-compiler/lib/system/compile_options.js b/packages/ember-template-compiler/lib/system/compile_options.js index 40017e42539..599570814f3 100644 --- a/packages/ember-template-compiler/lib/system/compile_options.js +++ b/packages/ember-template-compiler/lib/system/compile_options.js @@ -40,6 +40,7 @@ export default function(_options) { options.buildMeta = function buildMeta(program) { return { + topLevel: detectTopLevel(program), revision: 'Ember@VERSION_STRING_PLACEHOLDER', loc: program.loc, moduleName: options.moduleName @@ -48,3 +49,37 @@ export default function(_options) { return options; } + +function detectTopLevel(program) { + let { loc, body } = program; + if (!loc || loc.start.line !== 1 || loc.start.column !== 0) { return null; } + + let lastComponentNode; + let lastIndex; + let nodeCount = 0; + + for (let i=0, l=body.length; i 0) { return false; } + + if (curr.type === 'ComponentNode' || curr.type === 'ElementNode') { + lastComponentNode = curr; + lastIndex = i; + } + } + + if (!lastComponentNode) { return null; } + + if (lastComponentNode.type === 'ComponentNode') { + let tag = lastComponentNode.tag; + if (tag.charAt(0) !== '<') { return null; } + return tag.slice(1, -1); + } + + return null; +} diff --git a/packages/ember-views/lib/system/build-component-template.js b/packages/ember-views/lib/system/build-component-template.js index dcb19fa1e1f..e7d8ad26400 100644 --- a/packages/ember-views/lib/system/build-component-template.js +++ b/packages/ember-views/lib/system/build-component-template.js @@ -1,10 +1,12 @@ import Ember from 'ember-metal/core'; import { get } from 'ember-metal/property_get'; +import { assign } from 'ember-metal/merge'; import { isGlobal } from 'ember-metal/path_cache'; import { internal, render } from 'htmlbars-runtime'; import getValue from 'ember-htmlbars/hooks/get-value'; +import { isStream } from 'ember-metal/streams/utils'; -export default function buildComponentTemplate({ component, layout, isAngleBracket}, attrs, content) { +export default function buildComponentTemplate({ component, layout, isAngleBracket, isComponentElement, outerAttrs }, attrs, content) { var blockToRender, tagName, meta; if (component === undefined) { @@ -12,21 +14,25 @@ export default function buildComponentTemplate({ component, layout, isAngleBrack } if (layout && layout.raw) { + let attributes = (component && component._isAngleBracket) ? normalizeComponentAttributes(component, true, attrs) : undefined; + let yieldTo = createContentBlocks(content.templates, content.scope, content.self, component); - blockToRender = createLayoutBlock(layout.raw, yieldTo, content.self, component, attrs); + blockToRender = createLayoutBlock(layout.raw, yieldTo, content.self, component, attrs, attributes); meta = layout.raw.meta; } else if (content.templates && content.templates.default) { - blockToRender = createContentBlock(content.templates.default, content.scope, content.self, component); + let attributes = (component && component._isAngleBracket) ? normalizeComponentAttributes(component, true, attrs) : undefined; + blockToRender = createContentBlock(content.templates.default, content.scope, content.self, component, attributes); meta = content.templates.default.meta; } - if (component) { + if (component && !component._isAngleBracket || isComponentElement) { tagName = tagNameFor(component); // If this is not a tagless component, we need to create the wrapping // element. We use `manualElement` to create a template that represents // the wrapping element and yields to the previous block. if (tagName !== '') { + if (isComponentElement) { attrs = mergeAttrs(attrs, outerAttrs); } var attributes = normalizeComponentAttributes(component, isAngleBracket, attrs); var elementTemplate = internal.manualElement(tagName, attributes); elementTemplate.meta = meta; @@ -44,17 +50,28 @@ export default function buildComponentTemplate({ component, layout, isAngleBrack return { createdElement: !!tagName, block: blockToRender }; } +function mergeAttrs(innerAttrs, outerAttrs) { + let result = assign({}, innerAttrs, outerAttrs); + + if (innerAttrs.class && outerAttrs.class) { + result.class = ['subexpr', '-join-classes', [['value', innerAttrs.class], ['value', outerAttrs.class]], []]; + } + + return result; +} + function blockFor(template, options) { Ember.assert('BUG: Must pass a template to blockFor', !!template); return internal.blockFor(render, template, options); } -function createContentBlock(template, scope, self, component) { +function createContentBlock(template, scope, self, component, attributes) { Ember.assert('BUG: buildComponentTemplate can take a scope or a self, but not both', !(scope && self)); return blockFor(template, { - scope: scope, - self: self, + scope, + self, + attributes, options: { view: component } }); } @@ -75,9 +92,10 @@ function createContentBlocks(templates, scope, self, component) { return output; } -function createLayoutBlock(template, yieldTo, self, component, attrs) { +function createLayoutBlock(template, yieldTo, self, component, attrs, attributes) { return blockFor(template, { yieldTo, + attributes, // If we have an old-style Controller with a template it will be // passed as our `self` argument, and it should be the context for @@ -197,10 +215,10 @@ function normalizeClass(component, attrs) { var classNameBindings = get(component, 'classNameBindings'); if (attrs.class) { - if (typeof attrs.class === 'string') { - normalizedClass.push(attrs.class); - } else { + if (isStream(attrs.class)) { normalizedClass.push(['subexpr', '-normalize-class', [['value', attrs.class.path], ['value', attrs.class]], []]); + } else { + normalizedClass.push(attrs.class); } } diff --git a/tests/node/template-compiler-test.js b/tests/node/template-compiler-test.js index 9206a6c0128..3284f135638 100644 --- a/tests/node/template-compiler-test.js +++ b/tests/node/template-compiler-test.js @@ -2,12 +2,12 @@ var path = require('path'); var distPath = path.join(__dirname, '../../dist'); -var emberPath = path.join(distPath, 'ember.debug.cjs'); var templateCompilerPath = path.join(distPath, 'ember-template-compiler'); var module = QUnit.module; var ok = QUnit.ok; var equal = QUnit.equal; +var test = QUnit.test; var distPath = path.join(__dirname, '../../dist'); var templateCompiler = require(path.join(distPath, 'ember-template-compiler')); @@ -53,7 +53,7 @@ test('allows enabling of features', function() { templateCompiler._Ember.FEATURES['ember-htmlbars-component-generation'] = true; templateOutput = templateCompiler.precompile(''); - ok(templateOutput.indexOf('["component","",[],0]') > -1, 'component generation can be enabled'); + ok(templateOutput.indexOf('["component","@",[],0]') > -1, 'component generation can be enabled'); } else { ok(true, 'cannot test features in feature stripped build'); }