diff --git a/FEATURES.md b/FEATURES.md index fb0d67c5fc9..ca598244f45 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -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. diff --git a/features.json b/features.json index 4bb640ea38b..2f437d78411 100644 --- a/features.json +++ b/features.json @@ -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", diff --git a/package.json b/package.json index 98376097922..68a2e144df1 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/ember-application/lib/system/application.js b/packages/ember-application/lib/system/application.js index 2dd9c9ca1ac..2b7248351c5 100644 --- a/packages/ember-application/lib/system/application.js +++ b/packages/ember-application/lib/system/application.js @@ -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 }); 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 0bde4b6f075..713ebef475c 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 @@ -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"; @@ -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() { diff --git a/packages/ember-htmlbars/lib/helper.js b/packages/ember-htmlbars/lib/helper.js new file mode 100644 index 00000000000..3aab9f523d7 --- /dev/null +++ b/packages/ember-htmlbars/lib/helper.js @@ -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; diff --git a/packages/ember-htmlbars/lib/hooks/element.js b/packages/ember-htmlbars/lib/hooks/element.js index ddc63fd765e..d02fd31e61d 100644 --- a/packages/ember-htmlbars/lib/hooks/element.js +++ b/packages/ember-htmlbars/lib/hooks/element.js @@ -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; @@ -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); } diff --git a/packages/ember-htmlbars/lib/hooks/has-helper.js b/packages/ember-htmlbars/lib/hooks/has-helper.js index 169f2b4f881..35d8e69ab10 100644 --- a/packages/ember-htmlbars/lib/hooks/has-helper.js +++ b/packages/ember-htmlbars/lib/hooks/has-helper.js @@ -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; } diff --git a/packages/ember-htmlbars/lib/hooks/invoke-helper.js b/packages/ember-htmlbars/lib/hooks/invoke-helper.js index 9f72f98b8e2..92cd928c07d 100644 --- a/packages/ember-htmlbars/lib/hooks/invoke-helper.js +++ b/packages/ember-htmlbars/lib/hooks/invoke-helper.js @@ -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{{x-borf}} {{x-borf 'YES'}}"); + let helper = new HandlebarsCompatibleHelper(function(val) { + return arguments.length > 1 ? val : "BORF"; + }); - Ember.TEMPLATES.application = compile("
{{x-borf}} {{x-borf YES}}
"); - - boot(function() { - registry.register('helper:x-borf', function(val) { - return arguments.length > 1 ? val : "BORF"; - }); + boot(() => { + registry.register('helper:x-borf', helper); }); equal(Ember.$('#wrapper').text(), "BORF YES", "The helper was invoked from the container"); @@ -121,3 +123,24 @@ QUnit.test("Undashed helpers registered on the container can not (presently) be }); }, /A helper named 'omg' could not be found/); }); + +QUnit.test("Helpers can receive injections", function() { + Ember.TEMPLATES.application = compile("
{{full-name}}
"); + + var serviceCalled = false; + boot(function() { + registry.register('service:name-builder', Ember.Service.extend({ + build() { + serviceCalled = true; + } + })); + registry.register('helper:full-name', Helper.extend({ + nameBuilder: Ember.inject.service('name-builder'), + compute() { + this.get('nameBuilder').build(); + } + })); + }); + + ok(serviceCalled, 'service was injected, method called'); +});