From 19050f4a7f41aca587c7d762c5544a5832165c9a Mon Sep 17 00:00:00 2001 From: Greg Funtusov Date: Thu, 6 Aug 2015 00:12:00 +0300 Subject: [PATCH 01/28] Failing test for computed property alias in angle-bracket component --- .../tests/integration/component_invocation_test.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/ember-htmlbars/tests/integration/component_invocation_test.js b/packages/ember-htmlbars/tests/integration/component_invocation_test.js index 34006a6f2c3..924156fe7df 100644 --- a/packages/ember-htmlbars/tests/integration/component_invocation_test.js +++ b/packages/ember-htmlbars/tests/integration/component_invocation_test.js @@ -1264,6 +1264,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', Component.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}}')); From d315900ea5d7fc976b4b1b9901c6c7ff9f0fc46e Mon Sep 17 00:00:00 2001 From: Tom Dale Date: Thu, 9 Jul 2015 16:41:04 -0700 Subject: [PATCH 02/28] Remove unused view mixin --- .../tests/helpers/unbound_test.js | 2 +- .../mixins/normalized_rerender_if_needed.js | 40 ------------------- 2 files changed, 1 insertion(+), 41 deletions(-) delete mode 100644 packages/ember-views/lib/mixins/normalized_rerender_if_needed.js 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-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); - } -}); From f36f569cbe15d90ea9cb12b8f24cf230eed3d417 Mon Sep 17 00:00:00 2001 From: Tom Dale Date: Thu, 9 Jul 2015 17:30:27 -0700 Subject: [PATCH 03/28] Move Ember.Component to components/ directory --- .../system/dependency_injection/default_resolver_test.js | 2 +- packages/ember-htmlbars/lib/hooks/bind-shadow-scope.js | 2 +- .../lib/node-managers/component-node-manager.js | 2 +- .../ember-htmlbars/tests/compat/controller_keyword_test.js | 2 +- packages/ember-htmlbars/tests/compat/view_helper_test.js | 2 +- packages/ember-htmlbars/tests/compat/view_keyword_test.js | 2 +- packages/ember-htmlbars/tests/helpers/-html-safe-test.js | 2 +- packages/ember-htmlbars/tests/helpers/component_test.js | 2 +- packages/ember-htmlbars/tests/helpers/concat-test.js | 2 +- packages/ember-htmlbars/tests/helpers/custom_helper_test.js | 2 +- packages/ember-htmlbars/tests/helpers/each_in_test.js | 2 +- packages/ember-htmlbars/tests/helpers/view_test.js | 2 +- packages/ember-htmlbars/tests/helpers/yield_test.js | 2 +- .../ember-htmlbars/tests/integration/attrs_lookup_test.js | 2 +- .../ember-htmlbars/tests/integration/block_params_test.js | 2 +- .../tests/integration/component_element_id_test.js | 2 +- .../tests/integration/component_invocation_test.js | 2 +- .../tests/integration/component_lifecycle_test.js | 2 +- .../ember-htmlbars/tests/integration/helper-lookup-test.js | 2 +- .../ember-htmlbars/tests/integration/mutable_binding_test.js | 2 +- .../tests/integration/void-element-component-test.js | 2 +- .../tests/integration/will-destroy-element-hook-test.js | 2 +- .../ember-htmlbars/tests/system/append-templated-view-test.js | 2 +- packages/ember-htmlbars/tests/system/render_env_test.js | 2 +- .../tests/helpers/closure_action_test.js | 2 +- .../tests/helpers/element_action_test.js | 2 +- packages/ember-routing-views/lib/views/link.js | 2 +- packages/ember-views/lib/{views => components}/component.js | 0 packages/ember-views/lib/components/glimmer-component.js | 0 packages/ember-views/lib/main.js | 4 ++-- packages/ember-views/lib/views/checkbox.js | 2 +- packages/ember-views/lib/views/text_area.js | 2 +- packages/ember-views/lib/views/text_field.js | 2 +- packages/ember-views/tests/views/component_test.js | 2 +- packages/ember-views/tests/views/view/child_views_test.js | 2 +- 35 files changed, 34 insertions(+), 34 deletions(-) rename packages/ember-views/lib/{views => components}/component.js (100%) create mode 100644 packages/ember-views/lib/components/glimmer-component.js 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/hooks/bind-shadow-scope.js b/packages/ember-htmlbars/lib/hooks/bind-shadow-scope.js index f7a36741a29..f38cce6ea20 100644 --- a/packages/ember-htmlbars/lib/hooks/bind-shadow-scope.js +++ b/packages/ember-htmlbars/lib/hooks/bind-shadow-scope.js @@ -3,7 +3,7 @@ @submodule ember-htmlbars */ -import Component from 'ember-views/views/component'; +import Component from 'ember-views/components/component'; export default function bindShadowScope(env, parentScope, shadowScope, options) { if (!options) { return; } 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 d310293309c..d38c0d9958c 100644 --- a/packages/ember-htmlbars/lib/node-managers/component-node-manager.js +++ b/packages/ember-htmlbars/lib/node-managers/component-node-manager.js @@ -9,7 +9,7 @@ import setProperties from 'ember-metal/set_properties'; import { MUTABLE_CELL } from 'ember-views/compat/attrs-proxy'; import SafeString from 'htmlbars-util/safe-string'; import { instrument } from 'ember-htmlbars/system/instrumentation-support'; -import EmberComponent from 'ember-views/views/component'; +import EmberComponent from 'ember-views/components/component'; import Stream from 'ember-metal/streams/stream'; import { readArray } from 'ember-metal/streams/utils'; diff --git a/packages/ember-htmlbars/tests/compat/controller_keyword_test.js b/packages/ember-htmlbars/tests/compat/controller_keyword_test.js index ebac7a945e3..79c1f4ac3b0 100644 --- a/packages/ember-htmlbars/tests/compat/controller_keyword_test.js +++ b/packages/ember-htmlbars/tests/compat/controller_keyword_test.js @@ -1,4 +1,4 @@ -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 ee37731ab42..c56d38308e2 100644 --- a/packages/ember-htmlbars/tests/compat/view_helper_test.js +++ b/packages/ember-htmlbars/tests/compat/view_helper_test.js @@ -1,4 +1,4 @@ -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 8e745706043..c7b6c07bc51 100644 --- a/packages/ember-htmlbars/tests/compat/view_keyword_test.js +++ b/packages/ember-htmlbars/tests/compat/view_keyword_test.js @@ -1,4 +1,4 @@ -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/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 e326a4116c3..7e054e52ab4 100644 --- a/packages/ember-htmlbars/tests/helpers/component_test.js +++ b/packages/ember-htmlbars/tests/helpers/component_test.js @@ -7,7 +7,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 aa17209c93b..509205f9f10 100644 --- a/packages/ember-htmlbars/tests/helpers/each_in_test.js +++ b/packages/ember-htmlbars/tests/helpers/each_in_test.js @@ -1,5 +1,5 @@ import isEnabled from 'ember-metal/features'; -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/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/integration/attrs_lookup_test.js b/packages/ember-htmlbars/tests/integration/attrs_lookup_test.js index 2a0b2283c1e..28885a97854 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 34006a6f2c3..5faa292bd72 100644 --- a/packages/ember-htmlbars/tests/integration/component_invocation_test.js +++ b/packages/ember-htmlbars/tests/integration/component_invocation_test.js @@ -5,7 +5,7 @@ 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 { runAppend, runDestroy } from 'ember-runtime/tests/utils'; import { get } from 'ember-metal/property_get'; import { set } from 'ember-metal/property_set'; diff --git a/packages/ember-htmlbars/tests/integration/component_lifecycle_test.js b/packages/ember-htmlbars/tests/integration/component_lifecycle_test.js index f4c92b7b703..bd6c0e42a03 100644 --- a/packages/ember-htmlbars/tests/integration/component_lifecycle_test.js +++ b/packages/ember-htmlbars/tests/integration/component_lifecycle_test.js @@ -2,7 +2,7 @@ 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 { runAppend, runDestroy } from 'ember-runtime/tests/utils'; import run from 'ember-metal/run_loop'; import EmberView from 'ember-views/views/view'; diff --git a/packages/ember-htmlbars/tests/integration/helper-lookup-test.js b/packages/ember-htmlbars/tests/integration/helper-lookup-test.js index 05200f238e3..8e534a0a21e 100644 --- a/packages/ember-htmlbars/tests/integration/helper-lookup-test.js +++ b/packages/ember-htmlbars/tests/integration/helper-lookup-test.js @@ -2,7 +2,7 @@ import isEnabled from 'ember-metal/features'; 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..2763d359d65 100644 --- a/packages/ember-htmlbars/tests/integration/mutable_binding_test.js +++ b/packages/ember-htmlbars/tests/integration/mutable_binding_test.js @@ -4,7 +4,7 @@ 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 { runAppend, runDestroy } from 'ember-runtime/tests/utils'; import run from 'ember-metal/run_loop'; import { computed } from 'ember-metal/computed'; 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 188bcd11155..07fcba1d4ff 100644 --- a/packages/ember-routing-htmlbars/tests/helpers/closure_action_test.js +++ b/packages/ember-routing-htmlbars/tests/helpers/closure_action_test.js @@ -1,7 +1,7 @@ import isEnabled from 'ember-metal/features'; 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 7d4384626cd..1777045e438 100644 --- a/packages/ember-routing-htmlbars/tests/helpers/element_action_test.js +++ b/packages/ember-routing-htmlbars/tests/helpers/element_action_test.js @@ -10,7 +10,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 865c5bdd160..c94e601b43c 100644 --- a/packages/ember-routing-views/lib/views/link.js +++ b/packages/ember-routing-views/lib/views/link.js @@ -11,7 +11,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-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/components/glimmer-component.js b/packages/ember-views/lib/components/glimmer-component.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/ember-views/lib/main.js b/packages/ember-views/lib/main.js index 840111db980..2e9c0e2f777 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'; 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/tests/views/component_test.js b/packages/ember-views/tests/views/component_test.js index f7db50c8b02..8ede07cc830 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 500dde139cb..e0dc6cfd415 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'; From 9de956f44464af0ed9d89906ca3a2f3bf998fc02 Mon Sep 17 00:00:00 2001 From: Tom Dale Date: Mon, 20 Jul 2015 15:29:23 -0700 Subject: [PATCH 04/28] wip --- .../ember-htmlbars/lib/glimmer-component.js | 5 ++ .../tests/glimmer-component/render-test.js | 69 +++++++++++++++++++ .../tests/glimmer-component/test-helpers.js | 11 +++ .../glimmer-components/render-test.js} | 0 4 files changed, 85 insertions(+) create mode 100644 packages/ember-htmlbars/lib/glimmer-component.js create mode 100644 packages/ember-htmlbars/tests/glimmer-component/render-test.js create mode 100644 packages/ember-htmlbars/tests/glimmer-component/test-helpers.js rename packages/ember-views/{lib/components/glimmer-component.js => tests/glimmer-components/render-test.js} (100%) diff --git a/packages/ember-htmlbars/lib/glimmer-component.js b/packages/ember-htmlbars/lib/glimmer-component.js new file mode 100644 index 00000000000..fcade0a63fc --- /dev/null +++ b/packages/ember-htmlbars/lib/glimmer-component.js @@ -0,0 +1,5 @@ +import EmberObject from "ember-runtime/system/object"; + +export default EmberObject.extend({ + +}); 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..b259f436c78 --- /dev/null +++ b/packages/ember-htmlbars/tests/glimmer-component/render-test.js @@ -0,0 +1,69 @@ +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'; + +let view; + +QUnit.module("A basic glimmer component", { + teardown() { + runDestroy(view); + } +}); + +function renderComponent(tag, component) { + let { params, hash, 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:${tag}`, implementation); + + view = View.extend({ + container: registry.container(), + template: compile(`{{debugger}}<${tag} ${stringParams} ${stringHash}>`) + }).create(); + + runAppend(view); +} + +function hasSelector(assert, selector) { + assert.ok(document.querySelector(`#qunit-fixture ${selector}`), `${selector} exists`); +} + +QUnit.test("it renders", function(assert) { + let component; + + let MyComponent = GlimmerComponent.extend({ + init() { + component = this; + this._super(...arguments); + } + }); + + renderComponent('my-component', { + implementation: MyComponent + }); + + 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'); +}); + + +//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..2aa445ec9cc --- /dev/null +++ b/packages/ember-htmlbars/tests/glimmer-component/test-helpers.js @@ -0,0 +1,11 @@ +export function moduleForGlimmerComponent(name, options) { + let beforeEach = () => { + + }; + + let afterEach = () => { + + }; + + QUnit.module(`Glimmer Component - ${name}`, { beforeEach, afterEach }); +} diff --git a/packages/ember-views/lib/components/glimmer-component.js b/packages/ember-views/tests/glimmer-components/render-test.js similarity index 100% rename from packages/ember-views/lib/components/glimmer-component.js rename to packages/ember-views/tests/glimmer-components/render-test.js From a6856fdaf25afa4fbdf971a54084874eeece896b Mon Sep 17 00:00:00 2001 From: Yehuda Katz Date: Wed, 22 Jul 2015 03:04:24 +0000 Subject: [PATCH 05/28] Create GlimmerComponent subclass --- .../ember-htmlbars/lib/glimmer-component.js | 26 +- .../ember-htmlbars/lib/hooks/bind-self.js | 8 +- packages/ember-htmlbars/lib/main.js | 3 + .../tests/glimmer-component/render-test.js | 28 +- .../tests/glimmer-component/test-helpers.js | 10 +- .../integration/component_invocation_test.js | 13 +- .../integration/component_lifecycle_test.js | 563 ++++++------ packages/ember-views/lib/main.js | 2 +- .../lib/mixins/legacy_child_views_support.js | 20 + .../lib/mixins/legacy_view_support.js | 16 +- .../lib/mixins/view_child_views_support.js | 9 +- .../ember-views/lib/mixins/view_support.js | 770 ++++++++++++++++ packages/ember-views/lib/views/view.js | 819 +----------------- 13 files changed, 1189 insertions(+), 1098 deletions(-) create mode 100644 packages/ember-views/lib/mixins/legacy_child_views_support.js create mode 100644 packages/ember-views/lib/mixins/view_support.js diff --git a/packages/ember-htmlbars/lib/glimmer-component.js b/packages/ember-htmlbars/lib/glimmer-component.js index fcade0a63fc..02ff67bc15e 100644 --- a/packages/ember-htmlbars/lib/glimmer-component.js +++ b/packages/ember-htmlbars/lib/glimmer-component.js @@ -1,5 +1,25 @@ -import EmberObject from "ember-runtime/system/object"; +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 EmberObject.extend({ +export default CoreView.extend( + ViewChildViewsSupport, + ViewStateSupport, + TemplateRenderingSupport, + ClassNamesSupport, + InstrumentationSupport, + AriaRoleSupport, + ViewMixin, { + isGlimmerComponent: true, -}); + init() { + this._super(...arguments); + this._viewRegistry = this._viewRegistry || EmberView.views; + } + }); 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/main.js b/packages/ember-htmlbars/lib/main.js index 4a8ec4cfb58..95c874dead9 100644 --- a/packages/ember-htmlbars/lib/main.js +++ b/packages/ember-htmlbars/lib/main.js @@ -54,6 +54,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 @@ -91,6 +92,8 @@ Ember.HTMLBars = { DOMHelper }; +Ember.GlimmerComponent = GlimmerComponent; + if (isEnabled('ember-htmlbars-helper')) { Helper.helper = makeHelper; Ember.Helper = Helper; diff --git a/packages/ember-htmlbars/tests/glimmer-component/render-test.js b/packages/ember-htmlbars/tests/glimmer-component/render-test.js index b259f436c78..0a1d9fb6f99 100644 --- a/packages/ember-htmlbars/tests/glimmer-component/render-test.js +++ b/packages/ember-htmlbars/tests/glimmer-component/render-test.js @@ -1,32 +1,34 @@ -import Registry from "container/registry"; -import View from "ember-views/views/view"; -import GlimmerComponent from "ember-htmlbars/glimmer-component"; +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'; let view; -QUnit.module("A basic glimmer component", { +QUnit.module('A basic glimmer component', { teardown() { runDestroy(view); } }); function renderComponent(tag, component) { - let { params, hash, implementation } = component; + let { params, hash, yielded, implementation } = component; params = params || []; hash = hash || {}; - let stringParams = params.join(" "); + 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(`{{debugger}}<${tag} ${stringParams} ${stringHash}>`) + template: compile(`<${tag} ${stringParams} ${stringHash}>${yielded}`) }).create(); runAppend(view); @@ -36,28 +38,30 @@ function hasSelector(assert, selector) { assert.ok(document.querySelector(`#qunit-fixture ${selector}`), `${selector} exists`); } -QUnit.test("it renders", function(assert) { +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 + 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'); + hasSelector(assert, `my-component.ember-view[id=${component.elementId}]`); }); //testForComponent({ - //name: "my-component", + //name: 'my-component', //params: [], //hash: {}, //template: ` diff --git a/packages/ember-htmlbars/tests/glimmer-component/test-helpers.js b/packages/ember-htmlbars/tests/glimmer-component/test-helpers.js index 2aa445ec9cc..af369f682f0 100644 --- a/packages/ember-htmlbars/tests/glimmer-component/test-helpers.js +++ b/packages/ember-htmlbars/tests/glimmer-component/test-helpers.js @@ -1,11 +1,9 @@ export function moduleForGlimmerComponent(name, options) { - let beforeEach = () => { - - }; + function beforeEach() { + } - let afterEach = () => { - - }; + function afterEach() { + } QUnit.module(`Glimmer Component - ${name}`, { beforeEach, afterEach }); } diff --git a/packages/ember-htmlbars/tests/integration/component_invocation_test.js b/packages/ember-htmlbars/tests/integration/component_invocation_test.js index 5faa292bd72..cbf852ba626 100644 --- a/packages/ember-htmlbars/tests/integration/component_invocation_test.js +++ b/packages/ember-htmlbars/tests/integration/component_invocation_test.js @@ -6,6 +6,7 @@ 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/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'; @@ -1131,11 +1132,11 @@ if (isEnabled('ember-htmlbars-component-generation')) { //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('template:components/non-block', compile('In layout - {{attrs.text}}')); + registry.register('component:non-block', GlimmerComponent.extend({ text: null, dynamic: null, @@ -1166,8 +1167,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(''); @@ -1179,7 +1180,7 @@ if (isEnabled('ember-htmlbars-component-generation')) { var willUpdate = 0; var didReceiveAttrs = 0; - registry.register('component:non-block', Component.extend({ + registry.register('component:non-block', GlimmerComponent.extend({ didReceiveAttrs() { didReceiveAttrs++; }, diff --git a/packages/ember-htmlbars/tests/integration/component_lifecycle_test.js b/packages/ember-htmlbars/tests/integration/component_lifecycle_test.js index bd6c0e42a03..330980b2749 100644 --- a/packages/ember-htmlbars/tests/integration/component_lifecycle_test.js +++ b/packages/ember-htmlbars/tests/integration/component_lifecycle_test.js @@ -3,6 +3,7 @@ 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/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'; @@ -10,337 +11,373 @@ import EmberView from 'ember-views/views/view'; 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); - - hooks = []; - }, - - teardown() { - runDestroy(container); - runDestroy(view); - registry = container = view = null; +let styles = [{ + name: 'curly', + class: Component +}, { + name: 'angle', + class: GlimmerComponent +}]; + +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} />`; + } } -}); - -function pushHook(view, type, arg) { - hooks.push(hook(view, type, arg)); -} - -function hook(view, type, arg) { - return { type: type, view: view, arg: arg }; -} - -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); - }, - - 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')); - - 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(); - runAppend(view); + // Because the `twitter` attr is used by the all of the components, + // the lifecycle hooks are invoked for all components. - ok(component, 'The component was inserted'); - equal(jQuery('#qunit-fixture').text(), 'Top: Middle: Bottom: @tomdale'); - let topAttrs = { twitter: '@tomdale' }; - let middleAttrs = { twitterTop: '@tomdale' }; - let bottomAttrs = { twitterMiddle: '@tomdale' }; + topAttrs = { oldAttrs: { twitter: '@tomdale' }, newAttrs: { twitter: '@hipstertomdale' } }; + middleAttrs = { oldAttrs: { twitterTop: '@tomdale' }, newAttrs: { twitterTop: '@hipstertomdale' } }; + bottomAttrs = { oldAttrs: { twitterMiddle: '@tomdale' }, newAttrs: { twitterMiddle: '@hipstertomdale' } }; - 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', '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'), - run(function() { - view.set('twitter', '@hipstertomdale'); - }); - - // Because the `twitter` attr is used by the all of the components, - // the lifecycle hooks are invoked for all components. + 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') + ]); - 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-views/lib/main.js b/packages/ember-views/lib/main.js index 2e9c0e2f777..30b2e900e47 100644 --- a/packages/ember-views/lib/main.js +++ b/packages/ember-views/lib/main.js @@ -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/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..d7a6ec293a1 --- /dev/null +++ b/packages/ember-views/lib/mixins/view_support.js @@ -0,0 +1,770 @@ +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('templateName', { + 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('layoutName', { + 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 || {}); + }, + + __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/views/view.js b/packages/ember-views/lib/views/view.js index b54b8572ada..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,795 +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('templateName', { - 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('layoutName', { - 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 - @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); - }, - - /** - 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); - - 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; + AriaRoleSupport, + ViewMixin, { + init() { + this._super(...arguments); - this._super(...arguments); - - if (!this._viewRegistry) { - this._viewRegistry = View.views; - } - - this.renderer.componentInitAttrs(this, this.attrs || {}); - }, - - __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 /* From fb9477dc7fab6d38553d4774eacb993e56545531 Mon Sep 17 00:00:00 2001 From: Godhuda Date: Mon, 3 Aug 2015 17:50:39 -0700 Subject: [PATCH 06/28] Fix tests 1. Make sure tests that test Glimmer Components are feature flagged. 2. Update a test to use `init` instead of `didInitAttr` due to a change to the lifecycle that landed on master. --- .../tests/glimmer-component/render-test.js | 55 ++++++++++--------- .../integration/component_invocation_test.js | 7 ++- .../integration/component_lifecycle_test.js | 11 +++- 3 files changed, 42 insertions(+), 31 deletions(-) diff --git a/packages/ember-htmlbars/tests/glimmer-component/render-test.js b/packages/ember-htmlbars/tests/glimmer-component/render-test.js index 0a1d9fb6f99..048007927a0 100644 --- a/packages/ember-htmlbars/tests/glimmer-component/render-test.js +++ b/packages/ember-htmlbars/tests/glimmer-component/render-test.js @@ -4,14 +4,38 @@ 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; -QUnit.module('A basic glimmer component', { - teardown() { - runDestroy(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; @@ -38,27 +62,6 @@ function hasSelector(assert, selector) { assert.ok(document.querySelector(`#qunit-fixture ${selector}`), `${selector} exists`); } -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}]`); -}); - //testForComponent({ //name: 'my-component', diff --git a/packages/ember-htmlbars/tests/integration/component_invocation_test.js b/packages/ember-htmlbars/tests/integration/component_invocation_test.js index cbf852ba626..62d68dc1d06 100644 --- a/packages/ember-htmlbars/tests/integration/component_invocation_test.js +++ b/packages/ember-htmlbars/tests/integration/component_invocation_test.js @@ -1135,12 +1135,15 @@ if (isEnabled('ember-htmlbars-component-generation')) { QUnit.test('attributes are not installed on the top level', function() { let component; - registry.register('template:components/non-block', compile('In layout - {{attrs.text}}')); + registry.register('template:components/non-block', compile('In layout - {{attrs.text}} -- {{text}}')); 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; } })); diff --git a/packages/ember-htmlbars/tests/integration/component_lifecycle_test.js b/packages/ember-htmlbars/tests/integration/component_lifecycle_test.js index 330980b2749..019349e3dfb 100644 --- a/packages/ember-htmlbars/tests/integration/component_lifecycle_test.js +++ b/packages/ember-htmlbars/tests/integration/component_lifecycle_test.js @@ -7,6 +7,7 @@ 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; @@ -14,11 +15,15 @@ var hooks; let styles = [{ name: 'curly', class: Component -}, { - name: 'angle', - class: GlimmerComponent }]; +if (isEnabled('ember-htmlbars-component-generation')) { + styles.push({ + name: 'angle', + class: GlimmerComponent + }); +} + styles.forEach(style => { function invoke(name, hash) { if (style.name === 'curly') { From 8dc7d58cd4c0f45cc5aaf79c3ccf03d10dd7445f Mon Sep 17 00:00:00 2001 From: Godhuda Date: Mon, 3 Aug 2015 17:55:17 -0700 Subject: [PATCH 07/28] Fix attr setting in Glimmer Components Previously, Glimmer components got their attrs set after positional attrs were applied. The `didInitAttrs` hook was used to indicate that the full set of attributes were applied. Recently, positional attrs were changed to ensure that the full set of attrs are always applied before `init`, eliminating the need for `didInitAttrs`. However, the existing code that applied attrs on init only did so for non-angle bracket components (to avoid double-setting on new-world features). Consequently, angle bracket components *never* got their attrs set. This commit ensures that `attrs` are set on all components before init, regardless of style. --- .../lib/node-managers/component-node-manager.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 d38c0d9958c..60adcb5d7c6 100644 --- a/packages/ember-htmlbars/lib/node-managers/component-node-manager.js +++ b/packages/ember-htmlbars/lib/node-managers/component-node-manager.js @@ -301,13 +301,13 @@ ComponentNodeManager.prototype.destroy = function() { export function createComponent(_component, isAngleBracket, _props, renderNode, env, attrs = {}, proto = _component.proto()) { let props = assign({}, _props); + let snapshot = takeSnapshot(attrs); + props.attrs = snapshot; + if (!isAngleBracket) { let hasSuppliedController = 'controller' in attrs; // 2.0TODO remove Ember.deprecate('controller= is deprecated', !hasSuppliedController, { id: 'ember-htmlbars.create-component', until: '3.0.0' }); - let snapshot = takeSnapshot(attrs); - props.attrs = snapshot; - mergeBindings(props, shadowedAttrs(proto, snapshot)); } else { props._isAngleBracket = true; From 912392ea5d7d5bb607c0914c40b5810dbef3d4bd Mon Sep 17 00:00:00 2001 From: Godhuda Date: Tue, 4 Aug 2015 15:30:43 -0700 Subject: [PATCH 08/28] Show error for component class/invocation mismatch 1. {{my-component ...}} curly braces can only be used on legacy Component 2. angle brackets can only be used on GlimmerComponent TBD: rule number 2 might be loosened with an explicit opt-in later, depending on how painful this transition turns out to be for large apps. It might be better to provide automated rewriting through ember-watson, which would do the job quickly, once and for all, rather than leaving large apps in a semantic limbo with one foot in the old world and another foot in the new world. --- .../node-managers/component-node-manager.js | 13 ++++++++++-- .../integration/component_invocation_test.js | 21 ++++++++++++++++++- 2 files changed, 31 insertions(+), 3 deletions(-) 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 60adcb5d7c6..1342a80961e 100644 --- a/packages/ember-htmlbars/lib/node-managers/component-node-manager.js +++ b/packages/ember-htmlbars/lib/node-managers/component-node-manager.js @@ -9,7 +9,8 @@ import setProperties from 'ember-metal/set_properties'; import { MUTABLE_CELL } from 'ember-views/compat/attrs-proxy'; import SafeString from 'htmlbars-util/safe-string'; import { instrument } from 'ember-htmlbars/system/instrumentation-support'; -import EmberComponent from 'ember-views/components/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'; @@ -50,7 +51,7 @@ ComponentNodeManager.create = function(renderNode, env, options) { return component || layout; }); - component = component || EmberComponent; + component = component || (isAngleBracket ? GlimmerComponent : LegacyEmberComponent); let createOptions = { parentView }; @@ -80,6 +81,14 @@ ComponentNodeManager.create = function(renderNode, env, options) { // Instantiate the component component = createComponent(component, isAngleBracket, createOptions, renderNode, env, attrs, proto); + Ember.runInDebug(() => { + if (isAngleBracket) { + Ember.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 { + Ember.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 the component specifies its template via the `layout` or `template` // properties instead of using the template looked up in the container, get // them now that we have the component instance. diff --git a/packages/ember-htmlbars/tests/integration/component_invocation_test.js b/packages/ember-htmlbars/tests/integration/component_invocation_test.js index 62d68dc1d06..73ebf15a27b 100644 --- a/packages/ember-htmlbars/tests/integration/component_invocation_test.js +++ b/packages/ember-htmlbars/tests/integration/component_invocation_test.js @@ -65,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); @@ -1017,6 +1026,15 @@ if (isEnabled('ember-htmlbars-component-generation')) { equal(view.$().html(), 'In layout', 'Just the fragment was used'); }); + 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()); + + expectAssertion(function() { + view = appendViewFor(''); + }, /cannot invoke the 'non-block' component with angle brackets/); + }); + QUnit.test('non-block without properties replaced with a fragment when the content is multiple elements', function() { registry.register('template:components/non-block', compile('
This is a
fragment
')); @@ -1175,6 +1193,7 @@ if (isEnabled('ember-htmlbars-component-generation')) { registry.register('template:components/non-block', compile('In layout - someProp: {{attrs.someProp}}')); view = appendViewFor(''); + console.log(jQuery('#qunit-fixture').html()); equal(jQuery('#qunit-fixture').text(), 'In layout - someProp: something here'); }); @@ -1234,7 +1253,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); } From 6b002cafaaa6cae7d792814065eff7adb3fb6fc4 Mon Sep 17 00:00:00 2001 From: Godhuda Date: Tue, 4 Aug 2015 15:44:40 -0700 Subject: [PATCH 09/28] Re-enable skipped tests for GlimmerComponents They were disabled due to a bug in the feature flag infrastructure; but it has since been fixed and everything appears to be working again here. --- .../tests/integration/component_invocation_test.js | 13 ++++++------- .../tests/integration/mutable_binding_test.js | 13 +++++++------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/ember-htmlbars/tests/integration/component_invocation_test.js b/packages/ember-htmlbars/tests/integration/component_invocation_test.js index 73ebf15a27b..571c243a132 100644 --- a/packages/ember-htmlbars/tests/integration/component_invocation_test.js +++ b/packages/ember-htmlbars/tests/integration/component_invocation_test.js @@ -1043,7 +1043,7 @@ if (isEnabled('ember-htmlbars-component-generation')) { equal(view.$().html(), '
This is a
fragment
', 'Just the fragment was used'); }); - QUnit.skip('non-block without properties replaced with a div', function() { + QUnit.test('non-block without properties replaced with a div', function() { // The whitespace is added intentionally to verify that the heuristic is not "a single node" but // rather "a single non-whitespace, non-comment node" registry.register('template:components/non-block', compile('
In layout
')); @@ -1061,7 +1061,7 @@ if (isEnabled('ember-htmlbars-component-generation')) { ok(view.$('div.ember-view[id]').length === 1, 'The non-block tag name was used'); }); - QUnit.skip('non-block without properties replaced with identity element', function() { + QUnit.test('non-block without properties replaced with identity element', function() { registry.register('template:components/non-block', compile('In layout')); view = appendViewFor('', { @@ -1080,7 +1080,7 @@ if (isEnabled('ember-htmlbars-component-generation')) { ok(view.$().html().match(/^In layout<\/non-block>$/), 'The root element has gotten the default class and ids'); }); - QUnit.skip('non-block with class replaced with a div merges classes', function() { + QUnit.test('non-block with class replaced with a div merges classes', function() { registry.register('template:components/non-block', compile('
')); view = appendViewFor('', { @@ -1094,7 +1094,7 @@ if (isEnabled('ember-htmlbars-component-generation')) { equal(view.$('div').attr('class'), 'inner-class new-outer ember-view', 'the classes are merged'); }); - QUnit.skip('non-block with class replaced with a identity element merges classes', function() { + QUnit.test('non-block with class replaced with a identity element merges classes', function() { registry.register('template:components/non-block', compile('')); view = appendViewFor('', { @@ -1108,7 +1108,7 @@ if (isEnabled('ember-htmlbars-component-generation')) { equal(view.$('non-block').attr('class'), 'inner-class new-outer ember-view', 'the classes are merged'); }); - QUnit.skip('non-block rendering a fragment', function() { + QUnit.test('non-block rendering a fragment', function() { registry.register('template:components/non-block', compile('

{{attrs.first}}

{{attrs.second}}

')); view = appendViewFor('', { @@ -1193,12 +1193,11 @@ if (isEnabled('ember-htmlbars-component-generation')) { registry.register('template:components/non-block', compile('In layout - someProp: {{attrs.someProp}}')); view = appendViewFor(''); - console.log(jQuery('#qunit-fixture').html()); 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; diff --git a/packages/ember-htmlbars/tests/integration/mutable_binding_test.js b/packages/ember-htmlbars/tests/integration/mutable_binding_test.js index 2763d359d65..3f770d1aef7 100644 --- a/packages/ember-htmlbars/tests/integration/mutable_binding_test.js +++ b/packages/ember-htmlbars/tests/integration/mutable_binding_test.js @@ -5,6 +5,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/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() { From 361990401342186150f43004eb35f2a83dfc3789 Mon Sep 17 00:00:00 2001 From: Godhuda Date: Wed, 5 Aug 2015 17:16:26 -0700 Subject: [PATCH 10/28] Remove incorrect attributes This code was previously telling HTMLBars to post-process content templates to add top-level attributes. That is not correct (it only makes sense to post-process layouts). --- packages/ember-views/lib/system/build-component-template.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/ember-views/lib/system/build-component-template.js b/packages/ember-views/lib/system/build-component-template.js index 6db4e3f6100..a1eaa0dbde8 100644 --- a/packages/ember-views/lib/system/build-component-template.js +++ b/packages/ember-views/lib/system/build-component-template.js @@ -20,8 +20,7 @@ export default function buildComponentTemplate({ component, layout, isAngleBrack blockToRender = createLayoutBlock(layout.raw, yieldTo, content.self, component, attrs, attributes); 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; } From a3b88ec526edf92992ed2fb17c13c046385b55ab Mon Sep 17 00:00:00 2001 From: Godhuda Date: Wed, 5 Aug 2015 17:54:39 -0700 Subject: [PATCH 11/28] This commit fixes a top-level element bug If the layout for `` has `` as its root element, this is a special case. The semantics of this case are intended to be the same as a top-level `
`. However, in an attempt to share code, we inadvertantly introduced a bug where we installed the dynamic attributes defined by JS APIs twice (via `normalizeComponentAttributes` and post-processing of the parent template). The fix, therefore, is to not do anything special with dynamic attributes and rely on the same post-processing step as the `
` case. --- .../ember-htmlbars/lib/hooks/component.js | 30 +++++++-------- .../integration/component_invocation_test.js | 21 +++++++++- .../lib/system/build-component-template.js | 38 ++++++++++++------- 3 files changed, 60 insertions(+), 29 deletions(-) diff --git a/packages/ember-htmlbars/lib/hooks/component.js b/packages/ember-htmlbars/lib/hooks/component.js index 2a9a54ad42f..57426721171 100644 --- a/packages/ember-htmlbars/lib/hooks/component.js +++ b/packages/ember-htmlbars/lib/hooks/component.js @@ -24,21 +24,7 @@ export default function componentHook(renderNode, env, scope, _tagName, params, var parentView = env.view; - if (!isTopLevel || tagName !== env.view.tagName) { - var manager = ComponentNodeManager.create(renderNode, env, { - tagName, - params, - attrs, - parentView, - templates, - isAngleBracket, - isTopLevel, - parentScope: scope - }); - - state.manager = manager; - manager.render(env, visitor); - } else { + if (isTopLevel && tagName === env.view.tagName) { let component = env.view; let templateOptions = { component, @@ -52,5 +38,19 @@ export default function componentHook(renderNode, env, scope, _tagName, params, let { block } = buildComponentTemplate(templateOptions, attrs, contentOptions); block(env, [], undefined, renderNode, scope, visitor); + } else { + var manager = ComponentNodeManager.create(renderNode, env, { + tagName, + params, + attrs, + parentView, + templates, + isAngleBracket, + isTopLevel, + parentScope: scope + }); + + state.manager = manager; + manager.render(env, visitor); } } diff --git a/packages/ember-htmlbars/tests/integration/component_invocation_test.js b/packages/ember-htmlbars/tests/integration/component_invocation_test.js index 571c243a132..8f79203f6ab 100644 --- a/packages/ember-htmlbars/tests/integration/component_invocation_test.js +++ b/packages/ember-htmlbars/tests/integration/component_invocation_test.js @@ -1070,7 +1070,26 @@ if (isEnabled('ember-htmlbars-component-generation')) { 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.$().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'); + + run(() => view.set('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'); + }); + + QUnit.skip('non-block without properties replaced with identity element (regression if identity element has a single child element)', function() { + registry.register('template:components/non-block', compile('

In layout

')); + + view = appendViewFor('', { + stability: 'stability' + }); + + let node = view.$()[0]; + equal(view.$().text(), 'In layout'); + ok(view.$().html().match(/^

In layout<\/p><\/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'); run(() => view.set('stability', 'stability!')); diff --git a/packages/ember-views/lib/system/build-component-template.js b/packages/ember-views/lib/system/build-component-template.js index a1eaa0dbde8..ccb6e4ed0f3 100644 --- a/packages/ember-views/lib/system/build-component-template.js +++ b/packages/ember-views/lib/system/build-component-template.js @@ -1,6 +1,5 @@ import Ember from 'ember-metal/core'; import { get } from 'ember-metal/property_get'; -import assign from 'ember-metal/assign'; import { isGlobal } from 'ember-metal/path_cache'; import { internal, render } from 'htmlbars-runtime'; import getValue from 'ember-htmlbars/hooks/get-value'; @@ -31,8 +30,14 @@ export default function buildComponentTemplate({ component, layout, isAngleBrack // element. We use `manualElement` to create a template that represents // the wrapping element and yields to the previous block. if (tagName !== '') { - if (isComponentElement) { attrs = mergeAttrs(attrs, outerAttrs); } - var attributes = normalizeComponentAttributes(component, isAngleBracket, attrs); + let attributes; + + if (isComponentElement) { + attributes = convertAttrsToAst(attrs); + } else { + attributes = normalizeComponentAttributes(component, isAngleBracket, attrs); + } + var elementTemplate = internal.manualElement(tagName, attributes); elementTemplate.meta = meta; @@ -49,16 +54,6 @@ export default function buildComponentTemplate({ component, layout, isAngleBrack return { createdElement: !!tagName, block: blockToRender }; } -function mergeAttrs(innerAttrs, outerAttrs) { - let result = assign({}, innerAttrs, outerAttrs); - - if (innerAttrs.class && outerAttrs.class) { - result.class = ['subexpr', '-join-classes', [['value', innerAttrs.class], ['value', outerAttrs.class]], []]; - } - - return result; -} - function blockFor(template, options) { Ember.assert('BUG: Must pass a template to blockFor', !!template); return internal.blockFor(render, template, options); @@ -131,6 +126,23 @@ function tagNameFor(view) { return tagName; } +function convertAttrsToAst(attrs) { + let normalized = {}; + + for (var prop in attrs) { + let val = attrs[prop]; + if (!val) { continue; } + + if (typeof val === 'string') { + normalized[prop] = val; + } else if (val.isConcat) { + normalized[prop] = ['value', val]; + } + } + + return normalized; +} + // Takes a component and builds a normalized set of attribute // bindings consumable by HTMLBars' `attribute` hook. function normalizeComponentAttributes(component, isAngleBracket, attrs) { From a668299bc8dcc922617718d9dc4b63152413cef2 Mon Sep 17 00:00:00 2001 From: Godhuda Date: Fri, 7 Aug 2015 10:11:53 -0700 Subject: [PATCH 12/28] Add some new (failing) tests for component bugs --- .../integration/component_invocation_test.js | 132 +++++++++++++++++- 1 file changed, 130 insertions(+), 2 deletions(-) diff --git a/packages/ember-htmlbars/tests/integration/component_invocation_test.js b/packages/ember-htmlbars/tests/integration/component_invocation_test.js index 8f79203f6ab..21e9516400b 100644 --- a/packages/ember-htmlbars/tests/integration/component_invocation_test.js +++ b/packages/ember-htmlbars/tests/integration/component_invocation_test.js @@ -1113,7 +1113,7 @@ if (isEnabled('ember-htmlbars-component-generation')) { equal(view.$('div').attr('class'), 'inner-class new-outer ember-view', 'the classes are merged'); }); - QUnit.test('non-block with class replaced with a identity element merges classes', function() { + QUnit.test('non-block with class replaced with identity element merges classes', function() { registry.register('template:components/non-block', compile('')); view = appendViewFor('', { @@ -1127,6 +1127,134 @@ if (isEnabled('ember-htmlbars-component-generation')) { equal(view.$('non-block').attr('class'), 'inner-class new-outer ember-view', 'the classes are merged'); }); + QUnit.test('non-block with outer attributes replaced with a div shadows inner attributes', function() { + registry.register('template:components/non-block', compile('

')); + + view = appendViewFor(''); + + equal(view.$('div').attr('data-static'), 'outer', 'the outer attribute wins'); + equal(view.$('div').attr('data-dynamic'), 'outer', 'the outer attribute wins'); + + let component = view.childViews[0]; // HAX + + run(() => component.set('internal', 'changed')); + + equal(view.$('div').attr('data-static'), 'outer', 'the outer attribute wins'); + equal(view.$('div').attr('data-dynamic'), 'outer', 'the outer attribute wins'); + }); + + QUnit.test('non-block with outer attributes replaced with identity element shadows inner attributes', function() { + registry.register('template:components/non-block', compile('')); + + view = appendViewFor(''); + + equal(view.$('non-block').attr('data-static'), 'outer', 'the outer attribute wins'); + equal(view.$('non-block').attr('data-dynamic'), 'outer', 'the outer attribute wins'); + + let component = view.childViews[0]; // HAX + + run(() => component.set('internal', 'changed')); + + equal(view.$('non-block').attr('data-static'), 'outer', 'the outer attribute wins'); + equal(view.$('non-block').attr('data-dynamic'), 'outer', 'the outer attribute wins'); + }); + + QUnit.test('non-block recurrsive 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 recurrsive invocations with outer attributes replaced with identity element 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 a div should have correct scope', function() { + registry.register('template:components/non-block', compile('
{{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 identity element should have correct scope', function() { + registry.register('template:components/non-block', compile('{{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 a div should have inner attributes', function() { + registry.register('template:components/non-block', compile('
')); + + registry.register('component:non-block', GlimmerComponent.extend({ + init() { + this._super(...arguments); + this.set('internal', 'stuff'); + } + })); + + view = appendViewFor(''); + + equal(view.$('div').attr('data-static'), 'static'); + equal(view.$('div').attr('data-dynamic'), 'stuff'); + }); + + QUnit.test('non-block replaced with identity element should have inner attributes', function() { + registry.register('template:components/non-block', compile('')); + + registry.register('component:non-block', GlimmerComponent.extend({ + init() { + this._super(...arguments); + this.set('internal', 'stuff'); + } + })); + + view = appendViewFor(''); + + equal(view.$('non-block').attr('data-static'), 'static'); + equal(view.$('non-block').attr('data-dynamic'), 'stuff'); + }); + QUnit.test('non-block rendering a fragment', function() { registry.register('template:components/non-block', compile('

{{attrs.first}}

{{attrs.second}}

')); @@ -1153,7 +1281,7 @@ 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() { + QUnit.test('non-block with properties on attrs', function() { registry.register('template:components/non-block', compile('In layout')); view = appendViewFor('', { From d280c5353c3031272a3fda59e619e1d8f6fc6e02 Mon Sep 17 00:00:00 2001 From: Godhuda Date: Fri, 7 Aug 2015 15:06:13 -0700 Subject: [PATCH 13/28] Remove post-processing ("attributes") hook This was previously used for attaching outer attributes as well as system attributes (e.g. class="ember-view" id="ember123") onto regular HTML elements at the root, once they have been rendered. This results in multiple AttrNodes for a single attribute, which means that there is no guarantee that any particular AttrNode will actually "win". This incorrect strategy "works" a surprising amount of the time, but the failure modes are unacceptable. (See new tests introduced in the previous commit.) This is in anticipation of making all top-level elements in a component's layout dynamic, so that they can share attribute merging logic with the "identity element" case (top-level `` in the layout for `my-component`). --- packages/ember-htmlbars/lib/env.js | 4 +- .../ember-htmlbars/lib/hooks/attributes.js | 50 ------------------- .../lib/system/build-component-template.js | 48 ++++++------------ 3 files changed, 17 insertions(+), 85 deletions(-) delete mode 100644 packages/ember-htmlbars/lib/hooks/attributes.js diff --git a/packages/ember-htmlbars/lib/env.js b/packages/ember-htmlbars/lib/env.js index 938599701ec..6a74eb8319c 100644 --- a/packages/ember-htmlbars/lib/env.js +++ b/packages/ember-htmlbars/lib/env.js @@ -29,7 +29,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'; @@ -63,8 +62,7 @@ merge(emberHooks, { lookupHelper, hasHelper, invokeHelper, - element, - attributes + element }); import debuggerKeyword from 'ember-htmlbars/keywords/debugger'; diff --git a/packages/ember-htmlbars/lib/hooks/attributes.js b/packages/ember-htmlbars/lib/hooks/attributes.js 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-views/lib/system/build-component-template.js b/packages/ember-views/lib/system/build-component-template.js index ccb6e4ed0f3..246378dcea7 100644 --- a/packages/ember-views/lib/system/build-component-template.js +++ b/packages/ember-views/lib/system/build-component-template.js @@ -1,5 +1,6 @@ import Ember from 'ember-metal/core'; import { get } from 'ember-metal/property_get'; +import assign from 'ember-metal/assign'; import { isGlobal } from 'ember-metal/path_cache'; import { internal, render } from 'htmlbars-runtime'; import getValue from 'ember-htmlbars/hooks/get-value'; @@ -13,10 +14,8 @@ export default function buildComponentTemplate({ component, layout, isAngleBrack } if (layout && layout.raw) { - let attributes = (component && component._isAngleBracket) ? normalizeComponentAttributes(component, true, attrs) : undefined; - let yieldTo = createContentBlocks(content.templates, content.scope, content.self, component); - blockToRender = createLayoutBlock(layout.raw, yieldTo, content.self, component, attrs, attributes); + blockToRender = createLayoutBlock(layout.raw, yieldTo, content.self, component, attrs); meta = layout.raw.meta; } else if (content.templates && content.templates.default) { blockToRender = createContentBlock(content.templates.default, content.scope, content.self, component); @@ -30,14 +29,8 @@ export default function buildComponentTemplate({ component, layout, isAngleBrack // element. We use `manualElement` to create a template that represents // the wrapping element and yields to the previous block. if (tagName !== '') { - let attributes; - - if (isComponentElement) { - attributes = convertAttrsToAst(attrs); - } else { - attributes = normalizeComponentAttributes(component, isAngleBracket, attrs); - } - + if (isComponentElement) { attrs = mergeAttrs(attrs, outerAttrs); } + var attributes = normalizeComponentAttributes(component, isAngleBracket, attrs); var elementTemplate = internal.manualElement(tagName, attributes); elementTemplate.meta = meta; @@ -54,18 +47,27 @@ export default function buildComponentTemplate({ component, layout, isAngleBrack return { createdElement: !!tagName, block: blockToRender }; } +function mergeAttrs(innerAttrs, outerAttrs) { + let result = assign({}, innerAttrs, outerAttrs); + + if (innerAttrs.class && outerAttrs.class) { + result.class = ['subexpr', '-join-classes', [['value', innerAttrs.class], ['value', outerAttrs.class]], []]; + } + + return result; +} + function blockFor(template, options) { Ember.assert('BUG: Must pass a template to blockFor', !!template); return internal.blockFor(render, template, options); } -function createContentBlock(template, scope, self, component, 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 } }); } @@ -86,10 +88,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 @@ -126,23 +127,6 @@ function tagNameFor(view) { return tagName; } -function convertAttrsToAst(attrs) { - let normalized = {}; - - for (var prop in attrs) { - let val = attrs[prop]; - if (!val) { continue; } - - if (typeof val === 'string') { - normalized[prop] = val; - } else if (val.isConcat) { - normalized[prop] = ['value', val]; - } - } - - return normalized; -} - // Takes a component and builds a normalized set of attribute // bindings consumable by HTMLBars' `attribute` hook. function normalizeComponentAttributes(component, isAngleBracket, attrs) { From fa01e2424d1e699c43abfaed37266339936c3305 Mon Sep 17 00:00:00 2001 From: Godhuda Date: Fri, 7 Aug 2015 15:59:27 -0700 Subject: [PATCH 14/28] Fix GlimmerComponent not having `this.element` set --- .../node-managers/component-node-manager.js | 16 ++++++++- .../integration/component_invocation_test.js | 34 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) 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 1342a80961e..7df927aee3b 100644 --- a/packages/ember-htmlbars/lib/node-managers/component-node-manager.js +++ b/packages/ember-htmlbars/lib/node-managers/component-node-manager.js @@ -234,7 +234,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 = element.nextElementSibling; + } + } handleLegacyRender(component, element); env.renderer.didCreateElement(component, element); diff --git a/packages/ember-htmlbars/tests/integration/component_invocation_test.js b/packages/ember-htmlbars/tests/integration/component_invocation_test.js index 21e9516400b..a2bc754aaa5 100644 --- a/packages/ember-htmlbars/tests/integration/component_invocation_test.js +++ b/packages/ember-htmlbars/tests/integration/component_invocation_test.js @@ -1223,6 +1223,40 @@ if (isEnabled('ember-htmlbars-component-generation')) { equal(view.$().text(), 'stuff'); }); + QUnit.test('non-block replaced with a div should have correct `element`', function() { + registry.register('template:components/non-block', compile('
')); + + let component; + + registry.register('component:non-block', GlimmerComponent.extend({ + init() { + this._super(...arguments); + component = this; + } + })); + + view = appendViewFor(''); + + equal(component.element, view.$('div')[0]); + }); + + QUnit.test('non-block replaced with identity element should have correct `element`', function() { + registry.register('template:components/non-block', compile('')); + + let component; + + registry.register('component:non-block', GlimmerComponent.extend({ + init() { + this._super(...arguments); + component = this; + } + })); + + view = appendViewFor(''); + + equal(component.element, view.$('non-block')[0]); + }); + QUnit.test('non-block replaced with a div should have inner attributes', function() { registry.register('template:components/non-block', compile('
')); From fe1dd754739b8f8b36b5e5c2be4b05d9cbf4c8f1 Mon Sep 17 00:00:00 2001 From: Godhuda Date: Fri, 7 Aug 2015 16:00:01 -0700 Subject: [PATCH 15/28] Refactor test cases to not test exact output This was annoying because the ordering of non-user attributes are unimportant, but can change (and has changed) depending on the implementation. --- .../integration/component_invocation_test.js | 39 +++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/packages/ember-htmlbars/tests/integration/component_invocation_test.js b/packages/ember-htmlbars/tests/integration/component_invocation_test.js index a2bc754aaa5..88f7939b2f9 100644 --- a/packages/ember-htmlbars/tests/integration/component_invocation_test.js +++ b/packages/ember-htmlbars/tests/integration/component_invocation_test.js @@ -1070,14 +1070,14 @@ if (isEnabled('ember-htmlbars-component-generation')) { 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'); + equalsElement(node.firstElementChild, 'non-block', { such: 'stability', class: 'ember-view', id: regex(/^ember\d*$/) }, 'In layout'); ok(view.$('non-block.ember-view[id][such=stability]').length === 1, 'The non-block tag name was used'); - run(() => view.set('stability', 'stability!')); + run(() => view.set('stability', 'changed!!!')); 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'); + equalsElement(node.firstElementChild, 'non-block', { such: 'changed!!!', class: 'ember-view', id: regex(/^ember\d*$/) }, 'In layout'); }); QUnit.skip('non-block without properties replaced with identity element (regression if identity element has a single child element)', function() { @@ -1517,3 +1517,36 @@ if (isEnabled('ember-htmlbars-component-generation')) { }); }); } + +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`); +} + From f2f09e8d12cc2c8e440eca0582cefd507c5a05a9 Mon Sep 17 00:00:00 2001 From: Godhuda Date: Fri, 7 Aug 2015 16:14:27 -0700 Subject: [PATCH 16/28] Skip failing tests for now Also re-enabling a test that was actually fixed in the previous commit. --- .../integration/component_invocation_test.js | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/ember-htmlbars/tests/integration/component_invocation_test.js b/packages/ember-htmlbars/tests/integration/component_invocation_test.js index 88f7939b2f9..da5f50d16ad 100644 --- a/packages/ember-htmlbars/tests/integration/component_invocation_test.js +++ b/packages/ember-htmlbars/tests/integration/component_invocation_test.js @@ -1043,7 +1043,7 @@ if (isEnabled('ember-htmlbars-component-generation')) { equal(view.$().html(), '
This is a
fragment
', 'Just the fragment was used'); }); - QUnit.test('non-block without properties replaced with a div', function() { + 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
')); @@ -1080,7 +1080,7 @@ if (isEnabled('ember-htmlbars-component-generation')) { equalsElement(node.firstElementChild, 'non-block', { such: 'changed!!!', class: 'ember-view', id: regex(/^ember\d*$/) }, 'In layout'); }); - QUnit.skip('non-block without properties replaced with identity element (regression if identity element has a single child element)', function() { + QUnit.test('non-block without properties replaced with identity element (regression if identity element has a single child element)', function() { registry.register('template:components/non-block', compile('

In layout

')); view = appendViewFor('', { @@ -1089,17 +1089,17 @@ if (isEnabled('ember-htmlbars-component-generation')) { let node = view.$()[0]; equal(view.$().text(), 'In layout'); - ok(view.$().html().match(/^

In layout<\/p><\/non-block>$/), 'The root element has gotten the default class and ids'); + equalsElement(node.firstElementChild, 'non-block', { such: 'stability', class: 'ember-view', id: regex(/^ember\d*$/) }, '

In layout

'); ok(view.$('non-block.ember-view[id][such=stability]').length === 1, 'The non-block tag name was used'); - run(() => view.set('stability', 'stability!')); + run(() => view.set('stability', 'changed!!!')); 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'); + equalsElement(node.firstElementChild, 'non-block', { such: 'changed!!!', class: 'ember-view', id: regex(/^ember\d*$/) }, '

In layout

'); }); - QUnit.test('non-block with class replaced with a div merges classes', function() { + QUnit.skip('non-block with class replaced with a div merges classes', function() { registry.register('template:components/non-block', compile('
')); view = appendViewFor('', { @@ -1127,7 +1127,7 @@ if (isEnabled('ember-htmlbars-component-generation')) { equal(view.$('non-block').attr('class'), 'inner-class new-outer ember-view', 'the classes are merged'); }); - QUnit.test('non-block with outer attributes replaced with a div shadows inner attributes', function() { + QUnit.skip('non-block with outer attributes replaced with a div shadows inner attributes', function() { registry.register('template:components/non-block', compile('
')); view = appendViewFor(''); @@ -1159,7 +1159,7 @@ if (isEnabled('ember-htmlbars-component-generation')) { equal(view.$('non-block').attr('data-dynamic'), 'outer', 'the outer attribute wins'); }); - QUnit.test('non-block recurrsive invocations with outer attributes replaced with a div shadows inner attributes', function() { + 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('
')); @@ -1176,7 +1176,7 @@ if (isEnabled('ember-htmlbars-component-generation')) { equal(view.$('div').attr('data-dynamic'), 'outer', 'the outer-most attribute wins'); }); - QUnit.test('non-block recurrsive invocations with outer attributes replaced with identity element shadows inner attributes', function() { + QUnit.skip('non-block recursive invocations with outer attributes replaced with identity element shadows inner attributes', function() { registry.register('template:components/non-block-wrapper', compile('')); registry.register('template:components/non-block', compile('')); @@ -1193,7 +1193,7 @@ if (isEnabled('ember-htmlbars-component-generation')) { equal(view.$('div').attr('data-dynamic'), 'outer', 'the outer-most attribute wins'); }); - QUnit.test('non-block replaced with a div should have correct scope', function() { + QUnit.skip('non-block replaced with a div should have correct scope', function() { registry.register('template:components/non-block', compile('
{{internal}}
')); registry.register('component:non-block', GlimmerComponent.extend({ From c745c4766c371a3495049a9769876a6680ddbe85 Mon Sep 17 00:00:00 2001 From: Godhuda Date: Fri, 7 Aug 2015 17:38:51 -0700 Subject: [PATCH 17/28] Fix GlimmerComponent scoping issue See "non-block ... should have correct scope" test cases --- packages/ember-htmlbars/lib/glimmer-component.js | 1 + packages/ember-htmlbars/lib/hooks/bind-shadow-scope.js | 4 +--- .../tests/integration/component_invocation_test.js | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/ember-htmlbars/lib/glimmer-component.js b/packages/ember-htmlbars/lib/glimmer-component.js index 02ff67bc15e..e6120f76f2d 100644 --- a/packages/ember-htmlbars/lib/glimmer-component.js +++ b/packages/ember-htmlbars/lib/glimmer-component.js @@ -16,6 +16,7 @@ export default CoreView.extend( InstrumentationSupport, AriaRoleSupport, ViewMixin, { + isComponent: true, isGlimmerComponent: true, init() { diff --git a/packages/ember-htmlbars/lib/hooks/bind-shadow-scope.js b/packages/ember-htmlbars/lib/hooks/bind-shadow-scope.js index f38cce6ea20..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/components/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/tests/integration/component_invocation_test.js b/packages/ember-htmlbars/tests/integration/component_invocation_test.js index da5f50d16ad..7ecdbe15e3a 100644 --- a/packages/ember-htmlbars/tests/integration/component_invocation_test.js +++ b/packages/ember-htmlbars/tests/integration/component_invocation_test.js @@ -1193,7 +1193,7 @@ if (isEnabled('ember-htmlbars-component-generation')) { equal(view.$('div').attr('data-dynamic'), 'outer', 'the outer-most attribute wins'); }); - QUnit.skip('non-block replaced with a div should have correct scope', function() { + QUnit.test('non-block replaced with a div should have correct scope', function() { registry.register('template:components/non-block', compile('
{{internal}}
')); registry.register('component:non-block', GlimmerComponent.extend({ From ca436a2b1d4d488a6aeadd9ba7e6ab170e3a5e49 Mon Sep 17 00:00:00 2001 From: Godhuda Date: Fri, 7 Aug 2015 18:35:06 -0700 Subject: [PATCH 18/28] WIP: make regular elements like identity elements --- packages/ember-htmlbars/lib/hooks/component.js | 10 ++++++++-- .../integration/component_invocation_test.js | 6 +++--- .../plugins/transform-top-level-components.js | 17 ++++++++++++++--- .../lib/system/build-component-template.js | 6 +++--- 4 files changed, 28 insertions(+), 11 deletions(-) diff --git a/packages/ember-htmlbars/lib/hooks/component.js b/packages/ember-htmlbars/lib/hooks/component.js index 57426721171..407f33eda04 100644 --- a/packages/ember-htmlbars/lib/hooks/component.js +++ b/packages/ember-htmlbars/lib/hooks/component.js @@ -12,7 +12,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,12 +23,17 @@ export default function componentHook(renderNode, env, scope, _tagName, params, isTopLevel = !!angles[1]; } + if (tagName.indexOf('-') !== -1) { + isDasherized = true; + } + var parentView = env.view; - if (isTopLevel && tagName === env.view.tagName) { + if (isTopLevel && tagName === env.view.tagName || !isDasherized) { let component = env.view; let templateOptions = { component, + tagName, isAngleBracket: true, isComponentElement: true, outerAttrs: scope.attrs, diff --git a/packages/ember-htmlbars/tests/integration/component_invocation_test.js b/packages/ember-htmlbars/tests/integration/component_invocation_test.js index 7ecdbe15e3a..75b74236486 100644 --- a/packages/ember-htmlbars/tests/integration/component_invocation_test.js +++ b/packages/ember-htmlbars/tests/integration/component_invocation_test.js @@ -1043,7 +1043,7 @@ if (isEnabled('ember-htmlbars-component-generation')) { equal(view.$().html(), '
This is a
fragment
', 'Just the fragment was used'); }); - QUnit.skip('non-block without properties replaced with a div', function() { + QUnit.test('non-block without properties replaced with a div', function() { // The whitespace is added intentionally to verify that the heuristic is not "a single node" but // rather "a single non-whitespace, non-comment node" registry.register('template:components/non-block', compile('
In layout
')); @@ -1099,7 +1099,7 @@ if (isEnabled('ember-htmlbars-component-generation')) { equalsElement(node.firstElementChild, 'non-block', { such: 'changed!!!', class: 'ember-view', id: regex(/^ember\d*$/) }, '

In layout

'); }); - QUnit.skip('non-block with class replaced with a div merges classes', function() { + QUnit.test('non-block with class replaced with a div merges classes', function() { registry.register('template:components/non-block', compile('
')); view = appendViewFor('', { @@ -1127,7 +1127,7 @@ if (isEnabled('ember-htmlbars-component-generation')) { equal(view.$('non-block').attr('class'), 'inner-class new-outer ember-view', 'the classes are merged'); }); - QUnit.skip('non-block with outer attributes replaced with a div shadows inner attributes', function() { + QUnit.test('non-block with outer attributes replaced with a div shadows inner attributes', function() { registry.register('template:components/non-block', compile('
')); view = appendViewFor(''); 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..f8d72d2e24a 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 @@ -9,14 +9,22 @@ function TransformTopLevelComponents() { @param {AST} The AST to be transformed. */ TransformTopLevelComponents.prototype.transform = function TransformTopLevelComponents_transform(ast) { + let b = this.syntax.builders; + hasSingleComponentNode(ast.body, component => { - component.tag = `@${component.tag}`; + if (component.type === 'ComponentNode') { + component.tag = `@${component.tag}`; + } + }, element => { + // TODO: Properly copy loc from children + let program = b.program(element.children); + return b.component(`@<${element.tag}>`, element.attributes, program, element.loc); }); return ast; }; -function hasSingleComponentNode(body, callback) { +function hasSingleComponentNode(body, componentCallback, elementCallback) { let lastComponentNode; let lastIndex; let nodeCount = 0; @@ -39,7 +47,10 @@ function hasSingleComponentNode(body, callback) { if (!lastComponentNode) { return; } if (lastComponentNode.type === 'ComponentNode') { - callback(lastComponentNode); + componentCallback(lastComponentNode); + } else { + let component = elementCallback(lastComponentNode); + body.splice(lastIndex, 1, component); } } diff --git a/packages/ember-views/lib/system/build-component-template.js b/packages/ember-views/lib/system/build-component-template.js index 246378dcea7..97161b74da0 100644 --- a/packages/ember-views/lib/system/build-component-template.js +++ b/packages/ember-views/lib/system/build-component-template.js @@ -6,8 +6,8 @@ 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; @@ -23,7 +23,7 @@ export default function buildComponentTemplate({ component, layout, isAngleBrack } 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 From 0a66e7f0cad634876297c2814190c52db83a73ec Mon Sep 17 00:00:00 2001 From: Godhuda Date: Sun, 9 Aug 2015 18:47:06 -0700 Subject: [PATCH 19/28] Skip AST transform without Glimmer Components To reduce the possibility of regressions when the `ember-htmlbars-component-generation` flag is off, this commit moves an important AST transformation behind the flag. This AST transformation makes top-level elements in *all* templates dynamic, and requires runtime work to restore the original semantics. Moving the AST transformation behind the flag limits the impact of any bugs in the runtime work. --- .../lib/plugins/transform-top-level-components.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 f8d72d2e24a..afdcf58b51a 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; @@ -48,7 +50,7 @@ function hasSingleComponentNode(body, componentCallback, elementCallback) { if (lastComponentNode.type === 'ComponentNode') { componentCallback(lastComponentNode); - } else { + } else if (isEnabled('ember-htmlbars-component-generation')) { let component = elementCallback(lastComponentNode); body.splice(lastIndex, 1, component); } From 87e021a83559001dec692e6872a69ce2752f8f59 Mon Sep 17 00:00:00 2001 From: Godhuda Date: Sun, 9 Aug 2015 19:00:00 -0700 Subject: [PATCH 20/28] Make regular elements like identity elements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In Ember 1.x components, the top-level element was configured in JavaScript using APIs like `classNames`, `attributeBindings`, `tagName` etc. Glimmer components move that configuration into a top-level element in the template. However, the template compiler does not know how the template will actually be used, so it makes all top-level elements dynamic, and leaves it up to the runtime to figure out what to do. If the template is being invoked for a curly component, the runtime restores the original semantics, treating it just like a regular element. If the template is being invoked for an angle-bracket component, the top-level element is treated as the component’s element. This commit makes top-level `
`s work correctly in Glimmer components (they become the component’s element), and work correctly in curly components (they behave as before). Both modifiers (`
`) and triple-curly attributes are not supported in top-level elements of Glimmer components. As a result, if the AST transformation sees either of those two features, it assumes the template is for a curly component, and does not make the root element dynamic. At the moment, it would be impossible to support either of those two features, since the component AST node in HTMLBars do not support them. Ultimately, we may want to support triple-curly attributes, but probably not modifiers. Note that this commit requires an update to HTMLBars master. --- .../ember-htmlbars/lib/hooks/component.js | 36 ++++++++++++++++--- .../plugins/transform-top-level-components.js | 15 ++++++-- .../lib/system/build-component-template.js | 24 +++++++++++++ 3 files changed, 68 insertions(+), 7 deletions(-) diff --git a/packages/ember-htmlbars/lib/hooks/component.js b/packages/ember-htmlbars/lib/hooks/component.js index 407f33eda04..ce51e9f8125 100644 --- a/packages/ember-htmlbars/lib/hooks/component.js +++ b/packages/ember-htmlbars/lib/hooks/component.js @@ -1,5 +1,5 @@ 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'; export default function componentHook(renderNode, env, scope, _tagName, params, attrs, templates, visitor) { var state = renderNode.state; @@ -27,10 +27,33 @@ export default function componentHook(renderNode, env, scope, _tagName, params, isDasherized = true; } - var parentView = env.view; + let parentView = env.view; - if (isTopLevel && tagName === env.view.tagName || !isDasherized) { - let component = 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 component = env.view; + let isInvokedWithAngles = component && component._isAngleBracket; + let isInvokedWithCurlies = component && !component._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; + + if (isComponentIdentityElement || isComponentHTMLElement) { let templateOptions = { component, tagName, @@ -44,7 +67,12 @@ export default function componentHook(renderNode, env, scope, _tagName, params, 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 { + // "No special semantics" aka we are invoking a component + var manager = ComponentNodeManager.create(renderNode, env, { tagName, params, 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 afdcf58b51a..d6c17e24fc9 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 @@ -16,11 +16,20 @@ TransformTopLevelComponents.prototype.transform = function TransformTopLevelComp hasSingleComponentNode(ast.body, component => { if (component.type === 'ComponentNode') { component.tag = `@${component.tag}`; + component.isStatic = true; } }, element => { - // TODO: Properly copy loc from children - let program = b.program(element.children); - return b.component(`@<${element.tag}>`, element.attributes, program, element.loc); + 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; diff --git a/packages/ember-views/lib/system/build-component-template.js b/packages/ember-views/lib/system/build-component-template.js index 97161b74da0..ddcbd217ab2 100644 --- a/packages/ember-views/lib/system/build-component-template.js +++ b/packages/ember-views/lib/system/build-component-template.js @@ -47,6 +47,30 @@ export default function buildComponentTemplate({ component, tagName, layout, isA 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); From 64247979b8d112de7a89c259ca3e6edc9061abca Mon Sep 17 00:00:00 2001 From: Godhuda Date: Mon, 10 Aug 2015 17:21:14 -0700 Subject: [PATCH 21/28] Optimize the AST transform for top-level elements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Don’t bother to convert top-level elements into dynamic calls if they aren’t the top-level element *of a template*. Consider a template like this: ``` {{#each posts as |post|}}
hello
{{/each}} ``` The previous version of the transform would convert the nested template (the
) into a dynamic call, because the AST transformation didn’t differentiate between top-level templates and nested templates. It didn’t have any other effects (the dynamic call is required to restore the semantics of the original static form) but it unnecessarily bloats templates and makes execution slower. --- .../lib/plugins/transform-top-level-components.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 d6c17e24fc9..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 @@ -13,7 +13,7 @@ function TransformTopLevelComponents() { TransformTopLevelComponents.prototype.transform = function TransformTopLevelComponents_transform(ast) { let b = this.syntax.builders; - hasSingleComponentNode(ast.body, component => { + hasSingleComponentNode(ast, component => { if (component.type === 'ComponentNode') { component.tag = `@${component.tag}`; component.isStatic = true; @@ -35,7 +35,10 @@ TransformTopLevelComponents.prototype.transform = function TransformTopLevelComp return ast; }; -function hasSingleComponentNode(body, componentCallback, elementCallback) { +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; From 5b686d44e3cadb174ed30a2f00e605eca1ebf37f Mon Sep 17 00:00:00 2001 From: Godhuda Date: Mon, 10 Aug 2015 17:23:38 -0700 Subject: [PATCH 22/28] Disallow fragments in GlimmerComponent for now There are multiple competing proposals for how to represent fragments in GlimmerComponents, and we have not yet settled on the exact solution. In the interim, GlimmerComponents require a top-level wrapper element. This commit also provides a good error message describing exactly what the user did wrong. In the future, when fragments are allowed, we can update the error message to suggest opting into a fragment. --- .../node-managers/component-node-manager.js | 19 ++++++++ .../tests/hooks/component_test.js | 2 +- .../integration/component_invocation_test.js | 44 +++++++++++++------ .../integration/component_lifecycle_test.js | 12 ++--- .../lib/system/compile_options.js | 39 ++++++++++------ 5 files changed, 81 insertions(+), 35 deletions(-) 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 1787e5e7e91..d6e40f0d10f 100644 --- a/packages/ember-htmlbars/lib/node-managers/component-node-manager.js +++ b/packages/ember-htmlbars/lib/node-managers/component-node-manager.js @@ -85,6 +85,25 @@ ComponentNodeManager.create = function(renderNode, env, options) { // now that we have the component instance. layout = get(component, 'layout') || layout; + Ember.runInDebug(() => { + if (!layout) { return; } + + let fragmentReason = layout.meta.fragmentReason; + if (isAngleBracket && fragmentReason) { + switch (fragmentReason.name) { + case 'missing-wrapper': + Ember.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} ...}}`); + Ember.assert(`You cannot use ${ modifiers.join(', ') } in the top-level element of the <${tagName}> template because it is a GlimmerComponent.`); + break; + case 'triple-curlies': + Ember.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 } diff --git a/packages/ember-htmlbars/tests/hooks/component_test.js b/packages/ember-htmlbars/tests/hooks/component_test.js index 333c7f246b2..711ff152ee3 100644 --- a/packages/ember-htmlbars/tests/hooks/component_test.js +++ b/packages/ember-htmlbars/tests/hooks/component_test.js @@ -25,7 +25,7 @@ if (isEnabled('ember-htmlbars-component-generation')) { }); QUnit.test('component is looked up from the container', function() { - registry.register('template:components/foo-bar', compile('yippie!')); + registry.register('template:components/foo-bar', compile('yippie!')); view = EmberView.create({ container: container, diff --git a/packages/ember-htmlbars/tests/integration/component_invocation_test.js b/packages/ember-htmlbars/tests/integration/component_invocation_test.js index f3a11bd7ba9..78148bfa146 100644 --- a/packages/ember-htmlbars/tests/integration/component_invocation_test.js +++ b/packages/ember-htmlbars/tests/integration/component_invocation_test.js @@ -799,14 +799,6 @@ if (isEnabled('ember-htmlbars-component-generation')) { } }); - QUnit.test('non-block without properties replaced with a fragment when the content is just text', function() { - registry.register('template:components/non-block', compile('In layout')); - - view = appendViewFor(''); - - equal(view.$().html(), 'In layout', 'Just the fragment was used'); - }); - 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()); @@ -816,12 +808,36 @@ if (isEnabled('ember-htmlbars-component-generation')) { }, /cannot invoke the 'non-block' component with angle brackets/); }); - QUnit.test('non-block without properties replaced with a fragment when the content is multiple elements', function() { + 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('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('
')); - equal(view.$().html(), '
This is a
fragment
', 'Just the fragment was used'); + 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
')); + + 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.test('non-block without properties replaced with a div', function() { @@ -1070,7 +1086,7 @@ if (isEnabled('ember-htmlbars-component-generation')) { equal(view.$('non-block').attr('data-dynamic'), 'stuff'); }); - QUnit.test('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('', { @@ -1088,7 +1104,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'); @@ -1249,7 +1265,7 @@ if (isEnabled('ember-htmlbars-component-generation')) { }); QUnit.test('computed property alias on attrs', function() { - registry.register('template:components/computed-alias', compile('{{otherProp}}')); + registry.register('template:components/computed-alias', compile('{{otherProp}}')); registry.register('component:computed-alias', GlimmerComponent.extend({ otherProp: Ember.computed.alias('attrs.someProp') diff --git a/packages/ember-htmlbars/tests/integration/component_lifecycle_test.js b/packages/ember-htmlbars/tests/integration/component_lifecycle_test.js index 019349e3dfb..64118271b5e 100644 --- a/packages/ember-htmlbars/tests/integration/component_lifecycle_test.js +++ b/packages/ember-htmlbars/tests/integration/component_lifecycle_test.js @@ -127,9 +127,9 @@ styles.forEach(style => { 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}} ${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}}
')); view = EmberView.extend({ template: compile(invoke('the-top', { twitter: 'view.twitter' })), @@ -278,9 +278,9 @@ styles.forEach(style => { registry.register('component:the-middle', component('middle')); registry.register('component:the-bottom', component('bottom')); - 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}}')); + 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}}
')); view = EmberView.extend({ template: compile(invoke('the-top', { twitter: 'view.twitter' })), 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; + } } From 9b6d5ae4104d58b481ec49b1f93e69251c6e0f4e Mon Sep 17 00:00:00 2001 From: Godhuda Date: Mon, 10 Aug 2015 17:48:07 -0700 Subject: [PATCH 23/28] Implement nextElementSibling in JS This feature works in all browsers in our support matrix except for PhantomJS, so the Travis tests are failing. --- .../lib/node-managers/component-node-manager.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) 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 d6e40f0d10f..a646adb2929 100644 --- a/packages/ember-htmlbars/lib/node-managers/component-node-manager.js +++ b/packages/ember-htmlbars/lib/node-managers/component-node-manager.js @@ -191,7 +191,7 @@ ComponentNodeManager.prototype.render = function(_env, visitor) { // Glimmer components may have whitespace or boundary nodes around the // top-level element. if (element && element.nodeType !== 1) { - element = element.nextElementSibling; + element = nextElementSibling(element); } } @@ -202,6 +202,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; From 715aa2b2fa5801d4b17cce6915a6863acd798251 Mon Sep 17 00:00:00 2001 From: Godhuda Date: Tue, 11 Aug 2015 15:00:49 -0700 Subject: [PATCH 24/28] Reorganize for transformation bug The current ember-debug transformations do not support nested asserts inside runInDebug. --- .../node-managers/component-node-manager.js | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) 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 a646adb2929..c4ded2eb364 100644 --- a/packages/ember-htmlbars/lib/node-managers/component-node-manager.js +++ b/packages/ember-htmlbars/lib/node-managers/component-node-manager.js @@ -72,34 +72,34 @@ ComponentNodeManager.create = function(renderNode, env, options) { // Instantiate the component component = createComponent(component, isAngleBracket, createOptions, renderNode, env, attrs); - Ember.runInDebug(() => { - if (isAngleBracket) { - Ember.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 { - Ember.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 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': - Ember.assert(`The <${tagName}> template must have a single top-level element because it is a GlimmerComponent.`); + 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} ...}}`); - Ember.assert(`You cannot use ${ modifiers.join(', ') } in the top-level element of the <${tagName}> template because it is a GlimmerComponent.`); + assert(`You cannot use ${ modifiers.join(', ') } in the top-level element of the <${tagName}> template because it is a GlimmerComponent.`); break; case 'triple-curlies': - Ember.assert(`You cannot use triple curlies (e.g. style={{{ ... }}}) in the top-level element of the <${tagName}> template because it is a GlimmerComponent.`); + assert(`You cannot use triple curlies (e.g. style={{{ ... }}}) in the top-level element of the <${tagName}> template because it is a GlimmerComponent.`); break; } } From 974493f0ddef29f607498d385d3ff3258ba00894 Mon Sep 17 00:00:00 2001 From: Godhuda Date: Tue, 11 Aug 2015 18:08:48 -0700 Subject: [PATCH 25/28] Make non-Ember-component work As of the previous commit, any use of `` was assumed to be an Ember Glimmer Component. This breaks code that was using dasherized names either to refer to web components or just to produce regular HTML elements. We believe that both of these are done in practice. This commit allows `` to be treated the same as `
` in all currently implemented contexts. --- .../ember-htmlbars/lib/hooks/component.js | 31 ++++++++++++++++--- .../node-managers/component-node-manager.js | 11 ++----- .../tests/hooks/component_test.js | 16 +++++----- 3 files changed, 36 insertions(+), 22 deletions(-) diff --git a/packages/ember-htmlbars/lib/hooks/component.js b/packages/ember-htmlbars/lib/hooks/component.js index ce51e9f8125..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, { 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; @@ -40,9 +42,9 @@ export default function componentHook(renderNode, env, scope, _tagName, params, //
are defined by the compiled template, and we need to emulate // those semantics. - let component = env.view; - let isInvokedWithAngles = component && component._isAngleBracket; - let isInvokedWithCurlies = component && !component._isAngleBracket; + 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; @@ -53,9 +55,25 @@ export default function componentHook(renderNode, env, scope, _tagName, params, //
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, + component: currentComponent, tagName, isAngleBracket: true, isComponentElement: true, @@ -71,7 +89,8 @@ export default function componentHook(renderNode, env, scope, _tagName, params, let block = buildHTMLTemplate(tagName, attrs, { templates, scope }); block(env, [], undefined, renderNode, scope, visitor); } else { - // "No special semantics" aka we are invoking a component + // Invoking a component from the outside (either via angle brackets + // or {{foo-bar}} legacy curlies). var manager = ComponentNodeManager.create(renderNode, env, { tagName, @@ -81,6 +100,8 @@ export default function componentHook(renderNode, env, scope, _tagName, params, templates, isAngleBracket, isTopLevel, + component, + layout, parentScope: scope }); 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 c4ded2eb364..716eeb0471f 100644 --- a/packages/ember-htmlbars/lib/node-managers/component-node-manager.js +++ b/packages/ember-htmlbars/lib/node-managers/component-node-manager.js @@ -1,7 +1,6 @@ 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'; @@ -38,18 +37,12 @@ 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 || (isAngleBracket ? GlimmerComponent : LegacyEmberComponent); let createOptions = { parentView }; diff --git a/packages/ember-htmlbars/tests/hooks/component_test.js b/packages/ember-htmlbars/tests/hooks/component_test.js index 711ff152ee3..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'); }); } From 92d4854ca35b010541730053eae5810c8fe0a0eb Mon Sep 17 00:00:00 2001 From: Godhuda Date: Tue, 11 Aug 2015 18:44:14 -0700 Subject: [PATCH 26/28] Test all styles of top-level element in MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, we had hardcoded tests for two styles of top-level element in a GlimmerComponent. For the layout of ``, we had tests for the top-level element being `` itself and being a `
`. A recent commit allowed the top-level element to also be any dasherized element that was not registered as an Ember component (aka “web components”). This commit modifies the tests to enumerate all three styles and runs the same set of tests for each. --- .../integration/component_invocation_test.js | 327 +++++++----------- 1 file changed, 118 insertions(+), 209 deletions(-) diff --git a/packages/ember-htmlbars/tests/integration/component_invocation_test.js b/packages/ember-htmlbars/tests/integration/component_invocation_test.js index 78148bfa146..dd5b193ea48 100644 --- a/packages/ember-htmlbars/tests/integration/component_invocation_test.js +++ b/packages/ember-htmlbars/tests/integration/component_invocation_test.js @@ -840,250 +840,175 @@ if (isEnabled('ember-htmlbars-component-generation')) { }, `You cannot use triple curlies (e.g. style={{{ ... }}}) in the top-level element of the template because it is a GlimmerComponent.`); }); - QUnit.test('non-block without properties replaced with a div', function() { - // The whitespace is added intentionally to verify that the heuristic is not "a single node" but - // rather "a single non-whitespace, non-comment node" - registry.register('template:components/non-block', compile('
In layout
')); + let styles = [{ + name: 'a div', + tagName: 'div' + }, { + name: 'an identity element', + tagName: 'non-block' + }, { + name: 'a web component', + tagName: 'not-an-ember-component' + }]; + + 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 `)); - view = appendViewFor(''); - - equal(view.$().text(), ' In layout '); - ok(view.$().html().match(/^
In layout<\/div> $/), 'The root element has gotten the default class and ids'); - ok(view.$('div.ember-view[id]').length === 1, 'The div became an Ember view'); - - run(view, 'rerender'); + view = appendViewFor(''); - equal(view.$().text(), ' In layout '); - ok(view.$().html().match(/^
In layout<\/div> $/), 'The root element has gotten the default class and ids'); - ok(view.$('div.ember-view[id]').length === 1, 'The non-block tag name was used'); - }); + let node = view.element.firstElementChild; + equalsElement(node, style.tagName, { class: 'ember-view', id: regex(/^ember\d*$/) }, 'In layout'); - QUnit.test('non-block without properties replaced with identity element', function() { - registry.register('template:components/non-block', compile('In layout')); + run(view, 'rerender'); - view = appendViewFor('', { - stability: 'stability' + strictEqual(node, view.element.firstElementChild, 'The inner element has not changed'); + equalsElement(node, style.tagName, { class: 'ember-view', id: regex(/^ember\d*$/) }, 'In layout'); }); - let node = view.$()[0]; - equal(view.$().text(), 'In layout'); - equalsElement(node.firstElementChild, 'non-block', { such: 'stability', class: 'ember-view', id: regex(/^ember\d*$/) }, 'In layout'); - ok(view.$('non-block.ember-view[id][such=stability]').length === 1, 'The non-block tag name was used'); + 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 `)); - run(() => view.set('stability', 'changed!!!')); + view = appendViewFor('', { + stability: 'stability' + }); - strictEqual(view.$()[0], node, 'the DOM node has remained stable'); - equal(view.$().text(), 'In layout'); - equalsElement(node.firstElementChild, 'non-block', { such: 'changed!!!', class: 'ember-view', id: regex(/^ember\d*$/) }, 'In layout'); - }); + let node = view.element.firstElementChild; + equalsElement(node, style.tagName, { such: 'stability', class: 'ember-view', id: regex(/^ember\d*$/) }, 'In layout'); - QUnit.test('non-block without properties replaced with identity element (regression if identity element has a single child element)', function() { - registry.register('template:components/non-block', compile('

In layout

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

In layout

'); - 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', 'changed!!!')); + view = appendViewFor('', { + stability: 'stability' + }); - strictEqual(view.$()[0], node, 'the DOM node has remained stable'); - equal(view.$().text(), 'In layout'); - equalsElement(node.firstElementChild, 'non-block', { such: 'changed!!!', class: 'ember-view', id: regex(/^ember\d*$/) }, '

In layout

'); - }); + let node = view.element.firstElementChild; + equalsElement(node, style.tagName, { such: 'stability', class: 'ember-view', id: regex(/^ember\d*$/) }, '

In layout

'); - QUnit.test('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.test('non-block with class replaced with 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'); - - run(() => view.set('outer', 'new-outer')); - - equal(view.$('non-block').attr('class'), 'inner-class new-outer ember-view', 'the classes are merged'); - }); - - QUnit.test('non-block with outer attributes replaced with a div shadows inner attributes', function() { - registry.register('template:components/non-block', compile('
')); - - view = appendViewFor(''); - - equal(view.$('div').attr('data-static'), 'outer', 'the outer attribute wins'); - equal(view.$('div').attr('data-dynamic'), 'outer', 'the outer attribute wins'); - - let component = view.childViews[0]; // HAX - - run(() => component.set('internal', 'changed')); - - equal(view.$('div').attr('data-static'), 'outer', 'the outer attribute wins'); - equal(view.$('div').attr('data-dynamic'), 'outer', 'the outer attribute wins'); - }); - - QUnit.test('non-block with outer attributes replaced with identity element shadows inner attributes', function() { - registry.register('template:components/non-block', compile('')); - - view = appendViewFor(''); - - equal(view.$('non-block').attr('data-static'), 'outer', 'the outer attribute wins'); - equal(view.$('non-block').attr('data-dynamic'), 'outer', 'the outer attribute wins'); - - let component = view.childViews[0]; // HAX - - run(() => component.set('internal', 'changed')); - - equal(view.$('non-block').attr('data-static'), 'outer', 'the outer attribute wins'); - equal(view.$('non-block').attr('data-dynamic'), 'outer', 'the outer attribute wins'); - }); + 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}}" />`)); - 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(''); - view = appendViewFor(''); + 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.$('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]; // HAX - let component = view.childViews[0].childViews[0]; // HAX + run(() => component.set('internal', 'changed')); - 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.skip('non-block recursive invocations with outer attributes replaced with identity element 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 a div should have correct scope', function() { - registry.register('template:components/non-block', compile('
{{internal}}
')); - - registry.register('component:non-block', GlimmerComponent.extend({ - init() { - this._super(...arguments); - this.set('internal', 'stuff'); - } - })); - - view = appendViewFor(''); - - equal(view.$().text(), 'stuff'); - }); + equal(view.$(style.tagName).attr('data-static'), 'outer', 'the outer attribute wins'); + equal(view.$(style.tagName).attr('data-dynamic'), 'outer', 'the outer attribute wins'); + }); - QUnit.test('non-block replaced with identity element should have correct scope', function() { - registry.register('template:components/non-block', compile('{{internal}}')); + // 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('
')); - registry.register('component:non-block', GlimmerComponent.extend({ - init() { - this._super(...arguments); - this.set('internal', 'stuff'); - } - })); + view = appendViewFor(''); - 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'); - equal(view.$().text(), 'stuff'); - }); + let component = view.childViews[0].childViews[0]; // HAX - QUnit.test('non-block replaced with a div should have correct `element`', function() { - registry.register('template:components/non-block', compile('
')); + run(() => component.set('internal', 'changed')); - let component; + equal(view.$('div').attr('data-static'), 'outer', 'the outer-most attribute wins'); + equal(view.$('div').attr('data-dynamic'), 'outer', 'the outer-most attribute wins'); + }); - registry.register('component:non-block', GlimmerComponent.extend({ - init() { - this._super(...arguments); - component = this; - } - })); + QUnit.test(`non-block replaced with ${style.name} should have correct scope`, function() { + registry.register('template:components/non-block', compile(`<${style.tagName}>{{internal}}`)); - view = appendViewFor(''); + registry.register('component:non-block', GlimmerComponent.extend({ + init() { + this._super(...arguments); + this.set('internal', 'stuff'); + } + })); - equal(component.element, view.$('div')[0]); - }); + view = appendViewFor(''); - QUnit.test('non-block replaced with identity element should have correct `element`', function() { - registry.register('template:components/non-block', compile('')); + equal(view.$().text(), 'stuff'); + }); - let component; + QUnit.test(`non-block replaced with ${style.name} should have correct 'element'`, function() { + registry.register('template:components/non-block', compile(`<${style.tagName} />`)); - registry.register('component:non-block', GlimmerComponent.extend({ - init() { - this._super(...arguments); - component = this; - } - })); + let component; - view = appendViewFor(''); + registry.register('component:non-block', GlimmerComponent.extend({ + init() { + this._super(...arguments); + component = this; + } + })); - equal(component.element, view.$('non-block')[0]); - }); + view = appendViewFor(''); - QUnit.test('non-block replaced with a div should have inner attributes', function() { - registry.register('template:components/non-block', compile('
')); + equal(component.element, view.$(style.tagName)[0]); + }); - registry.register('component:non-block', GlimmerComponent.extend({ - init() { - this._super(...arguments); - this.set('internal', 'stuff'); - } - })); + 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}}" />`)); - view = appendViewFor(''); + registry.register('component:non-block', GlimmerComponent.extend({ + init() { + this._super(...arguments); + this.set('internal', 'stuff'); + } + })); - equal(view.$('div').attr('data-static'), 'static'); - equal(view.$('div').attr('data-dynamic'), 'stuff'); - }); + view = appendViewFor(''); - QUnit.test('non-block replaced with identity element should have inner attributes', function() { - registry.register('template:components/non-block', compile('')); + equal(view.$(style.tagName).attr('data-static'), 'static'); + equal(view.$(style.tagName).attr('data-dynamic'), 'stuff'); + }); - registry.register('component:non-block', GlimmerComponent.extend({ - init() { - this._super(...arguments); - this.set('internal', '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(''); + view = appendViewFor('', { + dynamic: 'dynamic' + }); - equal(view.$('non-block').attr('data-static'), 'static'); - equal(view.$('non-block').attr('data-dynamic'), 'stuff'); + let el = view.$(style.tagName); + ok(el, 'precond - the view was rendered'); + equal(el.attr('static-prop'), 'static text'); + equal(el.attr('concat-prop'), 'dynamic text'); + equal(el.attr('dynamic-prop'), undefined); + }); }); QUnit.skip('[FRAGMENT] non-block rendering a fragment', function() { @@ -1112,22 +1037,6 @@ 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.test('attributes are not installed on the top level', function() { let component; From 0604db16ea85496b294d31f4289fbf1687924e89 Mon Sep 17 00:00:00 2001 From: Godhuda Date: Thu, 13 Aug 2015 10:31:37 -0700 Subject: [PATCH 27/28] Add failing test for top-level {{partial}} Currently a top-level partial that happens to have a single top-level element confuses the heuristics in the component hook. --- .../integration/component_invocation_test.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/ember-htmlbars/tests/integration/component_invocation_test.js b/packages/ember-htmlbars/tests/integration/component_invocation_test.js index dd5b193ea48..4472d5b9dec 100644 --- a/packages/ember-htmlbars/tests/integration/component_invocation_test.js +++ b/packages/ember-htmlbars/tests/integration/component_invocation_test.js @@ -1004,11 +1004,25 @@ if (isEnabled('ember-htmlbars-component-generation')) { }); let el = view.$(style.tagName); - ok(el, 'precond - the view was rendered'); + 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('[FRAGMENT] non-block rendering a fragment', function() { From c4e43a2da05b43877f24993f7cf327e00b979313 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Sat, 15 Aug 2015 20:48:34 -0400 Subject: [PATCH 28/28] Do not export Ember.GlimmerComponent unless feature is enabled. --- packages/ember-htmlbars/lib/main.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/ember-htmlbars/lib/main.js b/packages/ember-htmlbars/lib/main.js index d732440a346..9bb1a3b98ca 100644 --- a/packages/ember-htmlbars/lib/main.js +++ b/packages/ember-htmlbars/lib/main.js @@ -162,7 +162,9 @@ Ember.HTMLBars = { DOMHelper }; -Ember.GlimmerComponent = GlimmerComponent; +if (isEnabled('ember-htmlbars-component-generation')) { + Ember.GlimmerComponent = GlimmerComponent; +} Helper.helper = makeHelper; Ember.Helper = Helper;