diff --git a/FEATURES.md b/FEATURES.md index ca598244f45..933ce3d4208 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -293,7 +293,7 @@ for a detailed explanation. displayedPropertyTitle: 'First Name', displayedPropertyKey: 'firstName' }; - ``` + ``` ```hbs

{{displayedPropertyTitle}}

@@ -315,3 +315,8 @@ for a detailed explanation. Implements RFC https://github.com/emberjs/rfcs/pull/53, a public helper api. + +* `ember-htmlbars-dashless-helpers` + + Implements RFC https://github.com/emberjs/rfcs/pull/58, adding support for + dashless helpers. diff --git a/features.json b/features.json index 83ade949666..3b02f6232fd 100644 --- a/features.json +++ b/features.json @@ -20,7 +20,8 @@ "ember-libraries-isregistered": null, "ember-routing-htmlbars-improved-actions": true, "ember-htmlbars-get-helper": null, - "ember-htmlbars-helper": true + "ember-htmlbars-helper": true, + "ember-htmlbars-dashless-helpers": true }, "debugStatements": [ "Ember.warn", diff --git a/packages/container/lib/registry.js b/packages/container/lib/registry.js index 54a48155c1c..7811037700b 100644 --- a/packages/container/lib/registry.js +++ b/packages/container/lib/registry.js @@ -1,6 +1,8 @@ import Ember from 'ember-metal/core'; // Ember.assert import isEnabled from "ember-metal/features"; import dictionary from 'ember-metal/dictionary'; +import keys from 'ember-metal/keys'; +import { assign } from 'ember-metal/merge'; import Container from './container'; var VALID_FULL_NAME_REGEXP = /^[^:]+.+:[^:]+$/; @@ -691,6 +693,36 @@ Registry.prototype = { }); }, + /** + @method knownForType + @param {String} type the type to iterate over + @private + */ + knownForType(type) { + let fallbackKnown, resolverKnown; + + let localKnown = dictionary(null); + let registeredNames = keys(this.registrations); + for (let index = 0, length = registeredNames.length; index < length; index++) { + let fullName = registeredNames[index]; + let itemType = fullName.split(':')[0]; + + if (itemType === type) { + localKnown[fullName] = true; + } + } + + if (this.fallback) { + fallbackKnown = this.fallback.knownForType(type); + } + + if (this.resolver.knownForType) { + resolverKnown = this.resolver.knownForType(type); + } + + return assign({}, fallbackKnown, localKnown, resolverKnown); + }, + validateFullName(fullName) { if (!VALID_FULL_NAME_REGEXP.test(fullName)) { throw new TypeError('Invalid Fullname, expected: `type:name` got: ' + fullName); diff --git a/packages/container/tests/registry_test.js b/packages/container/tests/registry_test.js index 2e10343de93..e84f886352c 100644 --- a/packages/container/tests/registry_test.js +++ b/packages/container/tests/registry_test.js @@ -345,3 +345,58 @@ QUnit.test("`getFactoryTypeInjections` includes factory type injections from a f equal(registry.getFactoryTypeInjections('model').length, 1, "Factory type injections from the fallback registry are merged"); }); + +QUnit.test("`knownForType` contains keys for each item of a given type", function() { + let registry = new Registry(); + + registry.register('foo:bar-baz', 'baz'); + registry.register('foo:qux-fez', 'fez'); + + let found = registry.knownForType('foo'); + + deepEqual(found, { + 'foo:bar-baz': true, + 'foo:qux-fez': true + }); +}); + +QUnit.test("`knownForType` includes fallback registry results", function() { + var fallback = new Registry(); + var registry = new Registry({ fallback: fallback }); + + registry.register('foo:bar-baz', 'baz'); + registry.register('foo:qux-fez', 'fez'); + fallback.register('foo:zurp-zorp', 'zorp'); + + let found = registry.knownForType('foo'); + + deepEqual(found, { + 'foo:bar-baz': true, + 'foo:qux-fez': true, + 'foo:zurp-zorp': true + }); +}); + +QUnit.test("`knownForType` is called on the resolver if present", function() { + expect(3); + + function resolver() { } + resolver.knownForType = function(type) { + ok(true, 'knownForType called on the resolver'); + equal(type, 'foo', 'the type was passed through'); + + return { 'foo:yorp': true }; + }; + + var registry = new Registry({ + resolver + }); + registry.register('foo:bar-baz', 'baz'); + + let found = registry.knownForType('foo'); + + deepEqual(found, { + 'foo:yorp': true, + 'foo:bar-baz': true + }); +}); diff --git a/packages/ember-application/lib/system/application.js b/packages/ember-application/lib/system/application.js index d6f8ef37041..0156e5fe9f5 100644 --- a/packages/ember-application/lib/system/application.js +++ b/packages/ember-application/lib/system/application.js @@ -1125,6 +1125,12 @@ function resolverFor(namespace) { } }; + resolve.knownForType = function knownForType(type) { + if (resolver.knownForType) { + return resolver.knownForType(type); + } + }; + resolve.moduleBasedResolver = resolver.moduleBasedResolver; resolve.__resolver__ = resolver; diff --git a/packages/ember-application/lib/system/resolver.js b/packages/ember-application/lib/system/resolver.js index 10ce9843160..2303b0cf2c8 100644 --- a/packages/ember-application/lib/system/resolver.js +++ b/packages/ember-application/lib/system/resolver.js @@ -6,15 +6,18 @@ import Ember from 'ember-metal/core'; // Ember.TEMPLATES, Ember.assert import { get } from 'ember-metal/property_get'; import Logger from 'ember-metal/logger'; +import keys from 'ember-metal/keys'; import { classify, capitalize, + dasherize, decamelize } from 'ember-runtime/system/string'; import EmberObject from 'ember-runtime/system/object'; import Namespace from 'ember-runtime/system/namespace'; import helpers from 'ember-htmlbars/helpers'; import validateType from 'ember-application/utils/validate-type'; +import dictionary from 'ember-metal/dictionary'; export var Resolver = EmberObject.extend({ /* @@ -104,7 +107,6 @@ export var Resolver = EmberObject.extend({ @extends Ember.Object @public */ -import dictionary from 'ember-metal/dictionary'; export default EmberObject.extend({ /** @@ -419,5 +421,54 @@ export default EmberObject.extend({ } Logger.info(symbol, parsedName.fullName, padding, this.lookupDescription(parsedName.fullName)); + }, + + /** + Used to iterate all items of a given type. + + @method knownForType + @param {String} type the type to search for + @private + */ + knownForType(type) { + let namespace = get(this, 'namespace'); + let suffix = classify(type); + let typeRegexp = new RegExp(`${suffix}$`); + + let known = dictionary(null); + let knownKeys = keys(namespace); + for (let index = 0, length = knownKeys.length; index < length; index++) { + let name = knownKeys[index]; + + if (typeRegexp.test(name)) { + let containerName = this.translateToContainerFullname(type, name); + + known[containerName] = true; + } + } + + return known; + }, + + /** + Converts provided name from the backing namespace into a container lookup name. + + Examples: + + App.FooBarHelper -> helper:foo-bar + App.THelper -> helper:t + + @method translateToContainerFullname + @param {String} type + @param {String} name + @private + */ + + translateToContainerFullname(type, name) { + let suffix = classify(type); + let namePrefix = name.slice(0, suffix.length * -1); + let dasherizedName = dasherize(namePrefix); + + return `${type}:${dasherizedName}`; } }); 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 713ebef475c..0dbd416c744 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 @@ -264,3 +264,23 @@ QUnit.test("no deprecation warning for component factories that extend from Embe application.FooView = Component.extend(); registry.resolve('component:foo'); }); + +QUnit.test('knownForType returns each item for a given type found', function() { + application.FooBarHelper = 'foo'; + application.BazQuxHelper = 'bar'; + + let found = registry.resolver.knownForType('helper'); + + deepEqual(found, { + 'helper:foo-bar': true, + 'helper:baz-qux': true + }); +}); + +QUnit.test('knownForType is not required to be present on the resolver', function() { + delete registry.resolver.__resolver__.knownForType; + + registry.resolver.knownForType('helper', function() { }); + + ok(true, 'does not error'); +}); diff --git a/packages/ember-htmlbars/lib/hooks/has-helper.js b/packages/ember-htmlbars/lib/hooks/has-helper.js index 35d8e69ab10..130c4a1093e 100644 --- a/packages/ember-htmlbars/lib/hooks/has-helper.js +++ b/packages/ember-htmlbars/lib/hooks/has-helper.js @@ -6,7 +6,7 @@ export default function hasHelperHook(env, scope, helperName) { } var container = env.container; - if (validateLazyHelperName(helperName, container, env.hooks.keywords)) { + if (validateLazyHelperName(helperName, container, env.hooks.keywords, env.knownHelpers)) { var containerName = 'helper:' + helperName; if (container._registry.has(containerName)) { return true; diff --git a/packages/ember-htmlbars/lib/system/discover-known-helpers.js b/packages/ember-htmlbars/lib/system/discover-known-helpers.js new file mode 100644 index 00000000000..41a51b598fd --- /dev/null +++ b/packages/ember-htmlbars/lib/system/discover-known-helpers.js @@ -0,0 +1,26 @@ +import isEnabled from "ember-metal/features"; +import dictionary from 'ember-metal/dictionary'; +import keys from 'ember-metal/keys'; + +export default function discoverKnownHelpers(container) { + let registry = container && container._registry; + let helpers = dictionary(null); + + if (isEnabled('ember-htmlbars-dashless-helpers')) { + if (!registry) { + return helpers; + } + + let known = registry.knownForType('helper'); + let knownContainerKeys = keys(known); + + for (let index = 0, length = knownContainerKeys.length; index < length; index++) { + let fullName = knownContainerKeys[index]; + let name = fullName.slice(7); // remove `helper:` from fullName + + helpers[name] = true; + } + } + + return helpers; +} diff --git a/packages/ember-htmlbars/lib/system/lookup-helper.js b/packages/ember-htmlbars/lib/system/lookup-helper.js index 0f738c376a4..3f46b624415 100644 --- a/packages/ember-htmlbars/lib/system/lookup-helper.js +++ b/packages/ember-htmlbars/lib/system/lookup-helper.js @@ -11,8 +11,14 @@ export var CONTAINS_DASH_CACHE = new Cache(1000, function(key) { return key.indexOf('-') !== -1; }); -export function validateLazyHelperName(helperName, container, keywords) { - return container && CONTAINS_DASH_CACHE.get(helperName) && !(helperName in keywords); +export function validateLazyHelperName(helperName, container, keywords, knownHelpers) { + if (!container || (helperName in keywords)) { + return false; + } + + if (knownHelpers[helperName] || CONTAINS_DASH_CACHE.get(helperName)) { + return true; + } } function isLegacyBareHelper(helper) { @@ -38,7 +44,7 @@ export function findHelper(name, view, env) { if (!helper) { var container = env.container; - if (validateLazyHelperName(name, container, env.hooks.keywords)) { + if (validateLazyHelperName(name, container, env.hooks.keywords, env.knownHelpers)) { var helperName = 'helper:' + name; if (container._registry.has(helperName)) { helper = container.lookupFactory(helperName); diff --git a/packages/ember-htmlbars/lib/system/render-env.js b/packages/ember-htmlbars/lib/system/render-env.js index 10867645ed5..bd05b0ba875 100644 --- a/packages/ember-htmlbars/lib/system/render-env.js +++ b/packages/ember-htmlbars/lib/system/render-env.js @@ -1,4 +1,5 @@ import defaultEnv from "ember-htmlbars/env"; +import discoverKnownHelpers from "ember-htmlbars/system/discover-known-helpers"; export default function RenderEnv(options) { this.lifecycleHooks = options.lifecycleHooks || []; @@ -11,6 +12,7 @@ export default function RenderEnv(options) { this.container = options.container; this.renderer = options.renderer; this.dom = options.dom; + this.knownHelpers = options.knownHelpers || discoverKnownHelpers(options.container); this.hooks = defaultEnv.hooks; this.helpers = defaultEnv.helpers; @@ -37,7 +39,8 @@ RenderEnv.prototype.childWithView = function(view) { lifecycleHooks: this.lifecycleHooks, renderedViews: this.renderedViews, renderedNodes: this.renderedNodes, - hasParentOutlet: this.hasParentOutlet + hasParentOutlet: this.hasParentOutlet, + knownHelpers: this.knownHelpers }); }; @@ -51,6 +54,7 @@ RenderEnv.prototype.childWithOutletState = function(outletState, hasParentOutlet lifecycleHooks: this.lifecycleHooks, renderedViews: this.renderedViews, renderedNodes: this.renderedNodes, - hasParentOutlet: hasParentOutlet + hasParentOutlet: hasParentOutlet, + knownHelpers: this.knownHelpers }); }; diff --git a/packages/ember-htmlbars/tests/integration/helper-lookup-test.js b/packages/ember-htmlbars/tests/integration/helper-lookup-test.js new file mode 100644 index 00000000000..2cb57982ac7 --- /dev/null +++ b/packages/ember-htmlbars/tests/integration/helper-lookup-test.js @@ -0,0 +1,46 @@ +import isEnabled from "ember-metal/features"; +import Registry from "container/registry"; +import compile from "ember-template-compiler/system/compile"; +import ComponentLookup from 'ember-views/component_lookup'; +import Component from "ember-views/views/component"; +import { helper } from "ember-htmlbars/helper"; +import { runAppend, runDestroy } from "ember-runtime/tests/utils"; + +var registry, container, component; + +QUnit.module('component - invocation', { + setup() { + registry = new Registry(); + container = registry.container(); + registry.optionsForType('component', { singleton: false }); + registry.optionsForType('view', { singleton: false }); + registry.optionsForType('template', { instantiate: false }); + registry.optionsForType('helper', { instantiate: false }); + registry.register('component-lookup:main', ComponentLookup); + }, + + teardown() { + runDestroy(container); + runDestroy(component); + registry = container = component = null; + } +}); + +if (isEnabled('ember-htmlbars-dashless-helpers')) { + QUnit.test('non-dashed helpers are found', function() { + expect(1); + + registry.register('helper:fullname', helper(function( [first, last]) { + return `${first} ${last}`; + })); + + component = Component.extend({ + layout: compile('{{fullname "Robert" "Jackson"}}'), + container: container + }).create(); + + runAppend(component); + + equal(component.$().text(), 'Robert Jackson'); + }); +} diff --git a/packages/ember-htmlbars/tests/system/discover-known-helpers-test.js b/packages/ember-htmlbars/tests/system/discover-known-helpers-test.js new file mode 100644 index 00000000000..f2da3e172d9 --- /dev/null +++ b/packages/ember-htmlbars/tests/system/discover-known-helpers-test.js @@ -0,0 +1,61 @@ +import isEnabled from "ember-metal/features"; +import Registry from "container/registry"; +import keys from "ember-metal/keys"; +import Helper from "ember-htmlbars/helper"; +import { runDestroy } from "ember-runtime/tests/utils"; +import discoverKnownHelpers from "ember-htmlbars/system/discover-known-helpers"; + +var resolver, registry, container; + +QUnit.module('ember-htmlbars: discover-known-helpers', { + setup() { + resolver = function() { }; + + registry = new Registry({ resolver }); + container = registry.container(); + }, + + teardown() { + runDestroy(container); + registry = container = null; + } +}); + +QUnit.test('returns an empty hash when no helpers are known', function() { + let result = discoverKnownHelpers(container); + + deepEqual(result, {}, 'no helpers were known'); +}); + +if (isEnabled('ember-htmlbars-dashless-helpers')) { + QUnit.test('includes helpers in the registry', function() { + registry.register('helper:t', Helper); + let result = discoverKnownHelpers(container); + let helpers = keys(result); + + deepEqual(helpers, ['t'], 'helpers from the registry were known'); + }); + + QUnit.test('includes resolved helpers', function() { + resolver.knownForType = function() { + return { + 'helper:f': true + }; + }; + + registry.register('helper:t', Helper); + let result = discoverKnownHelpers(container); + let helpers = keys(result); + + deepEqual(helpers, ['t', 'f'], 'helpers from the registry were known'); + }); +} else { + QUnit.test('returns empty object when disabled', function() { + registry.register('helper:t', Helper); + + let result = discoverKnownHelpers(container); + let helpers = keys(result); + + deepEqual(helpers, [], 'helpers from the registry were known'); + }); +} diff --git a/packages/ember-htmlbars/tests/system/lookup-helper_test.js b/packages/ember-htmlbars/tests/system/lookup-helper_test.js index 928b2172c0e..52faf1ecbb3 100644 --- a/packages/ember-htmlbars/tests/system/lookup-helper_test.js +++ b/packages/ember-htmlbars/tests/system/lookup-helper_test.js @@ -8,7 +8,8 @@ function generateEnv(helpers, container) { return { container: container, helpers: (helpers ? helpers : {}), - hooks: { keywords: {} } + hooks: { keywords: {} }, + knownHelpers: {} }; } @@ -72,6 +73,22 @@ QUnit.test('does a lookup in the container if the name contains a dash (and help ok(someName.detect(actual), 'helper is an instance of the helper class'); }); +QUnit.test('does a lookup in the container if the name is found in knownHelpers', function() { + var container = generateContainer(); + var env = generateEnv(null, container); + var view = { + container: container + }; + + env.knownHelpers['t'] = true; + var t = Helper.extend(); + view.container._registry.register('helper:t', t); + + var actual = lookupHelper('t', view, env); + + ok(t.detect(actual), 'helper is an instance of the helper class'); +}); + QUnit.test('looks up a shorthand helper in the container', function() { expect(2); var container = generateContainer(); diff --git a/packages/ember/tests/helpers/helper_registration_test.js b/packages/ember/tests/helpers/helper_registration_test.js index 8bc4c22c4d8..72b5a53ff2f 100644 --- a/packages/ember/tests/helpers/helper_registration_test.js +++ b/packages/ember/tests/helpers/helper_registration_test.js @@ -1,5 +1,6 @@ import "ember"; +import isEnabled from "ember-metal/features"; import EmberHandlebars from "ember-htmlbars/compat"; import HandlebarsCompatibleHelper from "ember-htmlbars/compat/helper"; import Helper from "ember-htmlbars/helper"; @@ -102,27 +103,45 @@ QUnit.test("Bound `makeViewHelper` helpers registered on the container can be us equal(Ember.$('#wrapper').text(), "woot!! woot!!alex", "The helper was invoked from the container"); }); -// we have unit tests for this in ember-htmlbars/tests/system/lookup-helper -// and we are not going to recreate the handlebars helperMissing concept -QUnit.test("Undashed helpers registered on the container can not (presently) be invoked", function() { +if (isEnabled('ember-htmlbars-dashless-helpers')) { + QUnit.test("Undashed helpers registered on the container can be invoked", function() { + Ember.TEMPLATES.application = compile("
{{omg}}|{{yorp 'boo'}}|{{yorp 'ya'}}
"); - // Note: the reason we're not allowing undashed helpers is to avoid - // a possible perf hit in hot code paths, i.e. _triageMustache. - // We only presently perform container lookups if prop.indexOf('-') >= 0 + expectDeprecation(function() { + boot(function() { + registry.register('helper:omg', function([value]) { + return "OMG"; + }); - Ember.TEMPLATES.application = compile("
{{omg}}|{{omg 'GRRR'}}|{{yorp}}|{{yorp 'ahh'}}
"); + registry.register('helper:yorp', makeBoundHelper(function(value) { + return value; + })); + }, /Please use Ember.Helper.build to wrap helper functions./); + }); - expectAssertion(function() { - boot(function() { - registry.register('helper:omg', function() { - return "OMG"; + equal(Ember.$('#wrapper').text(), "OMG|boo|ya", "The helper was invoked from the container"); + }); +} else { + QUnit.test("Undashed helpers registered on the container can not (presently) be invoked", function() { + + // Note: the reason we're not allowing undashed helpers is to avoid + // a possible perf hit in hot code paths, i.e. _triageMustache. + // We only presently perform container lookups if prop.indexOf('-') >= 0 + + Ember.TEMPLATES.application = compile("
{{omg}}|{{omg 'GRRR'}}|{{yorp}}|{{yorp 'ahh'}}
"); + + expectAssertion(function() { + boot(function() { + registry.register('helper:omg', function() { + return "OMG"; + }); + registry.register('helper:yorp', makeBoundHelper(function() { + return "YORP"; + })); }); - registry.register('helper:yorp', makeBoundHelper(function() { - return "YORP"; - })); - }); - }, /A helper named 'omg' could not be found/); -}); + }, /A helper named 'omg' could not be found/); + }); +} QUnit.test("Helpers can receive injections", function() { Ember.TEMPLATES.application = compile("
{{full-name}}
");