Using Ember 2.1, suppose I want to inject a service into all components:
// app/initializers/my-thing.js
export function initialize(application) {
application.inject('component', 'myThing', 'service:my-thing');
}
export default {
name: 'my-thing',
initialize: initialize
};
If I place a reference to {{myThing}} in app/templates/components/my-component.hbs, and I have no corresponding app/components/my-component.js, it comes out undefined. If I create that component JS file (with no content except export default Ember.Component.extend()), the reference in the template starts working. This is inconvenient, since I have many components that need this injected reference and would otherwise be template-only.
I find this strange, because the corresponding controller-based scenario works just fine. If I have only app/templates/index.hbs without any index controller defined, injecting the service into all controllers is sufficient to make it accessible in the template. Why do components work differently?
Seems like a bug to me.
We do not instantiate an Ember.Component for template only components. This is a side effect of that decision.
For this to work, we would have to either instantiate a component or figure out what injections would have been done by the container and inject them into the {{this}} context of that template only component. We would also have to figure out how to deal with a few other things (like what to do about Ember.Component.reopen({ attributeBindings: ['data-icon']}) type things which also do not work today for template only components and how to ensure that initial render speed of template only components does not regress).
I absolutely agree that this is a bug that we should fix.
@rwjblue I'm curious was it a performance decision to not instantiate Ember.Component for template only components?
I get that this issue is a side-effect of that decision. But, I'm curious what the benefit of the decision was?
E.g. maybe this is just a documentation issue for using template only components? No injections (soup) for your template components.
I'm curious was it a performance decision to not instantiate Ember.Component for template only components?
I believe one factor was performance, but I'm not sure of the others. @wycats and @tomdale would have to chime in with more detailed reasoning.
Yes, object creation is one of the most expensive things we do during an initial render. If you don't have a component JS file, we can skip that creation step and just pass attributes into the template directly, which is significantly faster (particularly in Glimmer 2).
It works in the controller case because we only have to instantiate one controller per route—but there may be hundreds if not thousands of components on screen at once, and object creation cost really adds up.
That said, perhaps template-only components are rare enough that the performance wins aren't worth the confusion in programming model. We have performance improvements planned for component creation, although there will always be some cost associated with it.
@grantovich I'd love to know more about your use case. Maybe there is another way of injecting something into the global scope.
Another good solution is a helper that returns the service.
For example, with the following helper:
// app/helpers/service.js
import getOwner from 'ember-getowner-polyfill';
export default Ember.Helper.extend({
compute([serviceName]) {
let owner = getOwner(this);
return owner.lookup(`service:${serviceName}`);
}
})
You could do this in the template:
{{#with (service 'fooBarBaz') as |foo|}}
{{! use foo here}}
{{/with}}
You could also do:
{{some-other-component derp=(service 'something')}}
@tomdale Thanks for that context! Our specific use case is a multi-tenant app – the global injection exposes an object that represents the "current" tenant, so that various components can make use of the tenant's attributes (name, logo, etc). Some of these components are otherwise static chunks of content, so if not for this limitation they would be template-only.
@rwjblue That seems like a promising approach, I'll give it a shot. One thing that concerns me is whether it would cause any problems to have a helper named the same as the injection. For instance, defining both a currentTenant service injection and a currentTenant helper that returns the service. If this is possible/supported, that's good because it means we can upgrade a template-only component to a "real" component without changing the access pattern.
One thing that concerns me is whether it would cause any problems to have a helper named the same as the injection.
I suppose it depends on what "cause any problems" means 😈 . You can absolutely have a helper with the same name as a property, Ember will happily allow this. However, the helper will always win. If that is good for your use case, then :shipit: 😸 ...
You can absolutely have a helper with the same name as a property, Ember will happily allow this. However, the helper will always win.
Great! It doesn't matter which one wins because they will both return the same object, my concern was whether there would _be_ a winner at all, or whether this situation would cause an error/instability. I'll try the helper approach for sure. :smiley_cat:
Zooming back out...
We would also have to figure out how to deal with a few other things (like what to do about
Ember.Component.reopen({ attributeBindings: ['data-icon']})type things which also do not work today for template only components [...]I absolutely agree that this is a bug that we should fix.
Since there is a good workaround for my particular case, I can update the issue title to reflect the wider scope of this problem (or a new issue could be started?).
For now, since there is a solution in place and we have many other somewhat related things inflight, I'm going to go ahead and close...
Most helpful comment
Another good solution is a helper that returns the service.
For example, with the following helper:
You could do this in the template:
You could also do: