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