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}}
");