Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Ember.Helper: RFC#53. #11278

Merged
merged 4 commits into from
Jun 8, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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