diff --git a/packages/ember-application/tests/system/dependency_injection/default_resolver_test.js b/packages/ember-application/tests/system/dependency_injection/default_resolver_test.js index 49e42b419ba..47a14443b02 100644 --- a/packages/ember-application/tests/system/dependency_injection/default_resolver_test.js +++ b/packages/ember-application/tests/system/dependency_injection/default_resolver_test.js @@ -3,7 +3,7 @@ import run from 'ember-metal/run_loop'; import Logger from 'ember-metal/logger'; import Controller from 'ember-runtime/controllers/controller'; import Route from 'ember-routing/system/route'; -import Component from 'ember-views/views/component'; +import Component from 'ember-views/components/component'; import View from 'ember-views/views/view'; import Service from 'ember-runtime/system/service'; import EmberObject from 'ember-runtime/system/object'; diff --git a/packages/ember-htmlbars/lib/env.js b/packages/ember-htmlbars/lib/env.js index e67a73fb71c..ed19e76a481 100644 --- a/packages/ember-htmlbars/lib/env.js +++ b/packages/ember-htmlbars/lib/env.js @@ -28,7 +28,6 @@ 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'; @@ -62,8 +61,7 @@ merge(emberHooks, { lookupHelper, hasHelper, invokeHelper, - element, - attributes + element }); import debuggerKeyword from 'ember-htmlbars/keywords/debugger'; diff --git a/packages/ember-htmlbars/lib/glimmer-component.js b/packages/ember-htmlbars/lib/glimmer-component.js new file mode 100644 index 00000000000..e6120f76f2d --- /dev/null +++ b/packages/ember-htmlbars/lib/glimmer-component.js @@ -0,0 +1,26 @@ +import CoreView from 'ember-views/views/core_view'; +import ViewChildViewsSupport from 'ember-views/mixins/view_child_views_support'; +import ViewStateSupport from 'ember-views/mixins/view_state_support'; +import TemplateRenderingSupport from 'ember-views/mixins/template_rendering_support'; +import ClassNamesSupport from 'ember-views/mixins/class_names_support'; +import InstrumentationSupport from 'ember-views/mixins/instrumentation_support'; +import AriaRoleSupport from 'ember-views/mixins/aria_role_support'; +import ViewMixin from 'ember-views/mixins/view_support'; +import EmberView from 'ember-views/views/view'; + +export default CoreView.extend( + ViewChildViewsSupport, + ViewStateSupport, + TemplateRenderingSupport, + ClassNamesSupport, + InstrumentationSupport, + AriaRoleSupport, + ViewMixin, { + isComponent: true, + isGlimmerComponent: true, + + init() { + this._super(...arguments); + this._viewRegistry = this._viewRegistry || EmberView.views; + } + }); diff --git a/packages/ember-htmlbars/lib/hooks/attributes.js b/packages/ember-htmlbars/lib/hooks/attributes.js deleted file mode 100644 index 7c72636b117..00000000000 --- a/packages/ember-htmlbars/lib/hooks/attributes.js +++ /dev/null @@ -1,50 +0,0 @@ -import { render, internal } from 'htmlbars-runtime'; - -export default function attributes(morph, env, scope, template, parentNode, visitor) { - let state = morph.state; - let block = state.block; - - if (!block) { - let element = findRootElement(parentNode); - if (!element) { return; } - - normalizeClassStatement(template.statements, element); - - template.element = element; - block = morph.state.block = internal.blockFor(render, template, { scope }); - } - - block(env, [], undefined, morph, undefined, visitor); -} - -function normalizeClassStatement(statements, element) { - let className = element.getAttribute('class'); - if (!className) { return; } - - for (let i = 0, l = statements.length; i < l; i++) { - let statement = statements[i]; - - if (statement[1] === 'class') { - statement[2][2].unshift(className); - } - } -} - -function findRootElement(parentNode) { - let node = parentNode.firstChild; - let found = null; - - while (node) { - if (node.nodeType === 1) { - // found more than one top-level element, so there is no "root element" - if (found) { return null; } - found = node; - } - node = node.nextSibling; - } - - let className = found && found.getAttribute('class'); - if (!className || className.split(' ').indexOf('ember-view') === -1) { - return found; - } -} diff --git a/packages/ember-htmlbars/lib/hooks/bind-self.js b/packages/ember-htmlbars/lib/hooks/bind-self.js index b0fc5a598ad..2e3f09bb1ee 100644 --- a/packages/ember-htmlbars/lib/hooks/bind-self.js +++ b/packages/ember-htmlbars/lib/hooks/bind-self.js @@ -19,7 +19,13 @@ export default function bindSelf(env, scope, _self) { if (self && self.isView) { newStream(scope.locals, 'view', self, null); newStream(scope.locals, 'controller', scope.locals.view.getKey('controller')); - newStream(scope, 'self', scope.locals.view.getKey('context'), null, true); + + if (self.isGlimmerComponent) { + newStream(scope, 'self', self, null, true); + } else { + newStream(scope, 'self', scope.locals.view.getKey('context'), null, true); + } + return; } diff --git a/packages/ember-htmlbars/lib/hooks/bind-shadow-scope.js b/packages/ember-htmlbars/lib/hooks/bind-shadow-scope.js index f7a36741a29..4e15a3dad6b 100644 --- a/packages/ember-htmlbars/lib/hooks/bind-shadow-scope.js +++ b/packages/ember-htmlbars/lib/hooks/bind-shadow-scope.js @@ -3,8 +3,6 @@ @submodule ember-htmlbars */ -import Component from 'ember-views/views/component'; - export default function bindShadowScope(env, parentScope, shadowScope, options) { if (!options) { return; } @@ -16,7 +14,7 @@ export default function bindShadowScope(env, parentScope, shadowScope, options) } var view = options.view; - if (view && !(view instanceof Component)) { + if (view && !view.isComponent) { newStream(shadowScope.locals, 'view', view, null); if (!didOverrideController) { diff --git a/packages/ember-htmlbars/lib/hooks/component.js b/packages/ember-htmlbars/lib/hooks/component.js index 2a9a54ad42f..8b835632cff 100644 --- a/packages/ember-htmlbars/lib/hooks/component.js +++ b/packages/ember-htmlbars/lib/hooks/component.js @@ -1,5 +1,7 @@ import ComponentNodeManager from 'ember-htmlbars/node-managers/component-node-manager'; -import buildComponentTemplate from 'ember-views/system/build-component-template'; +import buildComponentTemplate, { buildHTMLTemplate } from 'ember-views/system/build-component-template'; +import lookupComponent from 'ember-htmlbars/utils/lookup-component'; +import Ember from 'ember-metal/core'; export default function componentHook(renderNode, env, scope, _tagName, params, attrs, templates, visitor) { var state = renderNode.state; @@ -12,7 +14,8 @@ export default function componentHook(renderNode, env, scope, _tagName, params, let tagName = _tagName; let isAngleBracket = false; - let isTopLevel; + let isTopLevel = false; + let isDasherized = false; let angles = tagName.match(/^(@?)<(.*)>$/); @@ -22,9 +25,73 @@ export default function componentHook(renderNode, env, scope, _tagName, params, isTopLevel = !!angles[1]; } - var parentView = env.view; + if (tagName.indexOf('-') !== -1) { + isDasherized = true; + } + + let parentView = env.view; + + // | Top-level | Invocation: | Invocation: {{foo-bar}} | + // ---------------------------------------------------------------------- + // |
|
is component el | no special semantics (a) | + // | | is identity el | EWTF | + // | | recursive invocation | no special semantics | + // | {{anything}} | EWTF | no special semantics | + // + // (a) needs to be implemented specially, because the usual semantics of + //
are defined by the compiled template, and we need to emulate + // those semantics. + + let currentComponent = env.view; + let isInvokedWithAngles = currentComponent && currentComponent._isAngleBracket; + let isInvokedWithCurlies = currentComponent && !currentComponent._isAngleBracket; + + //
at the top level of a invocation + let isComponentHTMLElement = isAngleBracket && !isDasherized && isInvokedWithAngles; + + // at the top level of a invocation + let isComponentIdentityElement = isAngleBracket && isTopLevel && tagName === env.view.tagName; + + //
at the top level of a {{foo-bar}} invocation + let isNormalHTMLElement = isAngleBracket && !isDasherized && isInvokedWithCurlies; + + let component, layout; + if (isDasherized || !isAngleBracket) { + let result = lookupComponent(env.container, tagName); + component = result.component; + layout = result.layout; + + if (isAngleBracket && isDasherized && !component && !layout) { + isComponentHTMLElement = true; + } else { + Ember.assert(`HTMLBars error: Could not find component named "${tagName}" (no component or template with that name was found)`, !!(component || layout)); + } + } + + if (isComponentIdentityElement || isComponentHTMLElement) { + // Inside the layout for invoked with angles, this is the top-level element + // for the component. It can either be `` (the "identity element") or any + // normal HTML element (non-dasherized). + let templateOptions = { + component: currentComponent, + tagName, + 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); + } else if (isNormalHTMLElement) { + let block = buildHTMLTemplate(tagName, attrs, { templates, scope }); + block(env, [], undefined, renderNode, scope, visitor); + } else { + // Invoking a component from the outside (either via angle brackets + // or {{foo-bar}} legacy curlies). - if (!isTopLevel || tagName !== env.view.tagName) { var manager = ComponentNodeManager.create(renderNode, env, { tagName, params, @@ -33,24 +100,12 @@ export default function componentHook(renderNode, env, scope, _tagName, params, templates, isAngleBracket, isTopLevel, + component, + layout, 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); } } diff --git a/packages/ember-htmlbars/lib/main.js b/packages/ember-htmlbars/lib/main.js index d2778694efd..b3126ea608a 100644 --- a/packages/ember-htmlbars/lib/main.js +++ b/packages/ember-htmlbars/lib/main.js @@ -125,6 +125,7 @@ import legacyEachWithKeywordHelper from 'ember-htmlbars/helpers/-legacy-each-wit import htmlSafeHelper from 'ember-htmlbars/helpers/-html-safe'; import DOMHelper from 'ember-htmlbars/system/dom-helper'; import Helper, { helper as makeHelper } from 'ember-htmlbars/helper'; +import GlimmerComponent from 'ember-htmlbars/glimmer-component'; // importing adds template bootstrapping // initializer to enable embedded templates @@ -160,5 +161,9 @@ Ember.HTMLBars = { DOMHelper }; +if (isEnabled('ember-htmlbars-component-generation')) { + Ember.GlimmerComponent = GlimmerComponent; +} + Helper.helper = makeHelper; Ember.Helper = Helper; 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 0beb6819ad3..445c3e30a38 100644 --- a/packages/ember-htmlbars/lib/node-managers/component-node-manager.js +++ b/packages/ember-htmlbars/lib/node-managers/component-node-manager.js @@ -1,14 +1,14 @@ import Ember from 'ember-metal/core'; import assign from 'ember-metal/assign'; import buildComponentTemplate from 'ember-views/system/build-component-template'; -import lookupComponent from 'ember-htmlbars/utils/lookup-component'; import getCellOrValue from 'ember-htmlbars/hooks/get-cell-or-value'; import { get } from 'ember-metal/property_get'; import { set } from 'ember-metal/property_set'; import setProperties from 'ember-metal/set_properties'; import { MUTABLE_CELL } from 'ember-views/compat/attrs-proxy'; import { instrument } from 'ember-htmlbars/system/instrumentation-support'; -import EmberComponent from 'ember-views/views/component'; +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'; @@ -37,19 +37,13 @@ ComponentNodeManager.create = function(renderNode, env, options) { parentView, parentScope, isAngleBracket, + component, + layout, templates } = options; attrs = attrs || {}; - // Try to find the Component class and/or template for this component name in - // the container. - let { component, layout } = lookupComponent(env.container, tagName); - - Ember.assert('HTMLBars error: Could not find component named "' + tagName + '" (no component or template with that name was found)', function() { - return component || layout; - }); - - component = component || EmberComponent; + component = component || (isAngleBracket ? GlimmerComponent : LegacyEmberComponent); let createOptions = { parentView }; @@ -71,11 +65,38 @@ ComponentNodeManager.create = function(renderNode, env, options) { // Instantiate the component component = createComponent(component, isAngleBracket, createOptions, renderNode, env, attrs); - // If the component specifies its template via the `layout` - // properties instead of using the template looked up in the container, get - // them now that we have the component instance. + // If the component specifies its template via the `layout properties + // instead of using the template looked up in the container, get them + // now that we have the component instance. layout = get(component, 'layout') || layout; + Ember.runInDebug(() => { + var assert = Ember.assert; + + if (isAngleBracket) { + assert(`You cannot invoke the '${tagName}' component with angle brackets, because it's a subclass of Component. Please upgrade to GlimmerComponent. Alternatively, you can invoke as '{{${tagName}}}'.`, component.isGlimmerComponent); + } else { + assert(`You cannot invoke the '${tagName}' component with curly braces, because it's a subclass of GlimmerComponent. Please invoke it as '<${tagName}>' instead.`, !component.isGlimmerComponent); + } + + if (!layout) { return; } + + let fragmentReason = layout.meta.fragmentReason; + if (isAngleBracket && fragmentReason) { + switch (fragmentReason.name) { + case 'missing-wrapper': + assert(`The <${tagName}> template must have a single top-level element because it is a GlimmerComponent.`); + break; + case 'modifiers': + let modifiers = fragmentReason.modifiers.map(m => `{{${m} ...}}`); + assert(`You cannot use ${ modifiers.join(', ') } in the top-level element of the <${tagName}> template because it is a GlimmerComponent.`); + break; + case 'triple-curlies': + assert(`You cannot use triple curlies (e.g. style={{{ ... }}}) in the top-level element of the <${tagName}> template because it is a GlimmerComponent.`); + break; + } + } + }); let results = buildComponentTemplate( { layout, component, isAngleBracket }, attrs, { templates, scope: parentScope } @@ -151,7 +172,21 @@ ComponentNodeManager.prototype.render = function(_env, visitor) { this.block(env, [], undefined, this.renderNode, this.scope, visitor); } - var element = this.expectElement && this.renderNode.firstNode; + let element; + if (this.expectElement || component.isGlimmerComponent) { + // This code assumes that Glimmer components are never fragments. When + // Glimmer components gain fragment powers, we will need to communicate + // whether the layout produced a single top-level node or fragment + // somehow (either via static information on the template/component, or + // dynamically as the layout is being rendered). + element = this.renderNode.firstNode; + + // Glimmer components may have whitespace or boundary nodes around the + // top-level element. + if (element && element.nodeType !== 1) { + element = nextElementSibling(element); + } + } // In environments like FastBoot, disable any hooks that would cause the component // to access the DOM directly. @@ -164,6 +199,15 @@ ComponentNodeManager.prototype.render = function(_env, visitor) { }, this); }; +function nextElementSibling(node) { + let current = node; + + while (current) { + if (current.nodeType === 1) { return current; } + current = node.nextSibling; + } +} + ComponentNodeManager.prototype.rerender = function(_env, attrs, visitor) { var component = this.component; @@ -211,14 +255,14 @@ ComponentNodeManager.prototype.destroy = function() { export function createComponent(_component, isAngleBracket, _props, renderNode, env, attrs = {}) { let props = assign({}, _props); + let snapshot = takeSnapshot(attrs); + props.attrs = snapshot; + if (!isAngleBracket) { let proto = _component.proto(); Ember.assert('controller= is no longer supported', !('controller' in attrs)); - let snapshot = takeSnapshot(attrs); - props.attrs = snapshot; - mergeBindings(props, shadowedAttrs(proto, snapshot)); } else { props._isAngleBracket = true; diff --git a/packages/ember-htmlbars/tests/compat/controller_keyword_test.js b/packages/ember-htmlbars/tests/compat/controller_keyword_test.js index c422ee61fd7..8bb672faf58 100644 --- a/packages/ember-htmlbars/tests/compat/controller_keyword_test.js +++ b/packages/ember-htmlbars/tests/compat/controller_keyword_test.js @@ -1,5 +1,5 @@ import Ember from 'ember-metal/core'; -import EmberComponent from 'ember-views/views/component'; +import EmberComponent from 'ember-views/components/component'; import { runAppend, runDestroy } from 'ember-runtime/tests/utils'; import compile from 'ember-template-compiler/system/compile'; diff --git a/packages/ember-htmlbars/tests/compat/view_helper_test.js b/packages/ember-htmlbars/tests/compat/view_helper_test.js index cb4fe1222f8..60780c2586d 100644 --- a/packages/ember-htmlbars/tests/compat/view_helper_test.js +++ b/packages/ember-htmlbars/tests/compat/view_helper_test.js @@ -1,5 +1,5 @@ import Ember from 'ember-metal/core'; -import EmberComponent from 'ember-views/views/component'; +import EmberComponent from 'ember-views/components/component'; import EmberView from 'ember-views/views/view'; import EmberSelectView from 'ember-views/views/select'; import { runAppend, runDestroy } from 'ember-runtime/tests/utils'; diff --git a/packages/ember-htmlbars/tests/compat/view_keyword_test.js b/packages/ember-htmlbars/tests/compat/view_keyword_test.js index dba86aaca3d..31d23865465 100644 --- a/packages/ember-htmlbars/tests/compat/view_keyword_test.js +++ b/packages/ember-htmlbars/tests/compat/view_keyword_test.js @@ -1,5 +1,5 @@ import Ember from 'ember-metal/core'; -import EmberComponent from 'ember-views/views/component'; +import EmberComponent from 'ember-views/components/component'; import { runAppend, runDestroy } from 'ember-runtime/tests/utils'; import compile from 'ember-template-compiler/system/compile'; diff --git a/packages/ember-htmlbars/tests/glimmer-component/render-test.js b/packages/ember-htmlbars/tests/glimmer-component/render-test.js new file mode 100644 index 00000000000..048007927a0 --- /dev/null +++ b/packages/ember-htmlbars/tests/glimmer-component/render-test.js @@ -0,0 +1,76 @@ +import Registry from 'container/registry'; +import View from 'ember-views/views/view'; +import GlimmerComponent from 'ember-htmlbars/glimmer-component'; +import compile from 'ember-template-compiler/system/compile'; +import { runAppend, runDestroy } from 'ember-runtime/tests/utils'; +import ComponentLookup from 'ember-views/component_lookup'; +import isEnabled from 'ember-metal/features'; + +let view; + +if (isEnabled('ember-htmlbars-component-generation')) { + QUnit.module('A basic glimmer component', { + teardown() { + runDestroy(view); + } + }); + + QUnit.test('it renders', function(assert) { + let component; + + let MyComponent = GlimmerComponent.extend({ + init() { + component = this; + this._super(...arguments); + }, + layout: compile(`{{yield}}`) + }); + + renderComponent('my-component', { + implementation: MyComponent, + yielded: 'Hello world' + }); + + ok(component instanceof GlimmerComponent, 'the component was instantiated correctly'); + equal(view.childViews[0], component, 'the component was rendered and inserted into child views'); + hasSelector(assert, `my-component.ember-view[id=${component.elementId}]`); + }); +} + +function renderComponent(tag, component) { + let { params, hash, yielded, implementation } = component; + params = params || []; + hash = hash || {}; + let stringParams = params.join(' '); + let stringHash = Object.keys(hash) + .map(key => `${key}=${hash[key]}`) + .join(' '); + + let registry = new Registry(); + registry.register('component-lookup:main', ComponentLookup); + registry.register(`component:${tag}`, implementation); + + view = View.extend({ + container: registry.container(), + template: compile(`<${tag} ${stringParams} ${stringHash}>${yielded}`) + }).create(); + + runAppend(view); +} + +function hasSelector(assert, selector) { + assert.ok(document.querySelector(`#qunit-fixture ${selector}`), `${selector} exists`); +} + + +//testForComponent({ + //name: 'my-component', + //params: [], + //hash: {}, + //template: ` + // + //Hello world + // + //`, + //component: MyComponent +//}); diff --git a/packages/ember-htmlbars/tests/glimmer-component/test-helpers.js b/packages/ember-htmlbars/tests/glimmer-component/test-helpers.js new file mode 100644 index 00000000000..af369f682f0 --- /dev/null +++ b/packages/ember-htmlbars/tests/glimmer-component/test-helpers.js @@ -0,0 +1,9 @@ +export function moduleForGlimmerComponent(name, options) { + function beforeEach() { + } + + function afterEach() { + } + + QUnit.module(`Glimmer Component - ${name}`, { beforeEach, afterEach }); +} diff --git a/packages/ember-htmlbars/tests/helpers/-html-safe-test.js b/packages/ember-htmlbars/tests/helpers/-html-safe-test.js index c438607e128..10db1e9fc76 100644 --- a/packages/ember-htmlbars/tests/helpers/-html-safe-test.js +++ b/packages/ember-htmlbars/tests/helpers/-html-safe-test.js @@ -1,7 +1,7 @@ /* globals EmberDev */ import Ember from 'ember-metal/core'; import { Registry } from 'ember-runtime/system/container'; -import Component from 'ember-views/views/component'; +import Component from 'ember-views/components/component'; import compile from 'ember-template-compiler/system/compile'; import { runAppend, runDestroy } from 'ember-runtime/tests/utils'; diff --git a/packages/ember-htmlbars/tests/helpers/component_test.js b/packages/ember-htmlbars/tests/helpers/component_test.js index 17701fd9fcf..5354cd9672a 100644 --- a/packages/ember-htmlbars/tests/helpers/component_test.js +++ b/packages/ember-htmlbars/tests/helpers/component_test.js @@ -6,7 +6,7 @@ import Registry from 'container/registry'; import { runAppend, runDestroy } from 'ember-runtime/tests/utils'; import ComponentLookup from 'ember-views/component_lookup'; import EmberView from 'ember-views/views/view'; -import Component from 'ember-views/views/component'; +import Component from 'ember-views/components/component'; import compile from 'ember-template-compiler/system/compile'; import computed from 'ember-metal/computed'; diff --git a/packages/ember-htmlbars/tests/helpers/concat-test.js b/packages/ember-htmlbars/tests/helpers/concat-test.js index 658cc873bdb..4a51597e801 100644 --- a/packages/ember-htmlbars/tests/helpers/concat-test.js +++ b/packages/ember-htmlbars/tests/helpers/concat-test.js @@ -1,6 +1,6 @@ import run from 'ember-metal/run_loop'; import { Registry } from 'ember-runtime/system/container'; -import Component from 'ember-views/views/component'; +import Component from 'ember-views/components/component'; import compile from 'ember-template-compiler/system/compile'; import { helper as makeHelper } from 'ember-htmlbars/helper'; diff --git a/packages/ember-htmlbars/tests/helpers/custom_helper_test.js b/packages/ember-htmlbars/tests/helpers/custom_helper_test.js index 43010d6b12b..1872119ff17 100644 --- a/packages/ember-htmlbars/tests/helpers/custom_helper_test.js +++ b/packages/ember-htmlbars/tests/helpers/custom_helper_test.js @@ -1,5 +1,5 @@ import Ember from 'ember-metal/core'; -import Component from 'ember-views/views/component'; +import Component from 'ember-views/components/component'; import Helper, { helper as makeHelper } from 'ember-htmlbars/helper'; import compile from 'ember-template-compiler/system/compile'; import { runAppend, runDestroy } from 'ember-runtime/tests/utils'; diff --git a/packages/ember-htmlbars/tests/helpers/each_in_test.js b/packages/ember-htmlbars/tests/helpers/each_in_test.js index c96fb2f2851..d1bbd50262e 100644 --- a/packages/ember-htmlbars/tests/helpers/each_in_test.js +++ b/packages/ember-htmlbars/tests/helpers/each_in_test.js @@ -1,4 +1,4 @@ -import Component from 'ember-views/views/component'; +import Component from 'ember-views/components/component'; import compile from 'ember-template-compiler/system/compile'; import run from 'ember-metal/run_loop'; diff --git a/packages/ember-htmlbars/tests/helpers/unbound_test.js b/packages/ember-htmlbars/tests/helpers/unbound_test.js index 066e38b4e31..fd8638a0bf9 100644 --- a/packages/ember-htmlbars/tests/helpers/unbound_test.js +++ b/packages/ember-htmlbars/tests/helpers/unbound_test.js @@ -1,6 +1,6 @@ /*jshint newcap:false*/ import EmberView from 'ember-views/views/view'; -import EmberComponent from 'ember-views/views/component'; +import EmberComponent from 'ember-views/components/component'; import EmberObject from 'ember-runtime/system/object'; import { A } from 'ember-runtime/system/native_array'; diff --git a/packages/ember-htmlbars/tests/helpers/view_test.js b/packages/ember-htmlbars/tests/helpers/view_test.js index 19371b3e314..cc0c86c6ceb 100644 --- a/packages/ember-htmlbars/tests/helpers/view_test.js +++ b/packages/ember-htmlbars/tests/helpers/view_test.js @@ -1,7 +1,7 @@ /*globals EmberDev */ import Ember from 'ember-metal/core'; import EmberView from 'ember-views/views/view'; -import EmberComponent from 'ember-views/views/component'; +import EmberComponent from 'ember-views/components/component'; import Registry from 'container/registry'; import ComponentLookup from 'ember-views/component_lookup'; import run from 'ember-metal/run_loop'; diff --git a/packages/ember-htmlbars/tests/helpers/yield_test.js b/packages/ember-htmlbars/tests/helpers/yield_test.js index a83e4e2262d..20117465e91 100644 --- a/packages/ember-htmlbars/tests/helpers/yield_test.js +++ b/packages/ember-htmlbars/tests/helpers/yield_test.js @@ -6,7 +6,7 @@ import { computed } from 'ember-metal/computed'; import { Registry } from 'ember-runtime/system/container'; //import { set } from "ember-metal/property_set"; import { A } from 'ember-runtime/system/native_array'; -import Component from 'ember-views/views/component'; +import Component from 'ember-views/components/component'; import helpers from 'ember-htmlbars/helpers'; import ComponentLookup from 'ember-views/component_lookup'; diff --git a/packages/ember-htmlbars/tests/hooks/component_test.js b/packages/ember-htmlbars/tests/hooks/component_test.js index 333c7f246b2..4fe05a214c4 100644 --- a/packages/ember-htmlbars/tests/hooks/component_test.js +++ b/packages/ember-htmlbars/tests/hooks/component_test.js @@ -8,7 +8,7 @@ import { runAppend, runDestroy } from 'ember-runtime/tests/utils'; var view, registry, container; if (isEnabled('ember-htmlbars-component-generation')) { - QUnit.module('ember-htmlbars: component hook', { + QUnit.module('ember-htmlbars: dasherized components that are not in the container ("web components")', { setup() { registry = new Registry(); container = registry.container(); @@ -24,8 +24,8 @@ if (isEnabled('ember-htmlbars-component-generation')) { } }); - QUnit.test('component is looked up from the container', function() { - registry.register('template:components/foo-bar', compile('yippie!')); + QUnit.test('non-component dasherized elements can be used as top-level elements', function() { + registry.register('template:components/foo-bar', compile('yippie!')); view = EmberView.create({ container: container, @@ -34,17 +34,17 @@ if (isEnabled('ember-htmlbars-component-generation')) { runAppend(view); - equal(view.$().text(), 'yippie!', 'component was looked up and rendered'); + equal(view.$('baz-bat').length, 1, 'regular element fallback occurred'); }); - QUnit.test('asserts if component is not found', function() { + QUnit.test('falls back to web component when invoked with angles', function() { view = EmberView.create({ container: container, template: compile('') }); - expectAssertion(function() { - runAppend(view); - }, /Could not find component named "foo-bar" \(no component or template with that name was found\)/); + runAppend(view); + + equal(view.$('foo-bar').length, 1, 'regular element fallback occurred'); }); } diff --git a/packages/ember-htmlbars/tests/integration/attrs_lookup_test.js b/packages/ember-htmlbars/tests/integration/attrs_lookup_test.js index 635486f92e0..c9c9d44b759 100644 --- a/packages/ember-htmlbars/tests/integration/attrs_lookup_test.js +++ b/packages/ember-htmlbars/tests/integration/attrs_lookup_test.js @@ -1,7 +1,7 @@ import Registry from 'container/registry'; import compile from 'ember-template-compiler/system/compile'; import ComponentLookup from 'ember-views/component_lookup'; -import Component from 'ember-views/views/component'; +import Component from 'ember-views/components/component'; import { runAppend, runDestroy } from 'ember-runtime/tests/utils'; import EmberView from 'ember-views/views/view'; diff --git a/packages/ember-htmlbars/tests/integration/block_params_test.js b/packages/ember-htmlbars/tests/integration/block_params_test.js index 8a3ce1197cd..1339f21c22a 100644 --- a/packages/ember-htmlbars/tests/integration/block_params_test.js +++ b/packages/ember-htmlbars/tests/integration/block_params_test.js @@ -2,7 +2,7 @@ import Registry from 'container/registry'; import run from 'ember-metal/run_loop'; import ComponentLookup from 'ember-views/component_lookup'; import View from 'ember-views/views/view'; -import Component from 'ember-views/views/component'; +import Component from 'ember-views/components/component'; import compile from 'ember-template-compiler/system/compile'; import { runAppend, runDestroy } from 'ember-runtime/tests/utils'; diff --git a/packages/ember-htmlbars/tests/integration/component_element_id_test.js b/packages/ember-htmlbars/tests/integration/component_element_id_test.js index 3a9832b41c0..25b9ae9cce1 100644 --- a/packages/ember-htmlbars/tests/integration/component_element_id_test.js +++ b/packages/ember-htmlbars/tests/integration/component_element_id_test.js @@ -3,7 +3,7 @@ import { Registry } from 'ember-runtime/system/container'; import compile from 'ember-template-compiler/system/compile'; import { runAppend, runDestroy } from 'ember-runtime/tests/utils'; import ComponentLookup from 'ember-views/component_lookup'; -import Component from 'ember-views/views/component'; +import Component from 'ember-views/components/component'; var registry, container, view; diff --git a/packages/ember-htmlbars/tests/integration/component_invocation_test.js b/packages/ember-htmlbars/tests/integration/component_invocation_test.js index 764b296f7f2..92894a278f6 100644 --- a/packages/ember-htmlbars/tests/integration/component_invocation_test.js +++ b/packages/ember-htmlbars/tests/integration/component_invocation_test.js @@ -5,7 +5,8 @@ import Registry from 'container/registry'; import jQuery from 'ember-views/system/jquery'; import compile from 'ember-template-compiler/system/compile'; import ComponentLookup from 'ember-views/component_lookup'; -import Component from 'ember-views/views/component'; +import Component from 'ember-views/components/component'; +import GlimmerComponent from 'ember-htmlbars/glimmer-component'; import { runAppend, runDestroy } from 'ember-runtime/tests/utils'; import { get } from 'ember-metal/property_get'; import { set } from 'ember-metal/property_set'; @@ -64,6 +65,15 @@ QUnit.test('non-block without properties', function() { equal(jQuery('#qunit-fixture').text(), 'In layout'); }); +QUnit.test('GlimmerComponent cannot be invoked with curly braces', function() { + registry.register('template:components/non-block', compile('In layout')); + registry.register('component:non-block', GlimmerComponent.extend()); + + expectAssertion(function() { + view = appendViewFor('{{non-block}}'); + }, /cannot invoke the 'non-block' component with curly braces/); +}); + QUnit.test('block without properties', function() { expect(1); @@ -832,88 +842,233 @@ if (isEnabled('ember-htmlbars-component-generation')) { } }); - QUnit.test('non-block without properties replaced with a fragment when the content is just text', function() { + QUnit.test('legacy components cannot be invoked with angle brackets', function() { registry.register('template:components/non-block', compile('In layout')); + registry.register('component:non-block', Component.extend()); - view = appendViewFor(''); + expectAssertion(function() { + view = appendViewFor(''); + }, /cannot invoke the 'non-block' component with angle brackets/); + }); - equal(view.$().html(), 'In layout', 'Just the fragment was used'); + QUnit.test('using a text-fragment in a GlimmerComponent layout gives an error', function() { + registry.register('template:components/non-block', compile('In layout')); + + expectAssertion(() => { + view = appendViewFor(''); + }, `The template must have a single top-level element because it is a GlimmerComponent.`); }); - QUnit.test('non-block without properties replaced with a fragment when the content is multiple elements', function() { + QUnit.test('having multiple top-level elements in a GlimmerComponent layout gives an error', function() { registry.register('template:components/non-block', compile('
This is a
fragment
')); - view = appendViewFor(''); + expectAssertion(() => { + view = appendViewFor(''); + }, `The template must have a single top-level element because it is a GlimmerComponent.`); + }); + + QUnit.test('using a modifier in a GlimmerComponent layout gives an error', function() { + registry.register('template:components/non-block', compile('
')); + + expectAssertion(() => { + view = appendViewFor(''); + }, `You cannot use {{action ...}} in the top-level element of the template because it is a GlimmerComponent.`); + }); + + QUnit.test('using triple-curlies in a GlimmerComponent layout gives an error', function() { + registry.register('template:components/non-block', compile('
This is a
')); - equal(view.$().html(), '
This is a
fragment
', 'Just the fragment was used'); + expectAssertion(() => { + view = appendViewFor(''); + }, `You cannot use triple curlies (e.g. style={{{ ... }}}) in the top-level element of the template because it is a GlimmerComponent.`); }); - QUnit.skip('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
')); + let styles = [{ + name: 'a div', + tagName: 'div' + }, { + name: 'an identity element', + tagName: 'non-block' + }, { + name: 'a web component', + tagName: 'not-an-ember-component' + }]; - view = appendViewFor(''); + styles.forEach(style => { + QUnit.test(`non-block without attributes replaced with ${style.name}`, 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(` <${style.tagName}>In layout `)); - 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'); + view = appendViewFor(''); - run(view, 'rerender'); + let node = view.element.firstElementChild; + equalsElement(node, style.tagName, { class: 'ember-view', id: regex(/^ember\d*$/) }, 'In layout'); - 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'); - }); + run(view, 'rerender'); + + strictEqual(node, view.element.firstElementChild, 'The inner element has not changed'); + equalsElement(node, style.tagName, { class: 'ember-view', id: regex(/^ember\d*$/) }, 'In layout'); + }); + + QUnit.test(`non-block with attributes replaced with ${style.name}`, function() { + registry.register('template:components/non-block', compile(` <${style.tagName} such="{{attrs.stability}}">In layout `)); - QUnit.skip('non-block without properties replaced with identity element', function() { - registry.register('template:components/non-block', compile('In layout')); + view = appendViewFor('', { + stability: 'stability' + }); - view = appendViewFor('', { - stability: 'stability' + let node = view.element.firstElementChild; + equalsElement(node, style.tagName, { such: 'stability', class: 'ember-view', id: regex(/^ember\d*$/) }, 'In layout'); + + run(() => view.set('stability', 'changed!!!')); + + strictEqual(node, view.element.firstElementChild, 'The inner element has not changed'); + equalsElement(node, style.tagName, { such: 'changed!!!', class: 'ember-view', id: regex(/^ember\d*$/) }, 'In layout'); }); - 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'); + QUnit.test(`non-block replaced with ${style.name} (regression with single element in the root element)`, function() { + registry.register('template:components/non-block', compile(` <${style.tagName} such="{{attrs.stability}}">

In layout

`)); - run(() => view.set('stability', 'stability!')); + view = appendViewFor('', { + stability: 'stability' + }); - 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'); - }); + let node = view.element.firstElementChild; + equalsElement(node, style.tagName, { such: 'stability', class: 'ember-view', id: regex(/^ember\d*$/) }, '

In layout

'); - QUnit.skip('non-block with class replaced with a div merges classes', function() { - registry.register('template:components/non-block', compile('
')); + run(() => view.set('stability', 'changed!!!')); - view = appendViewFor('', { - outer: 'outer' + strictEqual(node, view.element.firstElementChild, 'The inner element has not changed'); + equalsElement(node, style.tagName, { such: 'changed!!!', class: 'ember-view', id: regex(/^ember\d*$/) }, '

In layout

'); }); - equal(view.$('div').attr('class'), 'inner-class outer ember-view', 'the classes are merged'); + QUnit.test(`non-block with class replaced with ${style.name} merges classes`, function() { + registry.register('template:components/non-block', compile(`<${style.tagName} class="inner-class" />`)); - run(() => view.set('outer', 'new-outer')); + view = appendViewFor('', { + outer: 'outer' + }); - equal(view.$('div').attr('class'), 'inner-class new-outer ember-view', 'the classes are merged'); - }); + equal(view.$(style.tagName).attr('class'), 'inner-class outer ember-view', 'the classes are merged'); - QUnit.skip('non-block with class replaced with a identity element merges classes', function() { - registry.register('template:components/non-block', compile('')); + run(() => view.set('outer', 'new-outer')); - view = appendViewFor('', { - outer: 'outer' + equal(view.$(style.tagName).attr('class'), 'inner-class new-outer ember-view', 'the classes are merged'); }); - equal(view.$('non-block').attr('class'), 'inner-class outer ember-view', 'the classes are merged'); + QUnit.test(`non-block with outer attributes replaced with ${style.name} shadows inner attributes`, function() { + registry.register('template:components/non-block', compile(`<${style.tagName} data-static="static" data-dynamic="{{internal}}" />`)); + + view = appendViewFor(''); - run(() => view.set('outer', 'new-outer')); + equal(view.$(style.tagName).attr('data-static'), 'outer', 'the outer attribute wins'); + equal(view.$(style.tagName).attr('data-dynamic'), 'outer', 'the outer attribute wins'); - equal(view.$('non-block').attr('class'), 'inner-class new-outer ember-view', 'the classes are merged'); + let component = view.childViews[0]; // HAX + + run(() => component.set('internal', 'changed')); + + equal(view.$(style.tagName).attr('data-static'), 'outer', 'the outer attribute wins'); + equal(view.$(style.tagName).attr('data-dynamic'), 'outer', 'the outer attribute wins'); + }); + + // TODO: When un-skipping, fix this so it handles all styles + QUnit.skip('non-block recursive invocations with outer attributes replaced with a div shadows inner attributes', function() { + registry.register('template:components/non-block-wrapper', compile('')); + registry.register('template:components/non-block', compile('
')); + + view = appendViewFor(''); + + equal(view.$('div').attr('data-static'), 'outer', 'the outer-most attribute wins'); + equal(view.$('div').attr('data-dynamic'), 'outer', 'the outer-most attribute wins'); + + let component = view.childViews[0].childViews[0]; // HAX + + run(() => component.set('internal', 'changed')); + + equal(view.$('div').attr('data-static'), 'outer', 'the outer-most attribute wins'); + equal(view.$('div').attr('data-dynamic'), 'outer', 'the outer-most attribute wins'); + }); + + QUnit.test(`non-block replaced with ${style.name} should have correct scope`, function() { + registry.register('template:components/non-block', compile(`<${style.tagName}>{{internal}}`)); + + registry.register('component:non-block', GlimmerComponent.extend({ + init() { + this._super(...arguments); + this.set('internal', 'stuff'); + } + })); + + view = appendViewFor(''); + + equal(view.$().text(), 'stuff'); + }); + + QUnit.test(`non-block replaced with ${style.name} should have correct 'element'`, function() { + registry.register('template:components/non-block', compile(`<${style.tagName} />`)); + + let component; + + registry.register('component:non-block', GlimmerComponent.extend({ + init() { + this._super(...arguments); + component = this; + } + })); + + view = appendViewFor(''); + + equal(component.element, view.$(style.tagName)[0]); + }); + + QUnit.test(`non-block replaced with ${style.name} should have inner attributes`, function() { + registry.register('template:components/non-block', compile(`<${style.tagName} data-static="static" data-dynamic="{{internal}}" />`)); + + registry.register('component:non-block', GlimmerComponent.extend({ + init() { + this._super(...arguments); + this.set('internal', 'stuff'); + } + })); + + view = appendViewFor(''); + + equal(view.$(style.tagName).attr('data-static'), 'static'); + equal(view.$(style.tagName).attr('data-dynamic'), 'stuff'); + }); + + QUnit.test(`only text attributes are reflected on the underlying DOM element (${style.name})`, function() { + registry.register('template:components/non-block', compile(`<${style.tagName}>In layout`)); + + view = appendViewFor('', { + dynamic: 'dynamic' + }); + + let el = view.$(style.tagName); + equal(el.length, 1, 'precond - the view was rendered'); + equal(el.text(), 'In layout'); + equal(el.attr('static-prop'), 'static text'); + equal(el.attr('concat-prop'), 'dynamic text'); + equal(el.attr('dynamic-prop'), undefined); + }); + + QUnit.skip(`partials templates should not be treated like a component layout for ${style.name}`, function() { + registry.register('template:_zomg', compile(`

In partial

`)); + registry.register('template:components/non-block', compile(`<${style.tagName}>{{partial "zomg"}}`)); + + view = appendViewFor(''); + + let el = view.$(style.tagName).find('p'); + equal(el.length, 1, 'precond - the partial was rendered'); + equal(el.text(), 'In partial'); + strictEqual(el.attr('id'), undefined, 'the partial should not get an id'); + strictEqual(el.attr('class'), undefined, 'the partial should not get a class'); + }); }); - QUnit.skip('non-block rendering a fragment', function() { + QUnit.skip('[FRAGMENT] non-block rendering a fragment', function() { registry.register('template:components/non-block', compile('

{{attrs.first}}

{{attrs.second}}

')); view = appendViewFor('', { @@ -931,7 +1086,7 @@ if (isEnabled('ember-htmlbars-component-generation')) { equal(view.$().html(), '

first2

second2

', 'The fragment was updated'); }); - QUnit.test('block without properties', function() { + QUnit.test('block without properties', function() { registry.register('template:components/with-block', compile('In layout - {{yield}}')); view = appendViewFor('In template'); @@ -939,31 +1094,18 @@ if (isEnabled('ember-htmlbars-component-generation')) { equal(view.$('with-block.ember-view').text(), 'In layout - In template', 'Both the layout and template are rendered'); }); - QUnit.test('non-block with properties on attrs', function() { - registry.register('template:components/non-block', compile('In layout')); - - view = appendViewFor('', { - dynamic: 'dynamic' - }); - - 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'); - }); - - QUnit.skip('attributes are not installed on the top level', function() { + QUnit.test('attributes are not installed on the top level', function() { let component; registry.register('template:components/non-block', compile('In layout - {{attrs.text}} -- {{text}}')); - registry.register('component:non-block', Component.extend({ + registry.register('component:non-block', GlimmerComponent.extend({ + // This is specifically attempting to trigger a 1.x-era heuristic that only copied + // attrs that were present as defined properties on the component. text: null, dynamic: null, - didInitAttrs() { + init() { + this._super(...arguments); component = this; } })); @@ -990,8 +1132,8 @@ if (isEnabled('ember-htmlbars-component-generation')) { strictEqual(get(component, 'dynamic'), null); }); - QUnit.test('non-block with properties on attrs and component class', function() { - registry.register('component:non-block', Component.extend()); + QUnit.test('non-block with properties on attrs and component class', function() { + registry.register('component:non-block', GlimmerComponent.extend()); registry.register('template:components/non-block', compile('In layout - someProp: {{attrs.someProp}}')); view = appendViewFor(''); @@ -999,11 +1141,11 @@ if (isEnabled('ember-htmlbars-component-generation')) { equal(jQuery('#qunit-fixture').text(), 'In layout - someProp: something here'); }); - QUnit.skip('rerendering component with attrs from parent', function() { + QUnit.test('rerendering component with attrs from parent', function() { var willUpdate = 0; var didReceiveAttrs = 0; - registry.register('component:non-block', Component.extend({ + registry.register('component:non-block', GlimmerComponent.extend({ didReceiveAttrs() { didReceiveAttrs++; }, @@ -1054,7 +1196,7 @@ if (isEnabled('ember-htmlbars-component-generation')) { moduleName: layoutModuleName }); registry.register('template:components/sample-component', sampleComponentLayout); - registry.register('component:sample-component', Component.extend({ + registry.register('component:sample-component', GlimmerComponent.extend({ didInsertElement: function() { equal(this._renderNode.lastResult.template.meta.moduleName, layoutModuleName); } @@ -1088,6 +1230,18 @@ if (isEnabled('ember-htmlbars-component-generation')) { runAppend(view); }); + QUnit.test('computed property alias on attrs', function() { + registry.register('template:components/computed-alias', compile('{{otherProp}}')); + + registry.register('component:computed-alias', GlimmerComponent.extend({ + otherProp: Ember.computed.alias('attrs.someProp') + })); + + view = appendViewFor(''); + + equal(view.$().text(), 'value'); + }); + QUnit.test('parameterized hasBlock default', function() { registry.register('template:components/check-block', compile('{{#if (hasBlock)}}Yes{{else}}No{{/if}}')); @@ -1124,3 +1278,36 @@ if (isEnabled('ember-htmlbars-component-generation')) { equal(view.$('#expect-yes').text(), 'Yes'); }); } + +function regex(r) { + return { + match(v) { + return r.test(v); + } + }; +} + +function equalsElement(element, tagName, attributes, content) { + QUnit.push(element.tagName === tagName.toUpperCase(), element.tagName.toLowerCase(), tagName, `expect tagName to be ${tagName}`); + + let expectedCount = 0; + for (let prop in attributes) { + expectedCount++; + let expected = attributes[prop]; + if (typeof expected === 'string') { + QUnit.push(element.getAttribute(prop) === attributes[prop], element.getAttribute(prop), attributes[prop], `The element should have ${prop}=${attributes[prop]}`); + } else { + QUnit.push(attributes[prop].match(element.getAttribute(prop)), element.getAttribute(prop), attributes[prop], `The element should have ${prop}=${attributes[prop]}`); + } + } + + let actualAttributes = {}; + for (let i = 0, l = element.attributes.length; i < l; i++) { + actualAttributes[element.attributes[i].name] = element.attributes[i].value; + } + + QUnit.push(element.attributes.length === expectedCount, actualAttributes, attributes, `Expected ${expectedCount} attributes`); + + QUnit.push(element.innerHTML === content, element.innerHTML, content, `The element had '${content}' as its content`); +} + diff --git a/packages/ember-htmlbars/tests/integration/component_lifecycle_test.js b/packages/ember-htmlbars/tests/integration/component_lifecycle_test.js index f4c92b7b703..64118271b5e 100644 --- a/packages/ember-htmlbars/tests/integration/component_lifecycle_test.js +++ b/packages/ember-htmlbars/tests/integration/component_lifecycle_test.js @@ -2,345 +2,387 @@ import Registry from 'container/registry'; import jQuery from 'ember-views/system/jquery'; import compile from 'ember-template-compiler/system/compile'; import ComponentLookup from 'ember-views/component_lookup'; -import Component from 'ember-views/views/component'; +import Component from 'ember-views/components/component'; +import GlimmerComponent from 'ember-htmlbars/glimmer-component'; import { runAppend, runDestroy } from 'ember-runtime/tests/utils'; import run from 'ember-metal/run_loop'; import EmberView from 'ember-views/views/view'; +import isEnabled from 'ember-metal/features'; var registry, container, view; var hooks; -QUnit.module('component - lifecycle hooks', { - setup() { - registry = new Registry(); - container = registry.container(); - registry.optionsForType('component', { singleton: false }); - registry.optionsForType('view', { singleton: false }); - registry.optionsForType('template', { instantiate: false }); - registry.register('component-lookup:main', ComponentLookup); +let styles = [{ + name: 'curly', + class: Component +}]; - hooks = []; - }, - - teardown() { - runDestroy(container); - runDestroy(view); - registry = container = view = null; - } -}); - -function pushHook(view, type, arg) { - hooks.push(hook(view, type, arg)); -} - -function hook(view, type, arg) { - return { type: type, view: view, arg: arg }; +if (isEnabled('ember-htmlbars-component-generation')) { + styles.push({ + name: 'angle', + class: GlimmerComponent + }); } -QUnit.test('lifecycle hooks are invoked in a predictable order', function() { - var components = {}; - - function component(label) { - return Component.extend({ - init() { - this.label = label; - components[label] = this; - this._super.apply(this, arguments); - }, +styles.forEach(style => { + function invoke(name, hash) { + if (style.name === 'curly') { + let attrs = Object.keys(hash).map(k => `${k}=${val(hash[k])}`).join(' '); + return `{{${name} ${attrs}}}`; + } else if (style.name === 'angle') { + let attrs = Object.keys(hash).map(k => `${k}=${val(hash[k])}`).join(' '); + return `<${name} ${attrs} />`; + } + } - didInitAttrs(options) { - pushHook(label, 'didInitAttrs', options); - }, + function val(value) { + if (value.isString) { + return JSON.stringify(value.value); + } - didUpdateAttrs(options) { - pushHook(label, 'didUpdateAttrs', options); - }, + if (style.name === 'curly') { + return `(readonly ${value})`; + } else { + return `{{${value}}}`; + } + } - willUpdate(options) { - pushHook(label, 'willUpdate', options); - }, + function string(val) { + return { isString: true, value: val }; + } - didReceiveAttrs(options) { - pushHook(label, 'didReceiveAttrs', options); - }, + QUnit.module(`component - lifecycle hooks (${style.name})`, { + setup() { + registry = new Registry(); + container = registry.container(); + registry.optionsForType('component', { singleton: false }); + registry.optionsForType('view', { singleton: false }); + registry.optionsForType('template', { instantiate: false }); + registry.register('component-lookup:main', ComponentLookup); + + hooks = []; + }, + + teardown() { + runDestroy(container); + runDestroy(view); + registry = container = view = null; + } + }); - willRender() { - pushHook(label, 'willRender'); - }, + function pushHook(view, type, arg) { + hooks.push(hook(view, type, arg)); + } - didRender() { - pushHook(label, 'didRender'); - }, + function hook(view, type, arg) { + return { type: type, view: view, arg: arg }; + } - didInsertElement() { - pushHook(label, 'didInsertElement'); - }, + QUnit.test('lifecycle hooks are invoked in a predictable order', function() { + var components = {}; + + function component(label) { + return style.class.extend({ + init() { + this.label = label; + components[label] = this; + this._super.apply(this, arguments); + }, + + didInitAttrs(options) { + pushHook(label, 'didInitAttrs', options); + }, + + didUpdateAttrs(options) { + pushHook(label, 'didUpdateAttrs', options); + }, + + willUpdate(options) { + pushHook(label, 'willUpdate', options); + }, + + didReceiveAttrs(options) { + pushHook(label, 'didReceiveAttrs', options); + }, + + willRender() { + pushHook(label, 'willRender'); + }, + + didRender() { + pushHook(label, 'didRender'); + }, + + didInsertElement() { + pushHook(label, 'didInsertElement'); + }, + + didUpdate(options) { + pushHook(label, 'didUpdate', options); + } + }); + } - didUpdate(options) { - pushHook(label, 'didUpdate', options); - } - }); - } + registry.register('component:the-top', component('top')); + registry.register('component:the-middle', component('middle')); + registry.register('component:the-bottom', component('bottom')); - registry.register('component:the-top', component('top')); - registry.register('component:the-middle', component('middle')); - registry.register('component:the-bottom', component('bottom')); + registry.register('template:components/the-top', compile(`
Twitter: {{attrs.twitter}} ${invoke('the-middle', { name: string('Tom Dale') })}
`)); + registry.register('template:components/the-middle', compile(`
Name: {{attrs.name}} ${invoke('the-bottom', { website: string('tomdale.net') })}
`)); + registry.register('template:components/the-bottom', compile('
Website: {{attrs.website}}
')); - registry.register('template:components/the-top', compile('Twitter: {{attrs.twitter}} {{the-middle name="Tom Dale"}}')); - registry.register('template:components/the-middle', compile('Name: {{attrs.name}} {{the-bottom website="tomdale.net"}}')); - registry.register('template:components/the-bottom', compile('Website: {{attrs.website}}')); + view = EmberView.extend({ + template: compile(invoke('the-top', { twitter: 'view.twitter' })), + twitter: '@tomdale', + container: container + }).create(); - view = EmberView.extend({ - template: compile('{{the-top twitter=(readonly view.twitter)}}'), - twitter: '@tomdale', - container: container - }).create(); + runAppend(view); - runAppend(view); + ok(component, 'The component was inserted'); + equal(jQuery('#qunit-fixture').text(), 'Twitter: @tomdale Name: Tom Dale Website: tomdale.net'); - ok(component, 'The component was inserted'); - equal(jQuery('#qunit-fixture').text(), 'Twitter: @tomdale Name: Tom Dale Website: tomdale.net'); + let topAttrs = { twitter: '@tomdale' }; + let middleAttrs = { name: 'Tom Dale' }; + let bottomAttrs = { website: 'tomdale.net' }; - let topAttrs = { twitter: '@tomdale' }; - let middleAttrs = { name: 'Tom Dale' }; - let bottomAttrs = { website: 'tomdale.net' }; + deepEqual(hooks, [ + hook('top', 'didInitAttrs', { attrs: topAttrs }), hook('top', 'didReceiveAttrs', { newAttrs: topAttrs }), hook('top', 'willRender'), + hook('middle', 'didInitAttrs', { attrs: middleAttrs }), hook('middle', 'didReceiveAttrs', { newAttrs: middleAttrs }), hook('middle', 'willRender'), + hook('bottom', 'didInitAttrs', { attrs: bottomAttrs }), hook('bottom', 'didReceiveAttrs', { newAttrs: bottomAttrs }), hook('bottom', 'willRender'), + hook('bottom', 'didInsertElement'), hook('bottom', 'didRender'), + hook('middle', 'didInsertElement'), hook('middle', 'didRender'), + hook('top', 'didInsertElement'), hook('top', 'didRender') + ]); - deepEqual(hooks, [ - hook('top', 'didInitAttrs', { attrs: topAttrs }), hook('top', 'didReceiveAttrs', { newAttrs: topAttrs }), hook('top', 'willRender'), - hook('middle', 'didInitAttrs', { attrs: middleAttrs }), hook('middle', 'didReceiveAttrs', { newAttrs: middleAttrs }), hook('middle', 'willRender'), - hook('bottom', 'didInitAttrs', { attrs: bottomAttrs }), hook('bottom', 'didReceiveAttrs', { newAttrs: bottomAttrs }), hook('bottom', 'willRender'), - hook('bottom', 'didInsertElement'), hook('bottom', 'didRender'), - hook('middle', 'didInsertElement'), hook('middle', 'didRender'), - hook('top', 'didInsertElement'), hook('top', 'didRender') - ]); + hooks = []; - hooks = []; + run(function() { + components.bottom.rerender(); + }); - run(function() { - components.bottom.rerender(); - }); + deepEqual(hooks, [ + hook('bottom', 'willUpdate'), hook('bottom', 'willRender'), + hook('bottom', 'didUpdate'), hook('bottom', 'didRender') + ]); - deepEqual(hooks, [ - hook('bottom', 'willUpdate'), hook('bottom', 'willRender'), - hook('bottom', 'didUpdate'), hook('bottom', 'didRender') - ]); + hooks = []; - hooks = []; + run(function() { + components.middle.rerender(); + }); - run(function() { - components.middle.rerender(); - }); + bottomAttrs = { oldAttrs: { website: 'tomdale.net' }, newAttrs: { website: 'tomdale.net' } }; - bottomAttrs = { oldAttrs: { website: 'tomdale.net' }, newAttrs: { website: 'tomdale.net' } }; + deepEqual(hooks, [ + hook('middle', 'willUpdate'), hook('middle', 'willRender'), - deepEqual(hooks, [ - hook('middle', 'willUpdate'), hook('middle', 'willRender'), + hook('bottom', 'didUpdateAttrs', bottomAttrs), + hook('bottom', 'didReceiveAttrs', bottomAttrs), - hook('bottom', 'didUpdateAttrs', bottomAttrs), - hook('bottom', 'didReceiveAttrs', bottomAttrs), + hook('bottom', 'willUpdate'), hook('bottom', 'willRender'), - hook('bottom', 'willUpdate'), hook('bottom', 'willRender'), + hook('bottom', 'didUpdate'), hook('bottom', 'didRender'), + hook('middle', 'didUpdate'), hook('middle', 'didRender') + ]); - hook('bottom', 'didUpdate'), hook('bottom', 'didRender'), - hook('middle', 'didUpdate'), hook('middle', 'didRender') - ]); + hooks = []; - hooks = []; + run(function() { + components.top.rerender(); + }); - run(function() { - components.top.rerender(); - }); + middleAttrs = { oldAttrs: { name: 'Tom Dale' }, newAttrs: { name: 'Tom Dale' } }; - middleAttrs = { oldAttrs: { name: 'Tom Dale' }, newAttrs: { name: 'Tom Dale' } }; + deepEqual(hooks, [ + hook('top', 'willUpdate'), hook('top', 'willRender'), - deepEqual(hooks, [ - hook('top', 'willUpdate'), hook('top', 'willRender'), + hook('middle', 'didUpdateAttrs', middleAttrs), hook('middle', 'didReceiveAttrs', middleAttrs), + hook('middle', 'willUpdate'), hook('middle', 'willRender'), - hook('middle', 'didUpdateAttrs', middleAttrs), hook('middle', 'didReceiveAttrs', middleAttrs), - hook('middle', 'willUpdate'), hook('middle', 'willRender'), + hook('bottom', 'didUpdateAttrs', bottomAttrs), hook('bottom', 'didReceiveAttrs', bottomAttrs), + hook('bottom', 'willUpdate'), hook('bottom', 'willRender'), + hook('bottom', 'didUpdate'), hook('bottom', 'didRender'), - hook('bottom', 'didUpdateAttrs', bottomAttrs), hook('bottom', 'didReceiveAttrs', bottomAttrs), - hook('bottom', 'willUpdate'), hook('bottom', 'willRender'), - hook('bottom', 'didUpdate'), hook('bottom', 'didRender'), + hook('middle', 'didUpdate'), hook('middle', 'didRender'), + hook('top', 'didUpdate'), hook('top', 'didRender') + ]); - hook('middle', 'didUpdate'), hook('middle', 'didRender'), - hook('top', 'didUpdate'), hook('top', 'didRender') - ]); + hooks = []; - hooks = []; + run(function() { + view.set('twitter', '@hipstertomdale'); + }); - run(function() { - view.set('twitter', '@hipstertomdale'); + // Because the `twitter` attr is only used by the topmost component, + // and not passed down, we do not expect to see lifecycle hooks + // called for child components. If the `didReceiveAttrs` hook used + // the new attribute to rerender itself imperatively, that would result + // in lifecycle hooks being invoked for the child. + + deepEqual(hooks, [ + hook('top', 'didUpdateAttrs', { oldAttrs: { twitter: '@tomdale' }, newAttrs: { twitter: '@hipstertomdale' } }), + hook('top', 'didReceiveAttrs', { oldAttrs: { twitter: '@tomdale' }, newAttrs: { twitter: '@hipstertomdale' } }), + hook('top', 'willUpdate'), + hook('top', 'willRender'), + hook('top', 'didUpdate'), hook('top', 'didRender') + ]); }); - // Because the `twitter` attr is only used by the topmost component, - // and not passed down, we do not expect to see lifecycle hooks - // called for child components. If the `didReceiveAttrs` hook used - // the new attribute to rerender itself imperatively, that would result - // in lifecycle hooks being invoked for the child. - - deepEqual(hooks, [ - hook('top', 'didUpdateAttrs', { oldAttrs: { twitter: '@tomdale' }, newAttrs: { twitter: '@hipstertomdale' } }), - hook('top', 'didReceiveAttrs', { oldAttrs: { twitter: '@tomdale' }, newAttrs: { twitter: '@hipstertomdale' } }), - hook('top', 'willUpdate'), - hook('top', 'willRender'), - hook('top', 'didUpdate'), hook('top', 'didRender') - ]); -}); - -QUnit.test('passing values through attrs causes lifecycle hooks to fire if the attribute values have changed', function() { - var components = {}; + QUnit.test('passing values through attrs causes lifecycle hooks to fire if the attribute values have changed', function() { + var components = {}; + + function component(label) { + return style.class.extend({ + init() { + this.label = label; + components[label] = this; + this._super.apply(this, arguments); + }, + + didInitAttrs(options) { + pushHook(label, 'didInitAttrs', options); + }, + + didUpdateAttrs(options) { + pushHook(label, 'didUpdateAttrs', options); + }, + + willUpdate(options) { + pushHook(label, 'willUpdate', options); + }, + + didReceiveAttrs(options) { + pushHook(label, 'didReceiveAttrs', options); + }, + + willRender() { + pushHook(label, 'willRender'); + }, + + didRender() { + pushHook(label, 'didRender'); + }, + + didInsertElement() { + pushHook(label, 'didInsertElement'); + }, + + didUpdate(options) { + pushHook(label, 'didUpdate', options); + } + }); + } - function component(label) { - return Component.extend({ - init() { - this.label = label; - components[label] = this; - this._super.apply(this, arguments); - }, + registry.register('component:the-top', component('top')); + registry.register('component:the-middle', component('middle')); + registry.register('component:the-bottom', component('bottom')); - didInitAttrs(options) { - pushHook(label, 'didInitAttrs', options); - }, + registry.register('template:components/the-top', compile(`
Top: ${invoke('the-middle', { twitterTop: 'attrs.twitter' })}
`)); + registry.register('template:components/the-middle', compile(`
Middle: ${invoke('the-bottom', { twitterMiddle: 'attrs.twitterTop' })}
`)); + registry.register('template:components/the-bottom', compile('
Bottom: {{attrs.twitterMiddle}}
')); - didUpdateAttrs(options) { - pushHook(label, 'didUpdateAttrs', options); - }, + view = EmberView.extend({ + template: compile(invoke('the-top', { twitter: 'view.twitter' })), + twitter: '@tomdale', + container: container + }).create(); - willUpdate(options) { - pushHook(label, 'willUpdate', options); - }, + runAppend(view); - didReceiveAttrs(options) { - pushHook(label, 'didReceiveAttrs', options); - }, + ok(component, 'The component was inserted'); + equal(jQuery('#qunit-fixture').text(), 'Top: Middle: Bottom: @tomdale'); - willRender() { - pushHook(label, 'willRender'); - }, + let topAttrs = { twitter: '@tomdale' }; + let middleAttrs = { twitterTop: '@tomdale' }; + let bottomAttrs = { twitterMiddle: '@tomdale' }; - didRender() { - pushHook(label, 'didRender'); - }, + deepEqual(hooks, [ + hook('top', 'didInitAttrs', { attrs: topAttrs }), hook('top', 'didReceiveAttrs', { newAttrs: topAttrs }), hook('top', 'willRender'), + hook('middle', 'didInitAttrs', { attrs: middleAttrs }), hook('middle', 'didReceiveAttrs', { newAttrs: middleAttrs }), hook('middle', 'willRender'), + hook('bottom', 'didInitAttrs', { attrs: bottomAttrs }), hook('bottom', 'didReceiveAttrs', { newAttrs: bottomAttrs }), hook('bottom', 'willRender'), + hook('bottom', 'didInsertElement'), hook('bottom', 'didRender'), + hook('middle', 'didInsertElement'), hook('middle', 'didRender'), + hook('top', 'didInsertElement'), hook('top', 'didRender') + ]); - didInsertElement() { - pushHook(label, 'didInsertElement'); - }, + hooks = []; - didUpdate(options) { - pushHook(label, 'didUpdate', options); - } + run(function() { + view.set('twitter', '@hipstertomdale'); }); - } - registry.register('component:the-top', component('top')); - registry.register('component:the-middle', component('middle')); - registry.register('component:the-bottom', component('bottom')); + // Because the `twitter` attr is used by the all of the components, + // the lifecycle hooks are invoked for all components. - registry.register('template:components/the-top', compile('Top: {{the-middle twitterTop=(readonly attrs.twitter)}}')); - registry.register('template:components/the-middle', compile('Middle: {{the-bottom twitterMiddle=(readonly attrs.twitterTop)}}')); - registry.register('template:components/the-bottom', compile('Bottom: {{attrs.twitterMiddle}}')); - view = EmberView.extend({ - template: compile('{{the-top twitter=(readonly view.twitter)}}'), - twitter: '@tomdale', - container: container - }).create(); + topAttrs = { oldAttrs: { twitter: '@tomdale' }, newAttrs: { twitter: '@hipstertomdale' } }; + middleAttrs = { oldAttrs: { twitterTop: '@tomdale' }, newAttrs: { twitterTop: '@hipstertomdale' } }; + bottomAttrs = { oldAttrs: { twitterMiddle: '@tomdale' }, newAttrs: { twitterMiddle: '@hipstertomdale' } }; - runAppend(view); + deepEqual(hooks, [ + hook('top', 'didUpdateAttrs', topAttrs), hook('top', 'didReceiveAttrs', topAttrs), + hook('top', 'willUpdate'), hook('top', 'willRender'), - ok(component, 'The component was inserted'); - equal(jQuery('#qunit-fixture').text(), 'Top: Middle: Bottom: @tomdale'); + hook('middle', 'didUpdateAttrs', middleAttrs), hook('middle', 'didReceiveAttrs', middleAttrs), + hook('middle', 'willUpdate'), hook('middle', 'willRender'), - let topAttrs = { twitter: '@tomdale' }; - let middleAttrs = { twitterTop: '@tomdale' }; - let bottomAttrs = { twitterMiddle: '@tomdale' }; + hook('bottom', 'didUpdateAttrs', bottomAttrs), hook('bottom', 'didReceiveAttrs', bottomAttrs), + hook('bottom', 'willUpdate'), hook('bottom', 'willRender'), + hook('bottom', 'didUpdate'), hook('bottom', 'didRender'), - deepEqual(hooks, [ - hook('top', 'didInitAttrs', { attrs: topAttrs }), hook('top', 'didReceiveAttrs', { newAttrs: topAttrs }), hook('top', 'willRender'), - hook('middle', 'didInitAttrs', { attrs: middleAttrs }), hook('middle', 'didReceiveAttrs', { newAttrs: middleAttrs }), hook('middle', 'willRender'), - hook('bottom', 'didInitAttrs', { attrs: bottomAttrs }), hook('bottom', 'didReceiveAttrs', { newAttrs: bottomAttrs }), hook('bottom', 'willRender'), - hook('bottom', 'didInsertElement'), hook('bottom', 'didRender'), - hook('middle', 'didInsertElement'), hook('middle', 'didRender'), - hook('top', 'didInsertElement'), hook('top', 'didRender') - ]); - - hooks = []; - - run(function() { - view.set('twitter', '@hipstertomdale'); - }); + hook('middle', 'didUpdate'), hook('middle', 'didRender'), + hook('top', 'didUpdate'), hook('top', 'didRender') + ]); - // Because the `twitter` attr is used by the all of the components, - // the lifecycle hooks are invoked for all components. - - - topAttrs = { oldAttrs: { twitter: '@tomdale' }, newAttrs: { twitter: '@hipstertomdale' } }; - middleAttrs = { oldAttrs: { twitterTop: '@tomdale' }, newAttrs: { twitterTop: '@hipstertomdale' } }; - bottomAttrs = { oldAttrs: { twitterMiddle: '@tomdale' }, newAttrs: { twitterMiddle: '@hipstertomdale' } }; + hooks = []; - deepEqual(hooks, [ - hook('top', 'didUpdateAttrs', topAttrs), hook('top', 'didReceiveAttrs', topAttrs), - hook('top', 'willUpdate'), hook('top', 'willRender'), + // In this case, because the attrs are passed down, all child components are invoked. - hook('middle', 'didUpdateAttrs', middleAttrs), hook('middle', 'didReceiveAttrs', middleAttrs), - hook('middle', 'willUpdate'), hook('middle', 'willRender'), + run(function() { + view.rerender(); + }); - hook('bottom', 'didUpdateAttrs', bottomAttrs), hook('bottom', 'didReceiveAttrs', bottomAttrs), - hook('bottom', 'willUpdate'), hook('bottom', 'willRender'), - hook('bottom', 'didUpdate'), hook('bottom', 'didRender'), + topAttrs = { oldAttrs: { twitter: '@hipstertomdale' }, newAttrs: { twitter: '@hipstertomdale' } }; + middleAttrs = { oldAttrs: { twitterTop: '@hipstertomdale' }, newAttrs: { twitterTop: '@hipstertomdale' } }; + bottomAttrs = { oldAttrs: { twitterMiddle: '@hipstertomdale' }, newAttrs: { twitterMiddle: '@hipstertomdale' } }; - hook('middle', 'didUpdate'), hook('middle', 'didRender'), - hook('top', 'didUpdate'), hook('top', 'didRender') - ]); + deepEqual(hooks, [ + hook('top', 'didUpdateAttrs', topAttrs), hook('top', 'didReceiveAttrs', topAttrs), + hook('top', 'willUpdate'), hook('top', 'willRender'), - hooks = []; + hook('middle', 'didUpdateAttrs', middleAttrs), hook('middle', 'didReceiveAttrs', middleAttrs), + hook('middle', 'willUpdate'), hook('middle', 'willRender'), - // In this case, because the attrs are passed down, all child components are invoked. + hook('bottom', 'didUpdateAttrs', bottomAttrs), hook('bottom', 'didReceiveAttrs', bottomAttrs), + hook('bottom', 'willUpdate'), hook('bottom', 'willRender'), + hook('bottom', 'didUpdate'), hook('bottom', 'didRender'), - run(function() { - view.rerender(); + hook('middle', 'didUpdate'), hook('middle', 'didRender'), + hook('top', 'didUpdate'), hook('top', 'didRender') + ]); }); - topAttrs = { oldAttrs: { twitter: '@hipstertomdale' }, newAttrs: { twitter: '@hipstertomdale' } }; - middleAttrs = { oldAttrs: { twitterTop: '@hipstertomdale' }, newAttrs: { twitterTop: '@hipstertomdale' } }; - bottomAttrs = { oldAttrs: { twitterMiddle: '@hipstertomdale' }, newAttrs: { twitterMiddle: '@hipstertomdale' } }; - - deepEqual(hooks, [ - hook('top', 'didUpdateAttrs', topAttrs), hook('top', 'didReceiveAttrs', topAttrs), - hook('top', 'willUpdate'), hook('top', 'willRender'), - - hook('middle', 'didUpdateAttrs', middleAttrs), hook('middle', 'didReceiveAttrs', middleAttrs), - hook('middle', 'willUpdate'), hook('middle', 'willRender'), - - hook('bottom', 'didUpdateAttrs', bottomAttrs), hook('bottom', 'didReceiveAttrs', bottomAttrs), - hook('bottom', 'willUpdate'), hook('bottom', 'willRender'), - hook('bottom', 'didUpdate'), hook('bottom', 'didRender'), + QUnit.test('changing a component\'s displayed properties inside didInsertElement() is deprecated', function(assert) { + let component = style.class.extend({ + layout: compile('
{{debugger}}{{handle}}
'), + handle: '@wycats', + container: container, - hook('middle', 'didUpdate'), hook('middle', 'didRender'), - hook('top', 'didUpdate'), hook('top', 'didRender') - ]); -}); - -QUnit.test('changing a component\'s displayed properties inside didInsertElement() is deprecated', function(assert) { - let component = Component.extend({ - layout: compile('{{handle}}'), - handle: '@wycats', - container: container, - - didInsertElement() { - this.set('handle', '@tomdale'); - } - }).create(); + didInsertElement() { + this.set('handle', '@tomdale'); + } + }).create(); - expectDeprecation(() => { - runAppend(component); - }, /modified inside the didInsertElement hook/); + expectDeprecation(() => { + runAppend(component); + }, /modified inside the didInsertElement hook/); - assert.strictEqual(component.$().text(), '@tomdale'); + assert.strictEqual(component.$().text(), '@tomdale'); - run(() => { - component.destroy(); + run(() => { + component.destroy(); + }); }); }); diff --git a/packages/ember-htmlbars/tests/integration/helper-lookup-test.js b/packages/ember-htmlbars/tests/integration/helper-lookup-test.js index 7d50deb299b..c7a608af634 100644 --- a/packages/ember-htmlbars/tests/integration/helper-lookup-test.js +++ b/packages/ember-htmlbars/tests/integration/helper-lookup-test.js @@ -1,7 +1,7 @@ import Registry from 'container/registry'; import compile from 'ember-template-compiler/system/compile'; import ComponentLookup from 'ember-views/component_lookup'; -import Component from 'ember-views/views/component'; +import Component from 'ember-views/components/component'; import { helper } from 'ember-htmlbars/helper'; import { runAppend, runDestroy } from 'ember-runtime/tests/utils'; diff --git a/packages/ember-htmlbars/tests/integration/mutable_binding_test.js b/packages/ember-htmlbars/tests/integration/mutable_binding_test.js index a40de1cd1d1..3f770d1aef7 100644 --- a/packages/ember-htmlbars/tests/integration/mutable_binding_test.js +++ b/packages/ember-htmlbars/tests/integration/mutable_binding_test.js @@ -4,7 +4,8 @@ import Registry from 'container/registry'; //import jQuery from "ember-views/system/jquery"; import compile from 'ember-template-compiler/system/compile'; import ComponentLookup from 'ember-views/component_lookup'; -import Component from 'ember-views/views/component'; +import Component from 'ember-views/components/component'; +import GlimmerComponent from 'ember-htmlbars/glimmer-component'; import { runAppend, runDestroy } from 'ember-runtime/tests/utils'; import run from 'ember-metal/run_loop'; import { computed } from 'ember-metal/computed'; @@ -342,10 +343,10 @@ QUnit.test('automatic mutable bindings to constant non-streams tolerate attempts // jscs:disable validateIndentation if (isEnabled('ember-htmlbars-component-generation')) { -QUnit.skip('mutable bindings work as angle-bracket component attributes', function(assert) { +QUnit.test('mutable bindings work as angle-bracket component attributes', function(assert) { var middle; - registry.register('component:middle-mut', Component.extend({ + registry.register('component:middle-mut', GlimmerComponent.extend({ // no longer mutable layout: compile(''), @@ -354,7 +355,7 @@ QUnit.skip('mutable bindings work as angle-bracket component attributes', functi } })); - registry.register('component:bottom-mut', Component.extend({ + registry.register('component:bottom-mut', GlimmerComponent.extend({ layout: compile('

{{attrs.setMe}}

') })); @@ -375,10 +376,10 @@ QUnit.skip('mutable bindings work as angle-bracket component attributes', functi assert.strictEqual(view.get('val'), 13, 'the set propagated back up'); }); - QUnit.skip('a simple mutable binding using `mut` can be converted into an immutable binding with angle-bracket components', function(assert) { +QUnit.test('a simple mutable binding using `mut` can be converted into an immutable binding with angle-bracket components', function(assert) { var middle, bottom; - registry.register('component:middle-mut', Component.extend({ + registry.register('component:middle-mut', GlimmerComponent.extend({ // no longer mutable layout: compile(''), @@ -387,7 +388,7 @@ QUnit.skip('mutable bindings work as angle-bracket component attributes', functi } })); - registry.register('component:bottom-mut', Component.extend({ + registry.register('component:bottom-mut', GlimmerComponent.extend({ layout: compile('

{{attrs.setMe}}

'), didInsertElement() { diff --git a/packages/ember-htmlbars/tests/integration/void-element-component-test.js b/packages/ember-htmlbars/tests/integration/void-element-component-test.js index 337d31b56da..284058b17b6 100644 --- a/packages/ember-htmlbars/tests/integration/void-element-component-test.js +++ b/packages/ember-htmlbars/tests/integration/void-element-component-test.js @@ -3,7 +3,7 @@ import { Registry } from 'ember-runtime/system/container'; import compile from 'ember-template-compiler/system/compile'; import { runAppend, runDestroy } from 'ember-runtime/tests/utils'; import ComponentLookup from 'ember-views/component_lookup'; -import Component from 'ember-views/views/component'; +import Component from 'ember-views/components/component'; var registry, container, view; diff --git a/packages/ember-htmlbars/tests/integration/will-destroy-element-hook-test.js b/packages/ember-htmlbars/tests/integration/will-destroy-element-hook-test.js index 4021b49a901..268858224e1 100644 --- a/packages/ember-htmlbars/tests/integration/will-destroy-element-hook-test.js +++ b/packages/ember-htmlbars/tests/integration/will-destroy-element-hook-test.js @@ -1,5 +1,5 @@ import run from 'ember-metal/run_loop'; -import Component from 'ember-views/views/component'; +import Component from 'ember-views/components/component'; import compile from 'ember-template-compiler/system/compile'; import { runAppend, runDestroy } from 'ember-runtime/tests/utils'; diff --git a/packages/ember-htmlbars/tests/system/append-templated-view-test.js b/packages/ember-htmlbars/tests/system/append-templated-view-test.js index 26cb06aed25..debfd1c131c 100644 --- a/packages/ember-htmlbars/tests/system/append-templated-view-test.js +++ b/packages/ember-htmlbars/tests/system/append-templated-view-test.js @@ -1,6 +1,6 @@ import { runAppend, runDestroy } from 'ember-runtime/tests/utils'; import EmberView from 'ember-views/views/view'; -import EmberComponent from 'ember-views/views/component'; +import EmberComponent from 'ember-views/components/component'; import compile from 'ember-template-compiler/system/compile'; import { registerKeyword, resetKeyword } from 'ember-htmlbars/tests/utils'; diff --git a/packages/ember-htmlbars/tests/system/render_env_test.js b/packages/ember-htmlbars/tests/system/render_env_test.js index 204efaab9b1..f3fd0b922d0 100644 --- a/packages/ember-htmlbars/tests/system/render_env_test.js +++ b/packages/ember-htmlbars/tests/system/render_env_test.js @@ -2,7 +2,7 @@ import EmberView from 'ember-views/views/view'; import Registry from 'container/registry'; import compile from 'ember-template-compiler/system/compile'; import ComponentLookup from 'ember-views/component_lookup'; -import Component from 'ember-views/views/component'; +import Component from 'ember-views/components/component'; import RenderEnv from 'ember-htmlbars/system/render-env'; import { runAppend, runDestroy } from 'ember-runtime/tests/utils'; import run from 'ember-metal/run_loop'; diff --git a/packages/ember-routing-htmlbars/tests/helpers/closure_action_test.js b/packages/ember-routing-htmlbars/tests/helpers/closure_action_test.js index e7bb8608205..d42b10dda29 100644 --- a/packages/ember-routing-htmlbars/tests/helpers/closure_action_test.js +++ b/packages/ember-routing-htmlbars/tests/helpers/closure_action_test.js @@ -1,6 +1,6 @@ import run from 'ember-metal/run_loop'; import compile from 'ember-template-compiler/system/compile'; -import EmberComponent from 'ember-views/views/component'; +import EmberComponent from 'ember-views/components/component'; import { computed } from 'ember-metal/computed'; import { diff --git a/packages/ember-routing-htmlbars/tests/helpers/element_action_test.js b/packages/ember-routing-htmlbars/tests/helpers/element_action_test.js index eaa5d0ff47d..be14fb1e806 100644 --- a/packages/ember-routing-htmlbars/tests/helpers/element_action_test.js +++ b/packages/ember-routing-htmlbars/tests/helpers/element_action_test.js @@ -9,7 +9,7 @@ import EmberController from 'ember-runtime/controllers/controller'; import compile from 'ember-template-compiler/system/compile'; import EmberView from 'ember-views/views/view'; -import EmberComponent from 'ember-views/views/component'; +import EmberComponent from 'ember-views/components/component'; import jQuery from 'ember-views/system/jquery'; import { ActionHelper } from 'ember-routing-htmlbars/keywords/element-action'; diff --git a/packages/ember-routing-views/lib/views/link.js b/packages/ember-routing-views/lib/views/link.js index 6207740edea..83b8cff1b73 100644 --- a/packages/ember-routing-views/lib/views/link.js +++ b/packages/ember-routing-views/lib/views/link.js @@ -10,7 +10,7 @@ import { set } from 'ember-metal/property_set'; import { computed } from 'ember-metal/computed'; import { deprecatingAlias } from 'ember-metal/computed_macros'; import { isSimpleClick } from 'ember-views/system/utils'; -import EmberComponent from 'ember-views/views/component'; +import EmberComponent from 'ember-views/components/component'; import inject from 'ember-runtime/inject'; import 'ember-runtime/system/service'; // creates inject.service import ControllerMixin from 'ember-runtime/mixins/controller'; 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 index 90cb9e466c5..25e65f41d80 100644 --- a/packages/ember-template-compiler/lib/plugins/transform-top-level-components.js +++ b/packages/ember-template-compiler/lib/plugins/transform-top-level-components.js @@ -1,3 +1,5 @@ +import isEnabled from 'ember-metal/features'; + function TransformTopLevelComponents() { // set later within HTMLBars to the syntax package this.syntax = null; @@ -9,14 +11,34 @@ function TransformTopLevelComponents() { @param {AST} The AST to be transformed. */ TransformTopLevelComponents.prototype.transform = function TransformTopLevelComponents_transform(ast) { - hasSingleComponentNode(ast.body, component => { - component.tag = `@${component.tag}`; + let b = this.syntax.builders; + + hasSingleComponentNode(ast, component => { + if (component.type === 'ComponentNode') { + component.tag = `@${component.tag}`; + component.isStatic = true; + } + }, element => { + let hasTripleCurlies = element.attributes.some(attr => attr.value.escaped === false); + + if (element.modifiers.length || hasTripleCurlies) { + return element; + } else { + // TODO: Properly copy loc from children + let program = b.program(element.children); + let component = b.component(`@<${element.tag}>`, element.attributes, program, element.loc); + component.isStatic = true; + return component; + } }); return ast; }; -function hasSingleComponentNode(body, callback) { +function hasSingleComponentNode(program, componentCallback, elementCallback) { + let { loc, body } = program; + if (!loc || loc.start.line !== 1 || loc.start.column !== 0) { return; } + let lastComponentNode; let lastIndex; let nodeCount = 0; @@ -39,7 +61,10 @@ function hasSingleComponentNode(body, callback) { if (!lastComponentNode) { return; } if (lastComponentNode.type === 'ComponentNode') { - callback(lastComponentNode); + componentCallback(lastComponentNode); + } else if (isEnabled('ember-htmlbars-component-generation')) { + let component = elementCallback(lastComponentNode); + body.splice(lastIndex, 1, component); } } diff --git a/packages/ember-template-compiler/lib/system/compile_options.js b/packages/ember-template-compiler/lib/system/compile_options.js index 52419f98782..cd67221035e 100644 --- a/packages/ember-template-compiler/lib/system/compile_options.js +++ b/packages/ember-template-compiler/lib/system/compile_options.js @@ -40,7 +40,7 @@ export default function(_options) { options.buildMeta = function buildMeta(program) { return { - topLevel: detectTopLevel(program), + fragmentReason: fragmentReason(program), revision: 'Ember@VERSION_STRING_PLACEHOLDER', loc: program.loc, moduleName: options.moduleName @@ -50,14 +50,15 @@ export default function(_options) { return options; } -function detectTopLevel(program) { +function fragmentReason(program) { let { loc, body } = program; - if (!loc || loc.start.line !== 1 || loc.start.column !== 0) { return null; } + if (!loc || loc.start.line !== 1 || loc.start.column !== 0) { return false; } - let lastComponentNode; - let lastIndex; + let candidate; let nodeCount = 0; + let problems = {}; + for (let i = 0, l = body.length; i < l; i++) { let curr = body[i]; @@ -65,21 +66,31 @@ function detectTopLevel(program) { if (curr.type === 'TextNode' && /^[\s]*$/.test(curr.chars)) { continue; } // has multiple root elements if we've been here before - if (nodeCount++ > 0) { return false; } + if (nodeCount++ > 0) { problems['multiple-nodes'] = true; } if (curr.type === 'ComponentNode' || curr.type === 'ElementNode') { - lastComponentNode = curr; - lastIndex = i; + candidate = curr; + } else { + problems['wrong-type'] = true; } } - if (!lastComponentNode) { return null; } + if (nodeCount === 0) { + return { name: 'missing-wrapper', problems: ['empty-body'] }; + } - if (lastComponentNode.type === 'ComponentNode') { - let tag = lastComponentNode.tag; - if (tag.charAt(0) !== '<') { return null; } - return tag.slice(1, -1); + let problemList = Object.keys(problems); + if (problemList.length) { + return { name: 'missing-wrapper', problems: problemList }; } - return null; + if (candidate.type === 'ComponentNode') { + return false; + } else if (candidate.modifiers.length) { + return { name: 'modifiers', modifiers: candidate.modifiers.map(m => m.path.original) }; + } else if (candidate.attributes.some(attr => !attr.value.escaped)) { + return { name: 'triple-curlies' }; + } else { + return false; + } } diff --git a/packages/ember-views/lib/views/component.js b/packages/ember-views/lib/components/component.js similarity index 100% rename from packages/ember-views/lib/views/component.js rename to packages/ember-views/lib/components/component.js diff --git a/packages/ember-views/lib/main.js b/packages/ember-views/lib/main.js index 153d967eb24..d4cd31e7d7c 100644 --- a/packages/ember-views/lib/main.js +++ b/packages/ember-views/lib/main.js @@ -21,8 +21,8 @@ import Renderer from 'ember-metal-views/renderer'; import { DeprecatedCoreView } from 'ember-views/views/core_view'; import { DeprecatedView } from 'ember-views/views/view'; import { DeprecatedContainerView } from 'ember-views/views/container_view'; -import { DeprecatedCollectionView } from 'ember-views/views/collection_view'; -import Component from 'ember-views/views/component'; +import CollectionView from 'ember-views/views/collection_view'; +import Component from 'ember-views/components/component'; import EventDispatcher from 'ember-views/system/event_dispatcher'; import ViewTargetActionSupport from 'ember-views/mixins/view_target_action_support'; @@ -67,7 +67,7 @@ if (Ember.ENV._ENABLE_LEGACY_VIEW_SUPPORT) { Ember.View.cloneStates = cloneStates; Ember.View._Renderer = Renderer; Ember.ContainerView = DeprecatedContainerView; - Ember.CollectionView = DeprecatedCollectionView; + Ember.CollectionView = CollectionView; } Ember._Renderer = Renderer; diff --git a/packages/ember-views/lib/mixins/legacy_child_views_support.js b/packages/ember-views/lib/mixins/legacy_child_views_support.js new file mode 100644 index 00000000000..c4fc6fe4501 --- /dev/null +++ b/packages/ember-views/lib/mixins/legacy_child_views_support.js @@ -0,0 +1,20 @@ +import { Mixin } from 'ember-metal/mixin'; +import { get } from 'ember-metal/property_get'; +import { set } from 'ember-metal/property_set'; + +export default Mixin.create({ + linkChild(instance) { + instance.container = this.container; + if (get(instance, 'parentView') !== this) { + // linkChild should be idempotent + set(instance, 'parentView', this); + instance.trigger('parentViewDidChange'); + } + instance.ownerView = this.ownerView; + }, + + unlinkChild(instance) { + set(instance, 'parentView', null); + instance.trigger('parentViewDidChange'); + } +}); diff --git a/packages/ember-views/lib/mixins/legacy_view_support.js b/packages/ember-views/lib/mixins/legacy_view_support.js index ec5dfd6f087..7b2a136b51f 100644 --- a/packages/ember-views/lib/mixins/legacy_view_support.js +++ b/packages/ember-views/lib/mixins/legacy_view_support.js @@ -3,7 +3,7 @@ @submodule ember-views */ import Ember from 'ember-metal/core'; -import { Mixin } from 'ember-metal/mixin'; +import { Mixin, observer } from 'ember-metal/mixin'; import { get } from 'ember-metal/property_get'; /** @@ -100,7 +100,19 @@ var LegacyViewSupport = Mixin.create({ if (view instanceof klass) { return view; } view = get(view, 'parentView'); } - } + }, + + /** + If a value that affects template rendering changes, the view should be + re-rendered to reflect the new value. + + @method _contextDidChange + @private + @private + */ + _contextDidChange: observer('context', function() { + this.rerender(); + }) }); export default LegacyViewSupport; diff --git a/packages/ember-views/lib/mixins/normalized_rerender_if_needed.js b/packages/ember-views/lib/mixins/normalized_rerender_if_needed.js deleted file mode 100644 index 12e5989dd63..00000000000 --- a/packages/ember-views/lib/mixins/normalized_rerender_if_needed.js +++ /dev/null @@ -1,40 +0,0 @@ -/** -@module ember -@submodule ember-views -*/ - -import { get } from 'ember-metal/property_get'; -import { Mixin } from 'ember-metal/mixin'; -import merge from 'ember-metal/merge'; -import { - cloneStates, - states as viewStates -} from 'ember-views/views/states'; - -var states = cloneStates(viewStates); - -merge(states._default, { - rerenderIfNeeded() { return this; } -}); - -merge(states.inDOM, { - rerenderIfNeeded(view) { - if (view.normalizedValue() !== view._lastNormalizedValue) { - view.rerender(); - } - } -}); - -export default Mixin.create({ - _states: states, - - normalizedValue() { - var value = this.lazyValue.value(); - var valueNormalizer = get(this, 'valueNormalizerFunc'); - return valueNormalizer ? valueNormalizer(value) : value; - }, - - rerenderIfNeeded() { - this.currentState.rerenderIfNeeded(this); - } -}); diff --git a/packages/ember-views/lib/mixins/view_child_views_support.js b/packages/ember-views/lib/mixins/view_child_views_support.js index 2644b814d8d..a44c5c99992 100644 --- a/packages/ember-views/lib/mixins/view_child_views_support.js +++ b/packages/ember-views/lib/mixins/view_child_views_support.js @@ -125,16 +125,11 @@ export default Mixin.create({ linkChild(instance) { instance.container = this.container; - if (get(instance, 'parentView') !== this) { - // linkChild should be idempotentj - set(instance, 'parentView', this); - instance.trigger('parentViewDidChange'); - } + instance.parentView = this; instance.ownerView = this.ownerView; }, unlinkChild(instance) { - set(instance, 'parentView', null); - instance.trigger('parentViewDidChange'); + instance.parentView = null; } }); diff --git a/packages/ember-views/lib/mixins/view_support.js b/packages/ember-views/lib/mixins/view_support.js new file mode 100644 index 00000000000..3f7d2e86012 --- /dev/null +++ b/packages/ember-views/lib/mixins/view_support.js @@ -0,0 +1,775 @@ +import Ember from 'ember-metal/core'; +import EmberError from 'ember-metal/error'; +import { get } from 'ember-metal/property_get'; +import run from 'ember-metal/run_loop'; +import { addObserver, removeObserver } from 'ember-metal/observer'; +import { guidFor } from 'ember-metal/utils'; +import { computed } from 'ember-metal/computed'; +import { Mixin } from 'ember-metal/mixin'; + +import jQuery from 'ember-views/system/jquery'; + +function K() { return this; } + +export default Mixin.create({ + concatenatedProperties: ['attributeBindings'], + + /** + @property isView + @type Boolean + @default true + @static + @private + */ + isView: true, + + // .......................................................... + // TEMPLATE SUPPORT + // + + /** + The name of the template to lookup if no template is provided. + + By default `Ember.View` will lookup a template with this name in + `Ember.TEMPLATES` (a shared global object). + + @property templateName + @type String + @default null + @private + */ + templateName: null, + + /** + The name of the layout to lookup if no layout is provided. + + By default `Ember.View` will lookup a template with this name in + `Ember.TEMPLATES` (a shared global object). + + @property layoutName + @type String + @default null + @private + */ + layoutName: null, + + /** + The template used to render the view. This should be a function that + accepts an optional context parameter and returns a string of HTML that + will be inserted into the DOM relative to its parent view. + + In general, you should set the `templateName` property instead of setting + the template yourself. + + @property template + @type Function + @private + */ + template: computed({ + get() { + var templateName = get(this, 'templateName'); + var template = this.templateForName(templateName, 'template'); + Ember.assert('You specified the templateName ' + templateName + ' for ' + this + ', but it did not exist.', !templateName || !!template); + return template || get(this, 'defaultTemplate'); + }, + set(key, value) { + if (value !== undefined) { return value; } + return get(this, key); + } + }), + + /** + A view may contain a layout. A layout is a regular template but + supersedes the `template` property during rendering. It is the + responsibility of the layout template to retrieve the `template` + property from the view (or alternatively, call `Handlebars.helpers.yield`, + `{{yield}}`) to render it in the correct location. + + This is useful for a view that has a shared wrapper, but which delegates + the rendering of the contents of the wrapper to the `template` property + on a subclass. + + @property layout + @type Function + @private + */ + layout: computed({ + get(key) { + var layoutName = get(this, 'layoutName'); + var layout = this.templateForName(layoutName, 'layout'); + + Ember.assert('You specified the layoutName ' + layoutName + ' for ' + this + ', but it did not exist.', !layoutName || !!layout); + + return layout || get(this, 'defaultLayout'); + }, + + set(key, value) { + return value; + } + }), + + templateForName(name, type) { + if (!name) { return; } + Ember.assert('templateNames are not allowed to contain periods: ' + name, name.indexOf('.') === -1); + + if (!this.container) { + throw new EmberError('Container was not found when looking up a views template. ' + + 'This is most likely due to manually instantiating an Ember.View. ' + + 'See: http://git.io/EKPpnA'); + } + + return this.container.lookup('template:' + name); + }, + + /** + Return the nearest ancestor that is an instance of the provided + class or mixin. + + @method nearestOfType + @param {Class,Mixin} klass Subclass of Ember.View (or Ember.View itself), + or an instance of Ember.Mixin. + @return Ember.View + @private + */ + nearestOfType(klass) { + var view = get(this, 'parentView'); + var isOfType = klass instanceof Mixin ? + function(view) { return klass.detect(view); } : + function(view) { return klass.detect(view.constructor); }; + + while (view) { + if (isOfType(view)) { return view; } + view = get(view, 'parentView'); + } + }, + + /** + Return the nearest ancestor that has a given property. + + @method nearestWithProperty + @param {String} property A property name + @return Ember.View + @private + */ + nearestWithProperty(property) { + var view = get(this, 'parentView'); + + while (view) { + if (property in view) { return view; } + view = get(view, 'parentView'); + } + }, + + /** + Renders the view again. This will work regardless of whether the + view is already in the DOM or not. If the view is in the DOM, the + rendering process will be deferred to give bindings a chance + to synchronize. + + If children were added during the rendering process using `appendChild`, + `rerender` will remove them, because they will be added again + if needed by the next `render`. + + In general, if the display of your view changes, you should modify + the DOM element directly instead of manually calling `rerender`, which can + be slow. + + @method rerender + @public + */ + rerender() { + return this.currentState.rerender(this); + }, + + // .......................................................... + // ELEMENT SUPPORT + // + + /** + Returns the current DOM element for the view. + + @property element + @type DOMElement + @public + */ + element: null, + + /** + Returns a jQuery object for this view's element. If you pass in a selector + string, this method will return a jQuery object, using the current element + as its buffer. + + For example, calling `view.$('li')` will return a jQuery object containing + all of the `li` elements inside the DOM element of this view. + + @method $ + @param {String} [selector] a jQuery-compatible selector string + @return {jQuery} the jQuery object for the DOM node + @public + */ + $(sel) { + Ember.assert('You cannot access this.$() on a component with `tagName: \'\'` specified.', this.tagName !== ''); + return this.currentState.$(this, sel); + }, + + forEachChildView(callback) { + var childViews = this.childViews; + + if (!childViews) { return this; } + + var len = childViews.length; + var view, idx; + + for (idx = 0; idx < len; idx++) { + view = childViews[idx]; + callback(view); + } + + return this; + }, + + /** + Appends the view's element to the specified parent element. + + If the view does not have an HTML representation yet, `createElement()` + will be called automatically. + + Note that this method just schedules the view to be appended; the DOM + element will not be appended to the given element until all bindings have + finished synchronizing. + + This is not typically a function that you will need to call directly when + building your application. You might consider using `Ember.ContainerView` + instead. If you do need to use `appendTo`, be sure that the target element + you are providing is associated with an `Ember.Application` and does not + have an ancestor element that is associated with an Ember view. + + @method appendTo + @param {String|DOMElement|jQuery} A selector, element, HTML string, or jQuery object + @return {Ember.View} receiver + @private + */ + appendTo(selector) { + var target = jQuery(selector); + + Ember.assert('You tried to append to (' + selector + ') but that isn\'t in the DOM', target.length > 0); + Ember.assert('You cannot append to an existing Ember.View. Consider using Ember.ContainerView instead.', !target.is('.ember-view') && !target.parents().is('.ember-view')); + + this.renderer.appendTo(this, target[0]); + + return this; + }, + + /** + @private + + Creates a new DOM element, renders the view into it, then returns the + element. + + By default, the element created and rendered into will be a `BODY` element, + since this is the default context that views are rendered into when being + inserted directly into the DOM. + + ```js + var element = view.renderToElement(); + element.tagName; // => "BODY" + ``` + + You can override the kind of element rendered into and returned by + specifying an optional tag name as the first argument. + + ```js + var element = view.renderToElement('table'); + element.tagName; // => "TABLE" + ``` + + This method is useful if you want to render the view into an element that + is not in the document's body. Instead, a new `body` element, detached from + the DOM is returned. FastBoot uses this to serialize the rendered view into + a string for transmission over the network. + + ```js + app.visit('/').then(function(instance) { + var element; + Ember.run(function() { + element = renderToElement(instance); + }); + + res.send(serialize(element)); + }); + ``` + + @method renderToElement + @param {String} tagName The tag of the element to create and render into. Defaults to "body". + @return {HTMLBodyElement} element + @private + */ + renderToElement(tagName) { + tagName = tagName || 'body'; + + var element = this.renderer._dom.createElement(tagName); + + this.renderer.appendTo(this, element); + return element; + }, + + /** + Replaces the content of the specified parent element with this view's + element. If the view does not have an HTML representation yet, + the element will be generated automatically. + + Note that this method just schedules the view to be appended; the DOM + element will not be appended to the given element until all bindings have + finished synchronizing + + @method replaceIn + @param {String|DOMElement|jQuery} target A selector, element, HTML string, or jQuery object + @return {Ember.View} received + @private + */ + replaceIn(selector) { + var target = jQuery(selector); + + Ember.assert('You tried to replace in (' + selector + ') but that isn\'t in the DOM', target.length > 0); + Ember.assert('You cannot replace an existing Ember.View. Consider using Ember.ContainerView instead.', !target.is('.ember-view') && !target.parents().is('.ember-view')); + + this.renderer.replaceIn(this, target[0]); + + return this; + }, + + /** + Appends the view's element to the document body. If the view does + not have an HTML representation yet + the element will be generated automatically. + + If your application uses the `rootElement` property, you must append + the view within that element. Rendering views outside of the `rootElement` + is not supported. + + Note that this method just schedules the view to be appended; the DOM + element will not be appended to the document body until all bindings have + finished synchronizing. + + @method append + @return {Ember.View} receiver + @private + */ + append() { + return this.appendTo(document.body); + }, + + /** + Removes the view's element from the element to which it is attached. + + @method remove + @return {Ember.View} receiver + @private + */ + remove() { + // What we should really do here is wait until the end of the run loop + // to determine if the element has been re-appended to a different + // element. + // In the interim, we will just re-render if that happens. It is more + // important than elements get garbage collected. + if (!this.removedFromDOM) { this.destroyElement(); } + + // Set flag to avoid future renders + this._willInsert = false; + }, + + /** + The HTML `id` of the view's element in the DOM. You can provide this + value yourself but it must be unique (just as in HTML): + + ```handlebars + {{my-component elementId="a-really-cool-id"}} + ``` + + If not manually set a default value will be provided by the framework. + + Once rendered an element's `elementId` is considered immutable and you + should never change it. If you need to compute a dynamic value for the + `elementId`, you should do this when the component or element is being + instantiated: + + ```javascript + export default Ember.Component.extend({ + setElementId: Ember.on('init', function() { + var index = this.get('index'); + this.set('elementId', 'component-id' + index); + }) + }); + ``` + + @property elementId + @type String + @public + */ + elementId: null, + + /** + Attempts to discover the element in the parent element. The default + implementation looks for an element with an ID of `elementId` (or the + view's guid if `elementId` is null). You can override this method to + provide your own form of lookup. For example, if you want to discover your + element using a CSS class name instead of an ID. + + @method findElementInParentElement + @param {DOMElement} parentElement The parent's DOM element + @return {DOMElement} The discovered element + @private + */ + findElementInParentElement(parentElem) { + var id = '#' + this.elementId; + return jQuery(id)[0] || jQuery(id, parentElem)[0]; + }, + + /** + Creates a DOM representation of the view and all of its child views by + recursively calling the `render()` method. Once the element is created, + it sets the `element` property of the view to the rendered element. + + After the element has been inserted into the DOM, `didInsertElement` will + be called on this view and all of its child views. + + @method createElement + @return {Ember.View} receiver + @private + */ + createElement() { + if (this.element) { return this; } + + this.renderer.createElement(this); + + return this; + }, + + /** + Called when a view is going to insert an element into the DOM. + + @event willInsertElement + @public + */ + willInsertElement: K, + + /** + Called when the element of the view has been inserted into the DOM + or after the view was re-rendered. Override this function to do any + set up that requires an element in the document body. + + When a view has children, didInsertElement will be called on the + child view(s) first, bubbling upwards through the hierarchy. + + @event didInsertElement + @public + */ + didInsertElement: K, + + /** + Called when the view is about to rerender, but before anything has + been torn down. This is a good opportunity to tear down any manual + observers you have installed based on the DOM state + + @event willClearRender + @public + */ + willClearRender: K, + + /** + Destroys any existing element along with the element for any child views + as well. If the view does not currently have a element, then this method + will do nothing. + + If you implement `willDestroyElement()` on your view, then this method will + be invoked on your view before your element is destroyed to give you a + chance to clean up any event handlers, etc. + + If you write a `willDestroyElement()` handler, you can assume that your + `didInsertElement()` handler was called earlier for the same element. + + You should not call or override this method yourself, but you may + want to implement the above callbacks. + + @method destroyElement + @return {Ember.View} receiver + @private + */ + destroyElement() { + return this.currentState.destroyElement(this); + }, + + /** + Called when the element of the view is going to be destroyed. Override + this function to do any teardown that requires an element, like removing + event listeners. + + Please note: any property changes made during this event will have no + effect on object observers. + + @event willDestroyElement + @public + */ + willDestroyElement: K, + + /** + Called when the parentView property has changed. + + @event parentViewDidChange + @private + */ + parentViewDidChange: K, + + // .......................................................... + // STANDARD RENDER PROPERTIES + // + + /** + Tag name for the view's outer element. The tag name is only used when an + element is first created. If you change the `tagName` for an element, you + must destroy and recreate the view element. + + By default, the render buffer will use a `
` tag for views. + + @property tagName + @type String + @default null + @public + */ + + // We leave this null by default so we can tell the difference between + // the default case and a user-specified tag. + tagName: null, + + /* + Used to specify a default tagName that can be overridden when extending + or invoking from a template. + + @property _defaultTagName + @private + */ + + /** + Normally, Ember's component model is "write-only". The component takes a + bunch of attributes that it got passed in, and uses them to render its + template. + + One nice thing about this model is that if you try to set a value to the + same thing as last time, Ember (through HTMLBars) will avoid doing any + work on the DOM. + + This is not just a performance optimization. If an attribute has not + changed, it is important not to clobber the element's "hidden state". + For example, if you set an input's `value` to the same value as before, + it will clobber selection state and cursor position. In other words, + setting an attribute is not **always** idempotent. + + This method provides a way to read an element's attribute and also + update the last value Ember knows about at the same time. This makes + setting an attribute idempotent. + + In particular, what this means is that if you get an `` element's + `value` attribute and then re-render the template with the same value, + it will avoid clobbering the cursor and selection position. + + Since most attribute sets are idempotent in the browser, you typically + can get away with reading attributes using jQuery, but the most reliable + way to do so is through this method. + + @method readDOMAttr + @param {String} name the name of the attribute + @return String + @public + */ + readDOMAttr(name) { + let attr = this._renderNode.childNodes.filter(node => node.attrName === name)[0]; + if (!attr) { return null; } + return attr.getContent(); + }, + + // ....................................................... + // CORE DISPLAY METHODS + // + + /** + Setup a view, but do not finish waking it up. + + * configure `childViews` + * register the view with the global views hash, which is used for event + dispatch + + @method init + @private + */ + init() { + if (!this.elementId) { + this.elementId = guidFor(this); + } + + this.scheduledRevalidation = false; + + this._super(...arguments); + this.renderer.componentInitAttrs(this, this.attrs || {}); + + Ember.assert( + 'Using a custom `.render` function is no longer supported.', + !this.render + ); + }, + + __defineNonEnumerable(property) { + this[property.name] = property.descriptor.value; + }, + + revalidate() { + this.renderer.revalidateTopLevelView(this); + this.scheduledRevalidation = false; + }, + + scheduleRevalidate(node, label, manualRerender) { + if (node && !this._dispatching && node.guid in this.env.renderedNodes) { + if (manualRerender) { + Ember.deprecate(`You manually rerendered ${label} (a parent component) from a child component during the rendering process. This rarely worked in Ember 1.x and will be removed in Ember 2.0`, + false, + { id: 'ember-views.manual-parent-rerender', until: '3.0.0' }); + } else { + Ember.deprecate(`You modified ${label} twice in a single render. This was unreliable in Ember 1.x and will be removed in Ember 2.0`, + false, + { id: 'ember-views.render-double-modify', until: '3.0.0' }); + } + run.scheduleOnce('render', this, this.revalidate); + return; + } + + Ember.deprecate(`A property of ${this} was modified inside the ${this._dispatching} hook. You should never change properties on components, services or models during ${this._dispatching} because it causes significant performance degradation.`, + !this._dispatching, + { id: 'ember-views.dispatching-modify-property', until: '3.0.0' }); + + if (!this.scheduledRevalidation || this._dispatching) { + this.scheduledRevalidation = true; + run.scheduleOnce('render', this, this.revalidate); + } + }, + + templateRenderer: null, + + /** + Removes the view from its `parentView`, if one is found. Otherwise + does nothing. + + @method removeFromParent + @return {Ember.View} receiver + @private + */ + removeFromParent() { + var parent = this.parentView; + + // Remove DOM element from parent + this.remove(); + + if (parent) { parent.removeChild(this); } + return this; + }, + + /** + You must call `destroy` on a view to destroy the view (and all of its + child views). This will remove the view from any parent node, then make + sure that the DOM element managed by the view can be released by the + memory manager. + + @method destroy + @private + */ + destroy() { + // get parentView before calling super because it'll be destroyed + var parentView = this.parentView; + var viewName = this.viewName; + + if (!this._super(...arguments)) { return; } + + // remove from non-virtual parent view if viewName was specified + if (viewName && parentView) { + parentView.set(viewName, null); + } + + // Destroy HTMLbars template + if (this.lastResult) { + this.lastResult.destroy(); + } + + return this; + }, + + // ....................................................... + // EVENT HANDLING + // + + /** + Handle events from `Ember.EventDispatcher` + + @method handleEvent + @param eventName {String} + @param evt {Event} + @private + */ + handleEvent(eventName, evt) { + return this.currentState.handleEvent(this, eventName, evt); + }, + + /** + Registers the view in the view registry, keyed on the view's `elementId`. + This is used by the EventDispatcher to locate the view in response to + events. + + This method should only be called once the view has been inserted into the + DOM. + + @method _register + @private + */ + _register() { + Ember.assert('Attempted to register a view with an id already in use: ' + this.elementId, !this._viewRegistry[this.elementId]); + this._viewRegistry[this.elementId] = this; + }, + + /** + Removes the view from the view registry. This should be called when the + view is removed from DOM. + + @method _unregister + @private + */ + _unregister() { + delete this._viewRegistry[this.elementId]; + }, + + registerObserver(root, path, target, observer) { + if (!observer && 'function' === typeof target) { + observer = target; + target = null; + } + + if (!root || typeof root !== 'object') { + return; + } + + var scheduledObserver = this._wrapAsScheduled(observer); + + addObserver(root, path, target, scheduledObserver); + + this.one('willClearRender', function() { + removeObserver(root, path, target, scheduledObserver); + }); + }, + + _wrapAsScheduled(fn) { + var view = this; + var stateCheckedFn = function() { + view.currentState.invokeObserver(this, fn); + }; + var scheduledFn = function() { + run.scheduleOnce('render', this, stateCheckedFn); + }; + return scheduledFn; + } +}); diff --git a/packages/ember-views/lib/system/build-component-template.js b/packages/ember-views/lib/system/build-component-template.js index 6db4e3f6100..ddcbd217ab2 100644 --- a/packages/ember-views/lib/system/build-component-template.js +++ b/packages/ember-views/lib/system/build-component-template.js @@ -6,27 +6,24 @@ 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, isComponentElement, outerAttrs }, attrs, content) { - var blockToRender, tagName, meta; +export default function buildComponentTemplate({ component, tagName, layout, isAngleBracket, isComponentElement, outerAttrs }, attrs, content) { + var blockToRender, meta; if (component === undefined) { component = null; } 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, attributes); + blockToRender = createLayoutBlock(layout.raw, yieldTo, content.self, component, attrs); meta = layout.raw.meta; } else if (content.templates && content.templates.default) { - let attributes = (component && component._isAngleBracket) ? normalizeComponentAttributes(component, true, attrs) : undefined; - blockToRender = createContentBlock(content.templates.default, content.scope, content.self, component, attributes); + blockToRender = createContentBlock(content.templates.default, content.scope, content.self, component); meta = content.templates.default.meta; } if (component && !component._isAngleBracket || isComponentElement) { - tagName = tagNameFor(component); + tagName = 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 @@ -50,6 +47,30 @@ export default function buildComponentTemplate({ component, layout, isAngleBrack return { createdElement: !!tagName, block: blockToRender }; } +export function buildHTMLTemplate(tagName, _attrs, content) { + let attrs = {}; + + for (let prop in _attrs) { + let val = _attrs[prop]; + + if (typeof val === 'string') { + attrs[prop] = val; + } else { + attrs[prop] = ['value', val]; + } + } + + let childTemplate = content.templates.default; + let elementTemplate = internal.manualElement(tagName, attrs, childTemplate.isEmpty); + + if (childTemplate.isEmpty) { + return blockFor(elementTemplate, { scope: content.scope }); + } else { + let blockToRender = blockFor(content.templates.default, content); + return blockFor(elementTemplate, { yieldTo: blockToRender, scope: content.scope }); + } +} + function mergeAttrs(innerAttrs, outerAttrs) { let result = assign({}, innerAttrs, outerAttrs); @@ -65,13 +86,12 @@ function blockFor(template, options) { return internal.blockFor(render, template, options); } -function createContentBlock(template, scope, self, component, attributes) { +function createContentBlock(template, scope, self, component) { Ember.assert('BUG: buildComponentTemplate can take a scope or a self, but not both', !(scope && self)); return blockFor(template, { scope, self, - attributes, options: { view: component } }); } @@ -92,10 +112,9 @@ function createContentBlocks(templates, scope, self, component) { return output; } -function createLayoutBlock(template, yieldTo, self, component, attrs, attributes) { +function createLayoutBlock(template, yieldTo, self, component, attrs) { 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 diff --git a/packages/ember-views/lib/views/checkbox.js b/packages/ember-views/lib/views/checkbox.js index 5d7be7de7f2..abd8acf48fd 100644 --- a/packages/ember-views/lib/views/checkbox.js +++ b/packages/ember-views/lib/views/checkbox.js @@ -1,6 +1,6 @@ import { get } from 'ember-metal/property_get'; import { set } from 'ember-metal/property_set'; -import EmberComponent from 'ember-views/views/component'; +import EmberComponent from 'ember-views/components/component'; /** @module ember diff --git a/packages/ember-views/lib/views/text_area.js b/packages/ember-views/lib/views/text_area.js index dcaa07cf25f..233e2cdc8d6 100644 --- a/packages/ember-views/lib/views/text_area.js +++ b/packages/ember-views/lib/views/text_area.js @@ -2,7 +2,7 @@ @module ember @submodule ember-views */ -import Component from 'ember-views/views/component'; +import Component from 'ember-views/components/component'; import TextSupport from 'ember-views/mixins/text_support'; /** diff --git a/packages/ember-views/lib/views/text_field.js b/packages/ember-views/lib/views/text_field.js index 0fc539b6827..0fe36f6c1e4 100644 --- a/packages/ember-views/lib/views/text_field.js +++ b/packages/ember-views/lib/views/text_field.js @@ -4,7 +4,7 @@ */ import { computed } from 'ember-metal/computed'; import environment from 'ember-metal/environment'; -import Component from 'ember-views/views/component'; +import Component from 'ember-views/components/component'; import TextSupport from 'ember-views/mixins/text_support'; import EmptyObject from 'ember-metal/empty_object'; diff --git a/packages/ember-views/lib/views/view.js b/packages/ember-views/lib/views/view.js index 4ad0dbc32f6..6a8892b40e1 100644 --- a/packages/ember-views/lib/views/view.js +++ b/packages/ember-views/lib/views/view.js @@ -4,23 +4,12 @@ // Ember.ENV import Ember from 'ember-metal/core'; -import EmberError from 'ember-metal/error'; -import { get } from 'ember-metal/property_get'; -import run from 'ember-metal/run_loop'; -import { addObserver, removeObserver } from 'ember-metal/observer'; -import { guidFor } from 'ember-metal/utils'; -import { computed } from 'ember-metal/computed'; -import { - Mixin, - observer -} from 'ember-metal/mixin'; - -import jQuery from 'ember-views/system/jquery'; import 'ember-views/system/ext'; // for the side effect of extending Ember.run.queues import CoreView from 'ember-views/views/core_view'; import ViewContextSupport from 'ember-views/mixins/view_context_support'; import ViewChildViewsSupport from 'ember-views/mixins/view_child_views_support'; +import ViewLegacyChildViewsSupport from 'ember-views/mixins/legacy_child_views_support'; import { childViewsProperty } from 'ember-views/mixins/view_child_views_support'; @@ -32,8 +21,7 @@ import InstrumentationSupport from 'ember-views/mixins/instrumentation_support'; import AriaRoleSupport from 'ember-views/mixins/aria_role_support'; import VisibilitySupport from 'ember-views/mixins/visibility_support'; import CompatAttrsProxy from 'ember-views/compat/attrs-proxy'; - -function K() { return this; } +import ViewMixin from 'ember-views/mixins/view_support'; /** @module ember @@ -673,6 +661,7 @@ Ember.TEMPLATES = {}; var View = CoreView.extend( ViewContextSupport, ViewChildViewsSupport, + ViewLegacyChildViewsSupport, ViewStateSupport, TemplateRenderingSupport, ClassNamesSupport, @@ -680,800 +669,31 @@ var View = CoreView.extend( InstrumentationSupport, VisibilitySupport, CompatAttrsProxy, - AriaRoleSupport, { - concatenatedProperties: ['attributeBindings'], - - /** - @property isView - @type Boolean - @default true - @static - @private - */ - isView: true, - - // .......................................................... - // TEMPLATE SUPPORT - // - - /** - The name of the template to lookup if no template is provided. - - By default `Ember.View` will lookup a template with this name in - `Ember.TEMPLATES` (a shared global object). - - @property templateName - @type String - @default null - @private - */ - templateName: null, - - /** - The name of the layout to lookup if no layout is provided. - - By default `Ember.View` will lookup a template with this name in - `Ember.TEMPLATES` (a shared global object). - - @property layoutName - @type String - @default null - @public - */ - layoutName: null, - - /** - The template used to render the view. This should be a function that - accepts an optional context parameter and returns a string of HTML that - will be inserted into the DOM relative to its parent view. - - In general, you should set the `templateName` property instead of setting - the template yourself. - - @property template - @type Function - @private - */ - template: computed({ - get() { - var templateName = get(this, 'templateName'); - var template = this.templateForName(templateName, 'template'); - Ember.assert('You specified the templateName ' + templateName + ' for ' + this + ', but it did not exist.', !templateName || !!template); - return template || get(this, 'defaultTemplate'); - }, - set(key, value) { - if (value !== undefined) { return value; } - return get(this, key); - } - }), - - /** - A view may contain a layout. A layout is a regular template but - supersedes the `template` property during rendering. It is the - responsibility of the layout template to retrieve the `template` - property from the view (or alternatively, call `Handlebars.helpers.yield`, - `{{yield}}`) to render it in the correct location. - - This is useful for a view that has a shared wrapper, but which delegates - the rendering of the contents of the wrapper to the `template` property - on a subclass. - - @property layout - @type Function - @public - */ - layout: computed({ - get(key) { - var layoutName = get(this, 'layoutName'); - var layout = this.templateForName(layoutName, 'layout'); - - Ember.assert('You specified the layoutName ' + layoutName + ' for ' + this + ', but it did not exist.', !layoutName || !!layout); - - return layout || get(this, 'defaultLayout'); - }, - - set(key, value) { - return value; - } - }), - - templateForName(name, type) { - if (!name) { return; } - Ember.assert('templateNames are not allowed to contain periods: ' + name, name.indexOf('.') === -1); - - if (!this.container) { - throw new EmberError('Container was not found when looking up a views template. ' + - 'This is most likely due to manually instantiating an Ember.View. ' + - 'See: http://git.io/EKPpnA'); - } - - return this.container.lookup('template:' + name); - }, - - /** - If a value that affects template rendering changes, the view should be - re-rendered to reflect the new value. - - @method _contextDidChange - @private - @private - */ - _contextDidChange: observer('context', function() { - this.rerender(); - }), - - /** - Return the nearest ancestor that is an instance of the provided - class or mixin. - - @method nearestOfType - @param {Class,Mixin} klass Subclass of Ember.View (or Ember.View itself), - or an instance of Ember.Mixin. - @return Ember.View - @public - */ - nearestOfType(klass) { - var view = get(this, 'parentView'); - var isOfType = klass instanceof Mixin ? - function(view) { return klass.detect(view); } : - function(view) { return klass.detect(view.constructor); }; - - while (view) { - if (isOfType(view)) { return view; } - view = get(view, 'parentView'); - } - }, - - /** - Return the nearest ancestor that has a given property. - - @method nearestWithProperty - @param {String} property A property name - @return Ember.View - @private - */ - nearestWithProperty(property) { - var view = get(this, 'parentView'); - - while (view) { - if (property in view) { return view; } - view = get(view, 'parentView'); - } - }, - - /** - Renders the view again. This will work regardless of whether the - view is already in the DOM or not. If the view is in the DOM, the - rendering process will be deferred to give bindings a chance - to synchronize. - - If children were added during the rendering process using `appendChild`, - `rerender` will remove them, because they will be added again - if needed by the next `render`. - - In general, if the display of your view changes, you should modify - the DOM element directly instead of manually calling `rerender`, which can - be slow. - - @method rerender - @public - */ - rerender() { - return this.currentState.rerender(this); - }, - - /** - Given a property name, returns a dasherized version of that - property name if the property evaluates to a non-falsy value. - - For example, if the view has property `isUrgent` that evaluates to true, - passing `isUrgent` to this method will return `"is-urgent"`. - - @method _classStringForProperty - @param property - @private - */ - _classStringForProperty(parsedPath) { - return View._classStringForValue(parsedPath.path, parsedPath.stream.value(), parsedPath.className, parsedPath.falsyClassName); - }, - - // .......................................................... - // ELEMENT SUPPORT - // - - /** - Returns the current DOM element for the view. - - @property element - @type DOMElement - @public - */ - element: null, - - /** - Returns a jQuery object for this view's element. If you pass in a selector - string, this method will return a jQuery object, using the current element - as its buffer. - - For example, calling `view.$('li')` will return a jQuery object containing - all of the `li` elements inside the DOM element of this view. - - @method $ - @param {String} [selector] a jQuery-compatible selector string - @return {jQuery} the jQuery object for the DOM node - @public - */ - $(sel) { - Ember.assert('You cannot access this.$() on a component with `tagName: \'\'` specified.', this.tagName !== ''); - return this.currentState.$(this, sel); - }, - - forEachChildView(callback) { - var childViews = this.childViews; - - if (!childViews) { return this; } - - var len = childViews.length; - var view, idx; - - for (idx = 0; idx < len; idx++) { - view = childViews[idx]; - callback(view); - } - - return this; - }, - - /** - Appends the view's element to the specified parent element. - - If the view does not have an HTML representation yet, `createElement()` - will be called automatically. - - Note that this method just schedules the view to be appended; the DOM - element will not be appended to the given element until all bindings have - finished synchronizing. - - This is not typically a function that you will need to call directly when - building your application. You might consider using `Ember.ContainerView` - instead. If you do need to use `appendTo`, be sure that the target element - you are providing is associated with an `Ember.Application` and does not - have an ancestor element that is associated with an Ember view. - - @method appendTo - @param {String|DOMElement|jQuery} A selector, element, HTML string, or jQuery object - @return {Ember.View} receiver - @private - */ - appendTo(selector) { - var target = jQuery(selector); + AriaRoleSupport, + ViewMixin, { + init() { + this._super(...arguments); - Ember.assert('You tried to append to (' + selector + ') but that isn\'t in the DOM', target.length > 0); - Ember.assert('You cannot append to an existing Ember.View. Consider using Ember.ContainerView instead.', !target.is('.ember-view') && !target.parents().is('.ember-view')); - - this.renderer.appendTo(this, target[0]); - - return this; - }, - - /** - @private - - Creates a new DOM element, renders the view into it, then returns the - element. - - By default, the element created and rendered into will be a `BODY` element, - since this is the default context that views are rendered into when being - inserted directly into the DOM. - - ```js - var element = view.renderToElement(); - element.tagName; // => "BODY" - ``` - - You can override the kind of element rendered into and returned by - specifying an optional tag name as the first argument. - - ```js - var element = view.renderToElement('table'); - element.tagName; // => "TABLE" - ``` - - This method is useful if you want to render the view into an element that - is not in the document's body. Instead, a new `body` element, detached from - the DOM is returned. FastBoot uses this to serialize the rendered view into - a string for transmission over the network. - - ```js - app.visit('/').then(function(instance) { - var element; - Ember.run(function() { - element = renderToElement(instance); - }); - - res.send(serialize(element)); - }); - ``` - - @method renderToElement - @param {String} tagName The tag of the element to create and render into. Defaults to "body". - @return {HTMLBodyElement} element - @private - */ - renderToElement(tagName) { - tagName = tagName || 'body'; - - var element = this.renderer._dom.createElement(tagName); - - this.renderer.appendTo(this, element); - return element; - }, - - /** - Replaces the content of the specified parent element with this view's - element. If the view does not have an HTML representation yet, - the element will be generated automatically. - - Note that this method just schedules the view to be appended; the DOM - element will not be appended to the given element until all bindings have - finished synchronizing - - @method replaceIn - @param {String|DOMElement|jQuery} target A selector, element, HTML string, or jQuery object - @return {Ember.View} received - @private - */ - replaceIn(selector) { - var target = jQuery(selector); - - Ember.assert('You tried to replace in (' + selector + ') but that isn\'t in the DOM', target.length > 0); - Ember.assert('You cannot replace an existing Ember.View. Consider using Ember.ContainerView instead.', !target.is('.ember-view') && !target.parents().is('.ember-view')); - - this.renderer.replaceIn(this, target[0]); - - return this; - }, - - /** - Appends the view's element to the document body. If the view does - not have an HTML representation yet - the element will be generated automatically. - - If your application uses the `rootElement` property, you must append - the view within that element. Rendering views outside of the `rootElement` - is not supported. - - Note that this method just schedules the view to be appended; the DOM - element will not be appended to the document body until all bindings have - finished synchronizing. - - @method append - @return {Ember.View} receiver - @private - */ - append() { - return this.appendTo(document.body); - }, - - /** - Removes the view's element from the element to which it is attached. - - @method remove - @return {Ember.View} receiver - @private - */ - remove() { - // What we should really do here is wait until the end of the run loop - // to determine if the element has been re-appended to a different - // element. - // In the interim, we will just re-render if that happens. It is more - // important than elements get garbage collected. - if (!this.removedFromDOM) { this.destroyElement(); } - - // Set flag to avoid future renders - this._willInsert = false; - }, - - /** - The HTML `id` of the view's element in the DOM. You can provide this - value yourself but it must be unique (just as in HTML): - - ```handlebars - {{my-component elementId="a-really-cool-id"}} - ``` - - If not manually set a default value will be provided by the framework. - - Once rendered an element's `elementId` is considered immutable and you - should never change it. If you need to compute a dynamic value for the - `elementId`, you should do this when the component or element is being - instantiated: - - ```javascript - export default Ember.Component.extend({ - setElementId: Ember.on('init', function() { - var index = this.get('index'); - this.set('elementId', 'component-id' + index); - }) - }); - ``` - - @property elementId - @type String - @public - */ - elementId: null, - - /** - Attempts to discover the element in the parent element. The default - implementation looks for an element with an ID of `elementId` (or the - view's guid if `elementId` is null). You can override this method to - provide your own form of lookup. For example, if you want to discover your - element using a CSS class name instead of an ID. - - @method findElementInParentElement - @param {DOMElement} parentElement The parent's DOM element - @return {DOMElement} The discovered element - @private - */ - findElementInParentElement(parentElem) { - var id = '#' + this.elementId; - return jQuery(id)[0] || jQuery(id, parentElem)[0]; - }, - - /** - Creates a DOM representation of the view and all of its child views by - recursively calling the `render()` method. Once the element is created, - it sets the `element` property of the view to the rendered element. - - After the element has been inserted into the DOM, `didInsertElement` will - be called on this view and all of its child views. - - @method createElement - @return {Ember.View} receiver - @private - */ - createElement() { - if (this.element) { return this; } - - this.renderer.createElement(this); - - return this; - }, - - /** - Called when a view is going to insert an element into the DOM. - - @event willInsertElement - @public - */ - willInsertElement: K, - - /** - Called when the element of the view has been inserted into the DOM - or after the view was re-rendered. Override this function to do any - set up that requires an element in the document body. - - When a view has children, didInsertElement will be called on the - child view(s) first, bubbling upwards through the hierarchy. - - @event didInsertElement - @public - */ - didInsertElement: K, - - /** - Called when the view is about to rerender, but before anything has - been torn down. This is a good opportunity to tear down any manual - observers you have installed based on the DOM state - - @event willClearRender - @public - */ - willClearRender: K, - - /** - Destroys any existing element along with the element for any child views - as well. If the view does not currently have a element, then this method - will do nothing. - - If you implement `willDestroyElement()` on your view, then this method will - be invoked on your view before your element is destroyed to give you a - chance to clean up any event handlers, etc. - - If you write a `willDestroyElement()` handler, you can assume that your - `didInsertElement()` handler was called earlier for the same element. - - You should not call or override this method yourself, but you may - want to implement the above callbacks. - - @method destroyElement - @return {Ember.View} receiver - @private - */ - destroyElement() { - return this.currentState.destroyElement(this); - }, - - /** - Called when the element of the view is going to be destroyed. Override - this function to do any teardown that requires an element, like removing - event listeners. - - Please note: any property changes made during this event will have no - effect on object observers. - - @event willDestroyElement - @public - */ - willDestroyElement: K, - - /** - Called when the parentView property has changed. - - @event parentViewDidChange - @private - */ - parentViewDidChange: K, - - // .......................................................... - // STANDARD RENDER PROPERTIES - // - - /** - Tag name for the view's outer element. The tag name is only used when an - element is first created. If you change the `tagName` for an element, you - must destroy and recreate the view element. - - By default, the render buffer will use a `
` tag for views. - - @property tagName - @type String - @default null - @public - */ - - // We leave this null by default so we can tell the difference between - // the default case and a user-specified tag. - tagName: null, - - /* - Used to specify a default tagName that can be overridden when extending - or invoking from a template. - - @property _defaultTagName - @private - */ - - /** - Normally, Ember's component model is "write-only". The component takes a - bunch of attributes that it got passed in, and uses them to render its - template. - - One nice thing about this model is that if you try to set a value to the - same thing as last time, Ember (through HTMLBars) will avoid doing any - work on the DOM. - - This is not just a performance optimization. If an attribute has not - changed, it is important not to clobber the element's "hidden state". - For example, if you set an input's `value` to the same value as before, - it will clobber selection state and cursor position. In other words, - setting an attribute is not **always** idempotent. - - This method provides a way to read an element's attribute and also - update the last value Ember knows about at the same time. This makes - setting an attribute idempotent. - - In particular, what this means is that if you get an `` element's - `value` attribute and then re-render the template with the same value, - it will avoid clobbering the cursor and selection position. - - Since most attribute sets are idempotent in the browser, you typically - can get away with reading attributes using jQuery, but the most reliable - way to do so is through this method. - - @method readDOMAttr - @param {String} name the name of the attribute - @return String - @public - */ - readDOMAttr(name) { - let attr = this._renderNode.childNodes.filter(node => node.attrName === name)[0]; - if (!attr) { return null; } - return attr.getContent(); - }, - - // ....................................................... - // CORE DISPLAY METHODS - // - - /** - Setup a view, but do not finish waking it up. - - * configure `childViews` - * register the view with the global views hash, which is used for event - dispatch - - @method init - @private - */ - init() { - if (!this.elementId) { - this.elementId = guidFor(this); - } - - this.scheduledRevalidation = false; - - this._super(...arguments); - - if (!this._viewRegistry) { - this._viewRegistry = View.views; - } - - this.renderer.componentInitAttrs(this, this.attrs || {}); - - Ember.assert( - 'Using a custom `.render` function is no longer supported.', - !this.render - ); - }, - - __defineNonEnumerable(property) { - this[property.name] = property.descriptor.value; - }, - - revalidate() { - this.renderer.revalidateTopLevelView(this); - this.scheduledRevalidation = false; - }, - - scheduleRevalidate(node, label, manualRerender) { - if (node && !this._dispatching && node.guid in this.env.renderedNodes) { - if (manualRerender) { - Ember.deprecate(`You manually rerendered ${label} (a parent component) from a child component during the rendering process. This rarely worked in Ember 1.x and will be removed in Ember 2.0`, - false, - { id: 'ember-views.manual-parent-rerender', until: '3.0.0' }); - } else { - Ember.deprecate(`You modified ${label} twice in a single render. This was unreliable in Ember 1.x and will be removed in Ember 2.0`, - false, - { id: 'ember-views.render-double-modify', until: '3.0.0' }); + if (!this._viewRegistry) { + this._viewRegistry = View.views; } - run.scheduleOnce('render', this, this.revalidate); - return; - } - - Ember.deprecate(`A property of ${this} was modified inside the ${this._dispatching} hook. You should never change properties on components, services or models during ${this._dispatching} because it causes significant performance degradation.`, - !this._dispatching, - { id: 'ember-views.dispatching-modify-property', until: '3.0.0' }); - - if (!this.scheduledRevalidation || this._dispatching) { - this.scheduledRevalidation = true; - run.scheduleOnce('render', this, this.revalidate); - } - }, - - templateRenderer: null, - - /** - Removes the view from its `parentView`, if one is found. Otherwise - does nothing. - - @method removeFromParent - @return {Ember.View} receiver - @private - */ - removeFromParent() { - var parent = this.parentView; - - // Remove DOM element from parent - this.remove(); - - if (parent) { parent.removeChild(this); } - return this; - }, - - /** - You must call `destroy` on a view to destroy the view (and all of its - child views). This will remove the view from any parent node, then make - sure that the DOM element managed by the view can be released by the - memory manager. - - @method destroy - @private - */ - destroy() { - // get parentView before calling super because it'll be destroyed - var parentView = this.parentView; - var viewName = this.viewName; - - if (!this._super(...arguments)) { return; } - - // remove from non-virtual parent view if viewName was specified - if (viewName && parentView) { - parentView.set(viewName, null); - } - - // Destroy HTMLbars template - if (this.lastResult) { - this.lastResult.destroy(); - } - - return this; - }, - - // ....................................................... - // EVENT HANDLING - // - - /** - Handle events from `Ember.EventDispatcher` - - @method handleEvent - @param eventName {String} - @param evt {Event} - @private - */ - handleEvent(eventName, evt) { - return this.currentState.handleEvent(this, eventName, evt); - }, - - /** - Registers the view in the view registry, keyed on the view's `elementId`. - This is used by the EventDispatcher to locate the view in response to - events. - - This method should only be called once the view has been inserted into the - DOM. - - @method _register - @private - */ - _register() { - Ember.assert('Attempted to register a view with an id already in use: ' + this.elementId, !this._viewRegistry[this.elementId]); - this._viewRegistry[this.elementId] = this; - }, + }, - /** - Removes the view from the view registry. This should be called when the - view is removed from DOM. + /** + Given a property name, returns a dasherized version of that + property name if the property evaluates to a non-falsy value. - @method _unregister - @private - */ - _unregister() { - delete this._viewRegistry[this.elementId]; - }, - - registerObserver(root, path, target, observer) { - if (!observer && 'function' === typeof target) { - observer = target; - target = null; - } + For example, if the view has property `isUrgent` that evaluates to true, + passing `isUrgent` to this method will return `"is-urgent"`. - if (!root || typeof root !== 'object') { - return; + @method _classStringForProperty + @param property + @private + */ + _classStringForProperty(parsedPath) { + return View._classStringForValue(parsedPath.path, parsedPath.stream.value(), parsedPath.className, parsedPath.falsyClassName); } - - var scheduledObserver = this._wrapAsScheduled(observer); - - addObserver(root, path, target, scheduledObserver); - - this.one('willClearRender', function() { - removeObserver(root, path, target, scheduledObserver); - }); - }, - - _wrapAsScheduled(fn) { - var view = this; - var stateCheckedFn = function() { - view.currentState.invokeObserver(this, fn); - }; - var scheduledFn = function() { - run.scheduleOnce('render', this, stateCheckedFn); - }; - return scheduledFn; - } -}); + }); // jscs:enable validateIndentation /* diff --git a/packages/ember-views/tests/glimmer-components/render-test.js b/packages/ember-views/tests/glimmer-components/render-test.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/ember-views/tests/views/component_test.js b/packages/ember-views/tests/views/component_test.js index cbf35feb549..f45f4b1246d 100644 --- a/packages/ember-views/tests/views/component_test.js +++ b/packages/ember-views/tests/views/component_test.js @@ -7,7 +7,7 @@ import inject from 'ember-runtime/inject'; import { get } from 'ember-metal/property_get'; import EmberView from 'ember-views/views/view'; -import Component from 'ember-views/views/component'; +import Component from 'ember-views/components/component'; import { MUTABLE_CELL } from 'ember-views/compat/attrs-proxy'; diff --git a/packages/ember-views/tests/views/view/child_views_test.js b/packages/ember-views/tests/views/view/child_views_test.js index eab0f3be309..7d785a82866 100644 --- a/packages/ember-views/tests/views/view/child_views_test.js +++ b/packages/ember-views/tests/views/view/child_views_test.js @@ -1,7 +1,7 @@ import run from 'ember-metal/run_loop'; import Ember from 'ember-metal/core'; import EmberView from 'ember-views/views/view'; -import Component from 'ember-views/views/component'; +import Component from 'ember-views/components/component'; import { compile } from 'ember-template-compiler'; import { registerKeyword, resetKeyword } from 'ember-htmlbars/tests/utils';