Skip to content

Commit

Permalink
Refactor shared view and component logic
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Tom Dale and Yehuda Katz authored and tilde-engineering committed Feb 23, 2015
1 parent 812921c commit 27ed45f
Show file tree
Hide file tree
Showing 4 changed files with 157 additions and 163 deletions.
2 changes: 1 addition & 1 deletion packages/ember-htmlbars/lib/hooks/bind-self.js
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
135 changes: 2 additions & 133 deletions packages/ember-htmlbars/lib/hooks/component.js
Original file line number Diff line number Diff line change
@@ -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
// <div class="bs-ui bs-button bs-active"><p>{{attrs.title}}</p>{{yield}}</div>
//
// app template
// <my-button title="Hello">world</my-button>
//
// <app-template>
// <my-button> component
// <range>world</range> (app-template scope)
//
// <my-button> shadow root (scope={ attrs: { title: "Hello" } })
// <range>{{attrs.title}}</range>
// <range>{{yield}}</range>
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;
Expand Down Expand Up @@ -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)
};
}
57 changes: 28 additions & 29 deletions packages/ember-htmlbars/lib/keywords/view.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -55,9 +58,5 @@ function getView(viewPath, container) {
viewClassOrInstance = readViewFactory(viewPath, container);
}

if (viewClassOrInstance instanceof EmberView) {
return viewClassOrInstance;
} else {
return viewClassOrInstance.create();
}
return viewClassOrInstance;
}
126 changes: 126 additions & 0 deletions packages/ember-htmlbars/lib/system/component-node.js
Original file line number Diff line number Diff line change
@@ -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)
};
}

0 comments on commit 27ed45f

Please sign in to comment.