Skip to content

Commit

Permalink
Merge pull request #11278 from mixonic/helpers
Browse files Browse the repository at this point in the history
Implement Ember.Helper: RFC#53.
  • Loading branch information
mixonic committed Jun 8, 2015
2 parents 9e3c165 + c350340 commit 071630b
Show file tree
Hide file tree
Showing 39 changed files with 728 additions and 194 deletions.
5 changes: 5 additions & 0 deletions FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -310,3 +310,8 @@ for a detailed explanation.
for each person.. E.g. a list of all `firstNames`, or `lastNames`, or `ages`.

Addd in [#11196](https://github.com/emberjs/ember.js/pull/11196)

* `ember-htmlbars-helper`

Implements RFC https://github.com/emberjs/rfcs/pull/53, a public helper
api.
3 changes: 2 additions & 1 deletion features.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
"ember-routing-route-configured-query-params": null,
"ember-libraries-isregistered": null,
"ember-routing-htmlbars-improved-actions": true,
"ember-htmlbars-get-helper": null
"ember-htmlbars-get-helper": null,
"ember-htmlbars-helper": true
},
"debugStatements": [
"Ember.warn",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"express": "^4.5.0",
"github": "^0.2.3",
"glob": "~4.3.2",
"htmlbars": "0.13.25",
"htmlbars": "0.13.28",
"qunit-extras": "^1.3.0",
"qunitjs": "^1.16.0",
"route-recognizer": "0.1.5",
Expand Down
1 change: 0 additions & 1 deletion packages/ember-application/lib/system/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -1016,7 +1016,6 @@ Application.reopenClass({
registry.optionsForType('component', { singleton: false });
registry.optionsForType('view', { singleton: false });
registry.optionsForType('template', { instantiate: false });
registry.optionsForType('helper', { instantiate: false });

registry.register('application:main', namespace, { instantiate: false });

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import Service from "ember-runtime/system/service";
import EmberObject from "ember-runtime/system/object";
import Namespace from "ember-runtime/system/namespace";
import Application from "ember-application/system/application";
import Helper, { helper as makeHelper } from "ember-htmlbars/helper";
import makeHandlebarsBoundHelper from "ember-htmlbars/compat/make-bound-helper";
import makeViewHelper from "ember-htmlbars/system/make-view-helper";
import makeHTMLBarsBoundHelper from "ember-htmlbars/system/make_bound_helper";
import {
registerHelper
} from "ember-htmlbars/helpers";
Expand Down Expand Up @@ -102,12 +106,48 @@ QUnit.test("the default resolver resolves helpers", function() {
});

QUnit.test("the default resolver resolves container-registered helpers", function() {
function gooresolvertestHelper() { return 'GOO'; }
function gooGazResolverTestHelper() { return 'GAZ'; }
application.register('helper:gooresolvertest', gooresolvertestHelper);
application.register('helper:goo-baz-resolver-test', gooGazResolverTestHelper);
equal(gooresolvertestHelper, locator.lookup('helper:gooresolvertest'), "looks up gooresolvertest helper");
equal(gooGazResolverTestHelper, locator.lookup('helper:goo-baz-resolver-test'), "looks up gooGazResolverTestHelper helper");
let shorthandHelper = makeHelper(function() {});
let helper = Helper.extend();

application.register('helper:shorthand', shorthandHelper);
application.register('helper:complete', helper);

let lookedUpShorthandHelper = locator.lookupFactory('helper:shorthand');
ok(lookedUpShorthandHelper.isHelperInstance, 'shorthand helper isHelper');

let lookedUpHelper = locator.lookupFactory('helper:complete');
ok(lookedUpHelper.isHelperFactory, 'complete helper is factory');
ok(helper.detect(lookedUpHelper), "looked up complete helper");
});

QUnit.test("the default resolver resolves helpers on the namespace", function() {
let ShorthandHelper = makeHelper(function() {});
let CompleteHelper = Helper.extend();
let LegacyBareFunctionHelper = function() {};
let LegacyHandlebarsBoundHelper = makeHandlebarsBoundHelper(function() {});
let LegacyHTMLBarsBoundHelper = makeHTMLBarsBoundHelper(function() {});
let ViewHelper = makeViewHelper(function() {});

application.ShorthandHelper = ShorthandHelper;
application.CompleteHelper = CompleteHelper;
application.LegacyBareFunctionHelper = LegacyBareFunctionHelper;
application.LegacyHandlebarsBoundHelper = LegacyHandlebarsBoundHelper;
application.LegacyHtmlBarsBoundHelper = LegacyHTMLBarsBoundHelper; // Must use lowered "tml" in "HTMLBars" for resolver to find this
application.ViewHelper = ViewHelper;

let resolvedShorthand = registry.resolve('helper:shorthand');
let resolvedComplete = registry.resolve('helper:complete');
let resolvedLegacy = registry.resolve('helper:legacy-bare-function');
let resolvedLegacyHandlebars = registry.resolve('helper:legacy-handlebars-bound');
let resolvedLegacyHTMLBars = registry.resolve('helper:legacy-html-bars-bound');
let resolvedView = registry.resolve('helper:view');

equal(resolvedShorthand, ShorthandHelper, 'resolve fetches the shorthand helper factory');
equal(resolvedComplete, CompleteHelper, 'resolve fetches the complete helper factory');
ok(typeof resolvedLegacy === 'function', 'legacy function helper is resolved');
equal(resolvedView, ViewHelper, 'resolves view helper');
equal(resolvedLegacyHTMLBars, LegacyHTMLBarsBoundHelper, 'resolves legacy HTMLBars bound helper');
equal(resolvedLegacyHandlebars, LegacyHandlebarsBoundHelper, 'resolves legacy Handlebars bound helper');
});

QUnit.test("the default resolver throws an error if the fullName to resolve is invalid", function() {
Expand Down
23 changes: 23 additions & 0 deletions packages/ember-htmlbars/lib/helper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Object from "ember-runtime/system/object";

// Ember.Helper.extend({ compute(params, hash) {} });
var Helper = Object.extend({
isHelper: true,
recompute() {
this._stream.notify();
}
});

Helper.reopenClass({
isHelperFactory: true
});

// Ember.Helper.helper(function(params, hash) {});
export function helper(helperFn) {
return {
isHelperInstance: true,
compute: helperFn
};
}

export default Helper;
4 changes: 3 additions & 1 deletion packages/ember-htmlbars/lib/hooks/element.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import { findHelper } from "ember-htmlbars/system/lookup-helper";
import { handleRedirect } from "htmlbars-runtime/hooks";
import { buildHelperStream } from "ember-htmlbars/system/invoke-helper";

var fakeElement;

Expand Down Expand Up @@ -32,7 +33,8 @@ export default function emberElement(morph, env, scope, path, params, hash, visi
var result;
var helper = findHelper(path, scope.self, env);
if (helper) {
result = env.hooks.invokeHelper(null, env, scope, null, params, hash, helper, { element: morph.element }).value;
var helperStream = buildHelperStream(helper, params, hash, { element: morph.element }, env, scope);
result = helperStream.value();
} else {
result = env.hooks.get(env, scope, path);
}
Expand Down
16 changes: 14 additions & 2 deletions packages/ember-htmlbars/lib/hooks/has-helper.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
import { findHelper } from "ember-htmlbars/system/lookup-helper";
import { validateLazyHelperName } from "ember-htmlbars/system/lookup-helper";

export default function hasHelperHook(env, scope, helperName) {
return !!findHelper(helperName, scope.self, env);
if (env.helpers[helperName]) {
return true;
}

var container = env.container;
if (validateLazyHelperName(helperName, container, env.hooks.keywords)) {
var containerName = 'helper:' + helperName;
if (container._registry.has(containerName)) {
return true;
}
}

return false;
}
43 changes: 12 additions & 31 deletions packages/ember-htmlbars/lib/hooks/invoke-helper.js
Original file line number Diff line number Diff line change
@@ -1,43 +1,24 @@
import Ember from 'ember-metal/core'; // Ember.assert
import getValue from "ember-htmlbars/hooks/get-value";
import { buildHelperStream } from "ember-htmlbars/system/invoke-helper";

export default function invokeHelper(morph, env, scope, visitor, params, hash, helper, templates, context) {


export default function invokeHelper(morph, env, scope, visitor, _params, _hash, helper, templates, context) {
var params, hash;

if (typeof helper === 'function') {
params = getArrayValues(_params);
hash = getHashValues(_hash);
return { value: helper.call(context, params, hash, templates) };
} else if (helper.isLegacyViewHelper) {
if (helper.isLegacyViewHelper) {
Ember.assert("You can only pass attributes (such as name=value) not bare " +
"values to a helper for a View found in '" + helper.viewClass + "'", _params.length === 0);
"values to a helper for a View found in '" + helper.viewClass + "'", params.length === 0);

env.hooks.keyword('view', morph, env, scope, [helper.viewClass], _hash, templates.template.raw, null, visitor);
env.hooks.keyword('view', morph, env, scope, [helper.viewClass], hash, templates.template.raw, null, visitor);
// Opts into a special mode for view helpers
return { handled: true };
} else if (helper && helper.helperFunction) {
var helperFunc = helper.helperFunction;
return { value: helperFunc.call({}, _params, _hash, templates, env, scope) };
}
}

// We don't want to leak mutable cells into helpers, which
// are pure functions that can only work with values.
function getArrayValues(params) {
let out = [];
for (let i=0, l=params.length; i<l; i++) {
out.push(getValue(params[i]));
}

return out;
}
var helperStream = buildHelperStream(helper, params, hash, templates, env, scope, context);

function getHashValues(hash) {
let out = {};
for (let prop in hash) {
out[prop] = getValue(hash[prop]);
// Ember.Helper helpers are pure values, thus linkable
if (helperStream.linkable) {
return { link: true, value: helperStream };
}

return out;
// Legacy helpers are not linkable, they must run every rerender
return { value: helperStream.value() };
}
46 changes: 12 additions & 34 deletions packages/ember-htmlbars/lib/hooks/subexpr.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,8 @@
*/

import lookupHelper from "ember-htmlbars/system/lookup-helper";
import merge from "ember-metal/merge";
import Stream from "ember-metal/streams/stream";
import create from "ember-metal/platform/create";
import { buildHelperStream } from "ember-htmlbars/system/invoke-helper";
import {
readArray,
readHash,
labelsFor,
labelFor
} from "ember-metal/streams/utils";
Expand All @@ -22,15 +18,20 @@ export default function subexpr(env, scope, helperName, params, hash) {
return keyword(null, env, scope, params, hash, null, null);
}

var label = labelForSubexpr(params, hash, helperName);
var helper = lookupHelper(helperName, scope.self, env);
var invoker = function(params, hash) {
return env.hooks.invokeHelper(null, env, scope, null, params, hash, helper, { template: {}, inverse: {} }, undefined).value;
};

//Ember.assert("A helper named '"+helperName+"' could not be found", typeof helper === 'function');
var helperStream = buildHelperStream(helper, params, hash, { template: {}, inverse: {} }, env, scope, label);

var label = labelForSubexpr(params, hash, helperName);
return new SubexprStream(params, hash, invoker, label);
for (var i = 0, l = params.length; i < l; i++) {
helperStream.addDependency(params[i]);
}

for (var key in hash) {
helperStream.addDependency(hash[key]);
}

return helperStream;
}

function labelForSubexpr(params, hash, helperName) {
Expand All @@ -57,26 +58,3 @@ function labelsForHash(hash) {

return out.join(" ");
}

function SubexprStream(params, hash, helper, label) {
this.init(label);
this.params = params;
this.hash = hash;
this.helper = helper;

for (var i = 0, l = params.length; i < l; i++) {
this.addDependency(params[i]);
}

for (var key in hash) {
this.addDependency(hash[key]);
}
}

SubexprStream.prototype = create(Stream.prototype);

merge(SubexprStream.prototype, {
compute() {
return this.helper(readArray(this.params), readHash(this.hash));
}
});
6 changes: 6 additions & 0 deletions packages/ember-htmlbars/lib/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import legacyEachWithKeywordHelper from "ember-htmlbars/helpers/-legacy-each-wit
import getHelper from "ember-htmlbars/helpers/-get";
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";

// importing adds template bootstrapping
// initializer to enable embedded templates
Expand Down Expand Up @@ -70,3 +71,8 @@ Ember.HTMLBars = {
registerPlugin: registerPlugin,
DOMHelper
};

if (Ember.FEATURES.isEnabled('ember-htmlbars-helpers')) {
Helper.helper = makeHelper;
Ember.Helper = Helper;
}
27 changes: 27 additions & 0 deletions packages/ember-htmlbars/lib/streams/built-in-helper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import Stream from "ember-metal/streams/stream";
import create from "ember-metal/platform/create";
import merge from "ember-metal/merge";
import {
getArrayValues,
getHashValues
} from "ember-htmlbars/streams/utils";

export default function BuiltInHelperStream(helper, params, hash, templates, env, scope, context, label) {
this.init(label);
this.helper = helper;
this.params = params;
this.templates = templates;
this.env = env;
this.scope = scope;
this.hash = hash;
this.context = context;
}

BuiltInHelperStream.prototype = create(Stream.prototype);

merge(BuiltInHelperStream.prototype, {
compute() {
// Using call and undefined is probably not needed, these are only internal
return this.helper.call(this.context, getArrayValues(this.params), getHashValues(this.hash), this.templates, this.env, this.scope);
}
});
22 changes: 22 additions & 0 deletions packages/ember-htmlbars/lib/streams/compat-helper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import Stream from "ember-metal/streams/stream";
import create from "ember-metal/platform/create";
import merge from "ember-metal/merge";

export default function CompatHelperStream(helper, params, hash, templates, env, scope, label) {
this.init(label);
this.helper = helper.helperFunction;
this.params = params;
this.templates = templates;
this.env = env;
this.scope = scope;
this.hash = hash;
}

CompatHelperStream.prototype = create(Stream.prototype);

merge(CompatHelperStream.prototype, {
compute() {
// Using call and undefined is probably not needed, these are only internal
return this.helper.call(undefined, this.params, this.hash, this.templates, this.env, this.scope);
}
});
35 changes: 35 additions & 0 deletions packages/ember-htmlbars/lib/streams/helper-factory.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import Stream from "ember-metal/streams/stream";
import create from "ember-metal/platform/create";
import merge from "ember-metal/merge";
import {
getArrayValues,
getHashValues
} from "ember-htmlbars/streams/utils";

export default function HelperFactoryStream(helperFactory, params, hash, label) {
this.init(label);
this.helperFactory = helperFactory;
this.params = params;
this.hash = hash;
this.linkable = true;
this.helper = null;
}

HelperFactoryStream.prototype = create(Stream.prototype);

merge(HelperFactoryStream.prototype, {
compute() {
if (!this.helper) {
this.helper = this.helperFactory.create({ _stream: this });
}
return this.helper.compute(getArrayValues(this.params), getHashValues(this.hash));
},
deactivate() {
this.super$deactivate();
if (this.helper) {
this.helper.destroy();
this.helper = null;
}
},
super$deactivate: HelperFactoryStream.prototype.deactivate
});
Loading

0 comments on commit 071630b

Please sign in to comment.