-
-
Notifications
You must be signed in to change notification settings - Fork 408
Make @service able to be used in templates #1118
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
base: main
Are you sure you want to change the base?
Changes from all commits
d9b2837
d819f83
61e1a2b
936b557
6690f14
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,219 @@ | ||||||||||||||||||||||||||||||||||||
--- | ||||||||||||||||||||||||||||||||||||
stage: accepted | ||||||||||||||||||||||||||||||||||||
start-date: 2025-06-15T00:00:00.000Z # In format YYYY-MM-DDT00:00:00.000Z | ||||||||||||||||||||||||||||||||||||
release-date: # In format YYYY-MM-DDT00:00:00.000Z | ||||||||||||||||||||||||||||||||||||
release-versions: | ||||||||||||||||||||||||||||||||||||
teams: | ||||||||||||||||||||||||||||||||||||
- framework | ||||||||||||||||||||||||||||||||||||
- typescript | ||||||||||||||||||||||||||||||||||||
prs: | ||||||||||||||||||||||||||||||||||||
accepted: https://github.com/emberjs/rfcs/pull/1118 | ||||||||||||||||||||||||||||||||||||
project-link: | ||||||||||||||||||||||||||||||||||||
suite: | ||||||||||||||||||||||||||||||||||||
--- | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
<!--- | ||||||||||||||||||||||||||||||||||||
Directions for above: | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
stage: Leave as is | ||||||||||||||||||||||||||||||||||||
start-date: Fill in with today's date, 2032-12-01T00:00:00.000Z | ||||||||||||||||||||||||||||||||||||
release-date: Leave as is | ||||||||||||||||||||||||||||||||||||
release-versions: Leave as is | ||||||||||||||||||||||||||||||||||||
teams: Include only the [team(s)](README.md#relevant-teams) for which this RFC applies | ||||||||||||||||||||||||||||||||||||
prs: | ||||||||||||||||||||||||||||||||||||
accepted: Fill this in with the URL for the Proposal RFC PR | ||||||||||||||||||||||||||||||||||||
project-link: Leave as is | ||||||||||||||||||||||||||||||||||||
suite: Leave as is | ||||||||||||||||||||||||||||||||||||
--> | ||||||||||||||||||||||||||||||||||||
# Service Helper Manager | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
## Summary | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
Add helper manager capabilities to `@ember/service`'s `service` export, enabling direct template usage while maintaining backward compatibility with existing service injection patterns. | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
## Motivation | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
Current service access in templates requires unnecessary boilerplate: | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
```js | ||||||||||||||||||||||||||||||||||||
import Component from '@glimmer/component'; | ||||||||||||||||||||||||||||||||||||
import { service } from '@ember/service'; | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
export default class extends Component { | ||||||||||||||||||||||||||||||||||||
@service theme; | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
// Boilerplate just to expose the service | ||||||||||||||||||||||||||||||||||||
get exposedTheme() { | ||||||||||||||||||||||||||||||||||||
return this.theme; | ||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||
Comment on lines
+39
to
+49
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why making the example more verbose than it needs to be?
Suggested change
|
||||||||||||||||||||||||||||||||||||
``` | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
```hbs | ||||||||||||||||||||||||||||||||||||
{{this.exposedTheme.toggle}} | ||||||||||||||||||||||||||||||||||||
``` | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
With helper manager capabilities, eliminate the boilerplate: | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
```gjs | ||||||||||||||||||||||||||||||||||||
import Component from '@glimmer/component'; | ||||||||||||||||||||||||||||||||||||
import { service } from '@ember/service'; | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
<template> | ||||||||||||||||||||||||||||||||||||
{{#let (service "theme") as |theme|}} | ||||||||||||||||||||||||||||||||||||
<button {{on "click" theme.toggle}}>Toggle Theme</button> | ||||||||||||||||||||||||||||||||||||
{{/let}} | ||||||||||||||||||||||||||||||||||||
</template> | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
export default class extends Component {} | ||||||||||||||||||||||||||||||||||||
``` | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
## Detailed design | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
### Enhanced `service` Export | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
The `service` export gains helper manager capabilities while preserving existing patterns: | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
```js | ||||||||||||||||||||||||||||||||||||
class { | ||||||||||||||||||||||||||||||||||||
// All existing usage unchanged | ||||||||||||||||||||||||||||||||||||
@service theme; | ||||||||||||||||||||||||||||||||||||
@service('user-preferences') prefs; | ||||||||||||||||||||||||||||||||||||
theme = service('theme'); | ||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||
``` | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
```gjs | ||||||||||||||||||||||||||||||||||||
import { service } from '@ember/service'; | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
// New template helper usage | ||||||||||||||||||||||||||||||||||||
<template> | ||||||||||||||||||||||||||||||||||||
{{! Direct service access with let }} | ||||||||||||||||||||||||||||||||||||
{{#let (service "theme") as |theme|}} | ||||||||||||||||||||||||||||||||||||
<button {{on "click" theme.toggle}}>Toggle Theme</button> | ||||||||||||||||||||||||||||||||||||
{{/let}} | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
{{! Dynamic service names }} | ||||||||||||||||||||||||||||||||||||
{{#let (service @serviceName) as |dynamicService|}} | ||||||||||||||||||||||||||||||||||||
{{dynamicService.someMethod}} | ||||||||||||||||||||||||||||||||||||
{{/let}} | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
{{! Composition with iteration }} | ||||||||||||||||||||||||||||||||||||
{{#let (service "cart") as |cart|}} | ||||||||||||||||||||||||||||||||||||
{{#each cart.items as |item|}} | ||||||||||||||||||||||||||||||||||||
<div>{{item.name}} - {{cart.formatPrice item.price}}</div> | ||||||||||||||||||||||||||||||||||||
{{/each}} | ||||||||||||||||||||||||||||||||||||
{{/let}} | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
{{! Property access }} | ||||||||||||||||||||||||||||||||||||
<div class={{if (get (service "theme") 'isDark') 'dark' 'light'}}> | ||||||||||||||||||||||||||||||||||||
Content here | ||||||||||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||||||||||
</template> | ||||||||||||||||||||||||||||||||||||
``` | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
### Implementation | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
```js | ||||||||||||||||||||||||||||||||||||
import { service } from '@ember/service'; | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
// roughly: | ||||||||||||||||||||||||||||||||||||
class ServiceHelperManager { | ||||||||||||||||||||||||||||||||||||
createHelper(state, { positional: [serviceName] }) { | ||||||||||||||||||||||||||||||||||||
return { service: getOwner(state).lookup(`service:${serviceName}`) }; | ||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
getValue({ service }) { | ||||||||||||||||||||||||||||||||||||
return service; | ||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
setHelperManager(ServiceHelperManager, service); | ||||||||||||||||||||||||||||||||||||
``` | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
### Template-only Components | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
Perfect for template-only components: | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
```gjs | ||||||||||||||||||||||||||||||||||||
import { service } from '@ember/service'; | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
<template> | ||||||||||||||||||||||||||||||||||||
{{#let (service "cart") as |cart|}} | ||||||||||||||||||||||||||||||||||||
<span class="cart-count">{{cart.totalItems}}</span> | ||||||||||||||||||||||||||||||||||||
<span class="cart-total">{{cart.formattedTotal}}</span> | ||||||||||||||||||||||||||||||||||||
{{/let}} | ||||||||||||||||||||||||||||||||||||
{{yield}} | ||||||||||||||||||||||||||||||||||||
</template> | ||||||||||||||||||||||||||||||||||||
``` | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
### Common Patterns | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
```gjs | ||||||||||||||||||||||||||||||||||||
import { service } from '@ember/service'; | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
<template> | ||||||||||||||||||||||||||||||||||||
{{! Service method calls require let }} | ||||||||||||||||||||||||||||||||||||
{{#let (service "formatter") as |formatter|}} | ||||||||||||||||||||||||||||||||||||
<span>{{formatter.currency @price}}</span> | ||||||||||||||||||||||||||||||||||||
<time>{{formatter.date @timestamp}}</time> | ||||||||||||||||||||||||||||||||||||
{{/let}} | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
{{! Multiple services }} | ||||||||||||||||||||||||||||||||||||
{{#let (service "cart") (service "theme") as |cart theme|}} | ||||||||||||||||||||||||||||||||||||
<div class={{theme.currentTheme}}> | ||||||||||||||||||||||||||||||||||||
<p>Items: {{cart.totalItems}}</p> | ||||||||||||||||||||||||||||||||||||
<button {{on "click" cart.clear}}>Clear Cart</button> | ||||||||||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||||||||||
{{/let}} | ||||||||||||||||||||||||||||||||||||
</template> | ||||||||||||||||||||||||||||||||||||
``` | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
### TypeScript Support | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
Full type inference: | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
```gts | ||||||||||||||||||||||||||||||||||||
import { service } from '@ember/service'; | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
<template> | ||||||||||||||||||||||||||||||||||||
{{#let (service "theme") as |theme|}} | ||||||||||||||||||||||||||||||||||||
{{! TypeScript knows theme is ThemeService }} | ||||||||||||||||||||||||||||||||||||
<button {{on "click" theme.toggle}}>{{theme.buttonText}}</button> | ||||||||||||||||||||||||||||||||||||
{{/let}} | ||||||||||||||||||||||||||||||||||||
</template> | ||||||||||||||||||||||||||||||||||||
``` | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
## How we teach this | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
### Mental Model | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
"Service gets you a service" - consistent whether in classes or templates. Template usage follows standard Handlebars patterns with `{{#let}}` for method calls. | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
### Documentation | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
Update `@ember/service` docs with template examples. Add Guides section on "Services in Templates" emphasizing the `{{#let}}` pattern for method calls. | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
## Drawbacks | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
- **API Surface**: Another way to access services | ||||||||||||||||||||||||||||||||||||
- **Template Verbosity**: `{{#let}}` adds nesting for method calls | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
## Alternatives | ||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. write about how service can actually be a resource |
||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
- Keep requiring backing class injection. Maintains separation but creates boilerplate. | ||||||||||||||||||||||||||||||||||||
This leads to utilities like [service in reactiveweb](https://reactive.nullvoxpopuli.com/functions/resource_service.service.html) | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
- There could be a future where services are user-land definable as _Resources_, as shown in [RFC #1122: Resources](https://github.com/emberjs/rfcs/pull/1122).| | ||||||||||||||||||||||||||||||||||||
Since resources _are_ helpers (by definition and implementation), the way we access singleton values on the application would be the same in JavaScript as in the component template. | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
## Unresolved questions | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
- Error handling for missing services? | ||||||||||||||||||||||||||||||||||||
- Would be more doable if we had try/catch in templates | ||||||||||||||||||||||||||||||||||||
- Build-time vs runtime service validation? | ||||||||||||||||||||||||||||||||||||
- forbid dynamic service names? |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Explain why service can't just work with the default helper manager (default helper manager doesn't know about the owner)
alternatively, we could make the default helper manager setOwner on the function
(then we don't need a separate helper manager)