From 27ed45f8a0591ac57a9f530d678b12d77965f943 Mon Sep 17 00:00:00 2001 From: Tom Dale and Yehuda Katz Date: Mon, 23 Feb 2015 14:16:02 -0800 Subject: [PATCH] Refactor shared view and component logic This commit moves most of the shared rendering logic for view and components into the `ComponentNode` object. This object was already used in the `component` hook, but it is now also used by the `view` keyword. This commit also changes the `view` keyword to use the new lifecycle style of keywords, which also means that it should now be idempotent. --- .../ember-htmlbars/lib/hooks/bind-self.js | 2 +- .../ember-htmlbars/lib/hooks/component.js | 135 +----------------- packages/ember-htmlbars/lib/keywords/view.js | 57 ++++---- .../lib/system/component-node.js | 126 ++++++++++++++++ 4 files changed, 157 insertions(+), 163 deletions(-) create mode 100644 packages/ember-htmlbars/lib/system/component-node.js diff --git a/packages/ember-htmlbars/lib/hooks/bind-self.js b/packages/ember-htmlbars/lib/hooks/bind-self.js index ffac5e04cc7..3e7e48da582 100644 --- a/packages/ember-htmlbars/lib/hooks/bind-self.js +++ b/packages/ember-htmlbars/lib/hooks/bind-self.js @@ -4,7 +4,7 @@ */ import SimpleStream from "ember-metal/streams/simple"; -import { componentSymbol } from "ember-htmlbars/hooks/component"; +import { componentSymbol } from "ember-htmlbars/system/component-node"; export default function bindSelf(scope, self) { var component = self[componentSymbol]; diff --git a/packages/ember-htmlbars/lib/hooks/component.js b/packages/ember-htmlbars/lib/hooks/component.js index 3a92b1bb202..bc563a55b11 100644 --- a/packages/ember-htmlbars/lib/hooks/component.js +++ b/packages/ember-htmlbars/lib/hooks/component.js @@ -1,28 +1,5 @@ -import { get } from "ember-metal/property_get"; -import Ember from "ember-metal/core"; -import { hooks as htmlbarsHooks } from "htmlbars-runtime"; -import { validateChildMorphs } from "htmlbars-util"; -import { readHash } from "ember-metal/streams/utils"; -import { symbol } from "ember-metal/utils"; -import merge from "ember-metal/merge"; - -export var componentClassSymbol = symbol("componentClass"); -export var componentLayoutSymbol = symbol("componentLayout"); -export var componentSymbol = symbol("component"); - -// my-button template -//

{{attrs.title}}

{{yield}}
-// -// app template -// world -// -// -// component -// world (app-template scope) -// -// shadow root (scope={ attrs: { title: "Hello" } }) -// {{attrs.title}} -// {{yield}} +import ComponentNode from "ember-htmlbars/system/component-node"; +import { componentClassSymbol, componentLayoutSymbol } from "ember-htmlbars/system/component-node"; export default function componentHook(renderNode, env, scope, tagName, attrs, template, visitor) { var state = renderNode.state; @@ -51,111 +28,3 @@ export default function componentHook(renderNode, env, scope, tagName, attrs, te componentNode.render(env, attrs, visitor, parentView._state === 'inDOM'); } -function ComponentNode(component, shadowRoot) { - this.component = component; - this.shadowRoot = shadowRoot; -} - -ComponentNode.create = function(renderNode, env, found, parentView, tagName, contentScope, contentTemplate) { - found = found || lookupComponent(env, tagName); - Ember.assert("Could not find component '" + tagName + "' (no component or template with that name was found)", !!found.component || !!found.layout); - - var component, layoutMorph, layoutTemplate; - - if (found.component) { - component = createComponent(found.component, parentView, renderNode); - component.renderNode = renderNode; - layoutMorph = component.renderer.contentMorphForView(component, renderNode); - layoutTemplate = get(component, 'layout') || get(component, 'template') || found.layout; - } else { - layoutMorph = renderNode; - layoutTemplate = found.layout; - } - - var shadowRoot = new ShadowRoot(layoutMorph, component, layoutTemplate, - contentScope, contentTemplate); - - return new ComponentNode(component, shadowRoot); -}; - -ComponentNode.prototype.render = function(env, attrs, visitor, inDOM) { - var component = this.component; - - if (component) { - env.renderer.setAttrs(this.component, readHash(attrs)); - } - - this.shadowRoot.render(env, attrs, visitor); - - if (component) { - if (inDOM) { - component.renderer.didInsertElement(component); - } else { - // TODO: This should be on ownerNode, not ownerView - component.ownerView.newlyCreated.push(component); - } - } -}; - -ComponentNode.prototype.rerender = function(env, attrs, visitor, shouldRerender) { - var component = this.component; - - if (component) { - var snapshot = readHash(attrs); - - if (shouldRerender) { - env.renderer.updateAttrs(component, snapshot); - } - - env.renderer.willUpdate(component, snapshot); - } - - validateChildMorphs(this.shadowRoot.layoutMorph, visitor); -}; - -function ShadowRoot(layoutMorph, component, layoutTemplate, contentScope, contentTemplate) { - this.layoutMorph = layoutMorph; - this.layoutTemplate = layoutTemplate; - this.hostComponent = component; - - this.contentScope = contentScope; - this.contentTemplate = contentTemplate; -} - -ShadowRoot.prototype.render = function(env, attrs, visitor) { - if (!this.layoutTemplate) { return; } - - var self = { attrs: attrs }; - self[componentSymbol] = this.hostComponent || true; - - var hash = { self: self, layout: this.layoutTemplate }; - - var newEnv = env; - if (this.hostComponent) { - newEnv = merge({}, env); - newEnv.view = this.hostComponent; - } - - // Invoke the `@view` helper. Tell it to render the layout template into the - // layout morph. When the layout template `{{yield}}`s, it should render the - // contentTemplate with the contentScope. - htmlbarsHooks.block(this.layoutMorph, newEnv, this.contentScope, '@view', - [], hash, this.contentTemplate, null, visitor); -}; - -function createComponent(foundComponent, parentView, morph) { - var component = foundComponent.create(); - parentView.linkChild(component); - morph.state.view = component; - return component; -} - -function lookupComponent(env, tagName) { - var container = env.container; - var componentLookup = container.lookup('component-lookup:main'); - - return { - component: componentLookup.componentFor(tagName, container), - layout: componentLookup.layoutFor(tagName, container) - }; -} diff --git a/packages/ember-htmlbars/lib/keywords/view.js b/packages/ember-htmlbars/lib/keywords/view.js index e87fa007540..cc2f2cb3fa3 100644 --- a/packages/ember-htmlbars/lib/keywords/view.js +++ b/packages/ember-htmlbars/lib/keywords/view.js @@ -3,43 +3,46 @@ @submodule ember-htmlbars */ -import { get } from "ember-metal/property_get"; import { readViewFactory } from "ember-views/streams/utils"; -import Ember from "ember-metal/core"; import EmberView from "ember-views/views/view"; +import ComponentNode from "ember-htmlbars/system/component-node"; -export default function viewKeyword(morph, env, scope, params, hash, template, inverse) { - var read = env.hooks.getValue; - var parentView = read(scope.locals.view); - var view = hash.view = getView(read(params[0]), parentView.container); - parentView.linkChild(view); +export default { + setupState: function(state, env, scope, params, hash) { + var read = env.hooks.getValue; + state.parentView = read(scope.locals.view); - morph.state.view = view; + debugger; + state.lastViewClassOrInstance = state.viewClassOrInstance; + state.viewClassOrInstance = getView(read(params[0]), env.container); + }, - Ember.assert("Expected morph to have only a single node", morph.firstNode === morph.lastNode); + isStable: function(state, env, scope, params, hash) { + return state.lastViewClassOrInstance === state.viewClassOrInstance; + }, - var dom = env.dom; + render: function(node, env, scope, params, hash, template, inverse, visitor) { + var state = node.state; + var parentView = state.parentView; - var contentMorph = view.renderer.contentMorphForView(view, morph, dom); + var view = hash.view = viewInstance(node.state.viewClassOrInstance); + parentView.linkChild(view); - var viewHasTemplate = get(view, 'template') || get(view, 'layout') || template; - var inDOM = parentView._state === 'inDOM'; + state.view = view; - if (viewHasTemplate) { - var viewHash = { self: view, layout: get(view, 'template') }; - env.hooks.block(contentMorph, env, scope, '@view', params, viewHash, template, null); - } + var options = { component: view, layout: null }; + var componentNode = ComponentNode.create(node, env, options, parentView, null, scope, template); - view.renderer.didCreateElement(view); + componentNode.render(env, hash, visitor, parentView._state === 'inDOM'); + } +}; - if (inDOM) { - // TODO: Make sure this gets called once all descendents are also in DOM - view.renderer.didInsertElement(view); +function viewInstance(viewClassOrInstance) { + if (viewClassOrInstance instanceof EmberView) { + return viewClassOrInstance; } else { - view.ownerView.newlyCreated.push(view); + return viewClassOrInstance.create(); } - - return true; } function getView(viewPath, container) { @@ -55,9 +58,5 @@ function getView(viewPath, container) { viewClassOrInstance = readViewFactory(viewPath, container); } - if (viewClassOrInstance instanceof EmberView) { - return viewClassOrInstance; - } else { - return viewClassOrInstance.create(); - } + return viewClassOrInstance; } diff --git a/packages/ember-htmlbars/lib/system/component-node.js b/packages/ember-htmlbars/lib/system/component-node.js new file mode 100644 index 00000000000..2ea4a838c05 --- /dev/null +++ b/packages/ember-htmlbars/lib/system/component-node.js @@ -0,0 +1,126 @@ +import { get } from "ember-metal/property_get"; +import Ember from "ember-metal/core"; +import { hooks as htmlbarsHooks } from "htmlbars-runtime"; +import { validateChildMorphs } from "htmlbars-util"; +import { readHash } from "ember-metal/streams/utils"; + +import merge from "ember-metal/merge"; +import { symbol } from "ember-metal/utils"; + +export var componentClassSymbol = symbol("componentClass"); +export var componentLayoutSymbol = symbol("componentLayout"); +export var componentSymbol = symbol("component"); + +function ComponentNode(component, shadowRoot) { + this.component = component; + this.shadowRoot = shadowRoot; +} + +export default ComponentNode; + +ComponentNode.create = function(renderNode, env, found, parentView, tagName, contentScope, contentTemplate) { + found = found || lookupComponent(env, tagName); + Ember.assert("Could not find component '" + tagName + "' (no component or template with that name was found)", !!found.component || !!found.layout); + + var component, layoutMorph, layoutTemplate; + + if (found.component) { + component = createComponent(found.component, parentView, renderNode); + component.renderNode = renderNode; + layoutMorph = component.renderer.contentMorphForView(component, renderNode); + layoutTemplate = get(component, 'layout') || get(component, 'template') || found.layout; + } else { + layoutMorph = renderNode; + layoutTemplate = found.layout; + } + + var shadowRoot = new ShadowRoot(layoutMorph, component, layoutTemplate, + contentScope, contentTemplate); + + return new ComponentNode(component, shadowRoot); +}; + +ComponentNode.prototype.render = function(env, attrs, visitor, inDOM) { + var component = this.component; + + if (component) { + env.renderer.setAttrs(this.component, readHash(attrs)); + } + + this.shadowRoot.render(env, attrs, visitor); + + if (component) { + if (inDOM) { + component.renderer.didInsertElement(component); + } else { + // TODO: This should be on ownerNode, not ownerView + component.ownerView.newlyCreated.push(component); + } + } +}; + +ComponentNode.prototype.rerender = function(env, attrs, visitor, shouldRerender) { + var component = this.component; + + if (component) { + var snapshot = readHash(attrs); + + if (shouldRerender) { + env.renderer.updateAttrs(component, snapshot); + } + + env.renderer.willUpdate(component, snapshot); + } + + validateChildMorphs(this.shadowRoot.layoutMorph, visitor); +}; + +function ShadowRoot(layoutMorph, component, layoutTemplate, contentScope, contentTemplate) { + this.layoutMorph = layoutMorph; + this.layoutTemplate = layoutTemplate; + this.hostComponent = component; + + this.contentScope = contentScope; + this.contentTemplate = contentTemplate; +} + +ShadowRoot.prototype.render = function(env, attrs, visitor) { + if (!this.layoutTemplate && !this.contentTemplate) { return; } + + var self = { attrs: attrs }; + self[componentSymbol] = this.hostComponent || true; + + var hash = { self: self, layout: this.layoutTemplate }; + + var newEnv = env; + if (this.hostComponent) { + newEnv = merge({}, env); + newEnv.view = this.hostComponent; + } + + // Invoke the `@view` helper. Tell it to render the layout template into the + // layout morph. When the layout template `{{yield}}`s, it should render the + // contentTemplate with the contentScope. + htmlbarsHooks.block(this.layoutMorph, newEnv, this.contentScope, '@view', + [], hash, this.contentTemplate || null, null, visitor); +}; + +function createComponent(component, parentView, morph) { + if (component.create) { + component = component.create(); + } + + parentView.linkChild(component); + morph.state.view = component; + return component; +} + +function lookupComponent(env, tagName) { + var container = env.container; + var componentLookup = container.lookup('component-lookup:main'); + + return { + component: componentLookup.componentFor(tagName, container), + layout: componentLookup.layoutFor(tagName, container) + }; +}