Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
219 changes: 219 additions & 0 deletions text/1118-add-helper-manager-for-service.md
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
Copy link
Contributor Author

@NullVoxPopuli NullVoxPopuli Jun 27, 2025

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)

  • re-shaping services as resources would solves this (internal change)

alternatively, we could make the default helper manager setOwner on the function
(then we don't need a separate helper manager)


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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why making the example more verbose than it needs to be?
In the HBS, the Service could be reached via just this.theme.

Suggested change
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;
}
}
import Component from '@glimmer/component';
import { service } from '@ember/service';
export default class extends Component {
@service theme;
}

```

```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
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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?