Skip to content

Commit 83764f8

Browse files
author
Steve Szczecina
committed
{{id}} helper
1 parent 4ae184e commit 83764f8

File tree

1 file changed

+198
-0
lines changed

1 file changed

+198
-0
lines changed

text/0000-id-helper.md

+198
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
- Start Date: 2020-08-25
2+
- Relevant Team(s): Ember.js
3+
- RFC PR:
4+
- Tracking:
5+
6+
# {{id}} helper
7+
8+
## Summary
9+
10+
Add a new built-in template helper `{{id}}` for generating unique IDs.
11+
12+
See [pre-RFC issue #612](https://github.com/emberjs/rfcs/issues/612)
13+
14+
## Motivation
15+
16+
When working with HTML it is very common to need to create and reference [DOM IDs that are unique within the HTML document](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/id). Classic Ember components provide the `elementId` attribute which can be used to construct unique ids within classic components, but `elementId` is not available within Glimmer components or route templates.
17+
18+
There are several common use cases where a developer may need to generate a unique ID for use in a template:
19+
1. Associating `label` and `input` elements using the label's `for` attribute and the input's `id` attribute.
20+
2. Using WAI-ARIA attributes to improve accessibility (eg. aria-labelledby, aria-controls)
21+
3. Integrating 3rd party libraries that attach themselves to DOM elements using DOM IDs (eg. maps, datepickers, jquery plugins, etc)
22+
23+
Since providing some faculty for generating unique IDs for DOM elements can reasonably be considered a requirement for most Ember apps wishing to implement an accessible UI (via labelled inputs and/or WAI-ARIA), it is reasonable for Ember to provide this functionality at the framework level. Ember already provides the `guidFor` utility in javascript, so it is reasonable for Ember to provide similar functionality within templates.
24+
25+
## Detailed design
26+
27+
Add built-in `{{id}}` template helper.
28+
29+
### `{{id}}`
30+
31+
The id helper can be invoked with no arguments. When invoked this way, the `id` helper will return a new unique id string for every invocation.
32+
33+
In practice this invocation style would usually be paired with a `let` block to enable re-use of the unique id generated by `{{id}}`.
34+
35+
```hbs
36+
{{#let (id) as |emailId|}}
37+
<label for={{emailId}}>Email address</label>
38+
<input id={{emailId}} type="email" />
39+
{{/let}}
40+
41+
{{#let (id) as |passwordId|}}
42+
<label for={{passwordId}}>password</label>
43+
<input id={{passwordId}} type="password" />
44+
{{/let}}
45+
```
46+
47+
In the future, an inline or template version of `let` could enable a single invocation of `{{id}}` for re-use of the id within a template.
48+
49+
### `{{id for=object}}`
50+
51+
The `id` helper can be optionally invoked with a single named argument `for`. This argument accepts any object, string, number, Element, or primitive, which will be treated as a stable reference for an id, allowing the helper to return the same id value for every invocation using the same `for` value.
52+
53+
In class-backed templates where `this` is available, the `for` argument can be used to achieve a more ergonomic invocation style that avoids using `let` blocks.
54+
55+
```
56+
<label for="{{id for=this}}-email">Email address</label>
57+
<input id="{{id for=this}}-email" type="email" />
58+
59+
<label for="{{id for=this}}-password">password</label>
60+
<input id="{{id for=this}}-password" type="password" />
61+
```
62+
63+
### Implementation
64+
65+
Ember already has a `guidFor` utility, so it makes sense to use this existing utility to implement the `{{id}}` helper. Using `guidFor` ensures that all unique ids generated by Ember use the same underlying guid implementation and avoid ID collisions.
66+
67+
A bare-bones implementation of the id helper might looks something like this:
68+
69+
```
70+
import { helper } from '@ember/component/helper';
71+
import { guidFor } from '@ember/object/internals';
72+
import { isPresent } from '@ember/utils';
73+
74+
export default helper(({ for }) => {
75+
return isPresent(for) ? guidFor(for) : guidFor();
76+
});
77+
```
78+
79+
## How we teach this
80+
81+
### Ember API docs: Ember.templates.helpers
82+
83+
The Ember API docs can be updated to include the `id` helper on the page for [Ember.templates.helpers](https://api.emberjs.com/ember/release/classes/Ember.Templates.helpers)
84+
85+
> Use the `{{id}}` helper to generate a unique ID string suitable for use as an ID attribute in the DOM.
86+
>
87+
> ```hbs
88+
> <input id={{id}} type="email" />
89+
> ```
90+
>
91+
> Each invocation of `{{id}}` will return a new, unique ID string. You can use the `let` helper to create an ID that can be reused within a template.
92+
> ```hbs
93+
> {{#let (id) as |emailId|}}
94+
> <label for={{emailId}}>Email address</label>
95+
> <input id={{emailId}} type="email" />
96+
> {{/let}}
97+
> ```
98+
>
99+
> #### using the `for` argument
100+
>
101+
> `id` can be invoked with the named argument `for`. Any object passed as `for` will be used as a stable reference for a unique ID. Whenever `id` is invoked with the same `for` argument, the same ID will be returned. Objects, strings, numbers, component/controller classes, and even DOM elements can be used as a `for` argument.
102+
>
103+
> Assuming the following template has a backing class (such as a controller or component class), `{{id for=this}}` will return the same id string every time it is invoked within that template instance.
104+
>
105+
> ```hbs
106+
> <label for="{{id for=this}}-email">Email address</label>
107+
> <input id="{{id for=this}}-email" type="email" />
108+
> ```
109+
110+
111+
### Ember Guides: Associating labels and inputs
112+
The Ember guides currently include a section on associating labels and inputs. This section can be updated to use this new `{{id}}` helper.
113+
114+
[Guides: associating labels and inputs](https://guides.emberjs.com/release/components/built-in-components/#toc_ways-to-associate-labels-and-inputs)
115+
116+
> Every input should be associated with a label. Within HTML, there are several different ways to do this. In this section, we will show how to apply those strategies for Ember inputs.
117+
>
118+
> You can nest the input inside the label:
119+
> ```hbs
120+
> <label>
121+
> Ask a question about Ember:
122+
> <Input type="text" @value={{this.val}} />
123+
> </label>
124+
> ```
125+
> You can associate the label using for and id:
126+
> ```hbs
127+
> <label for={{this.myUniqueId}}>
128+
> Ask a question about Ember:
129+
> </label>
130+
> <Input id={{this.myUniqueId}} type="text" @value={{this.val}} />
131+
> ```
132+
>
133+
> In HTML, each element's id attribute must be a value that is unique within the HTML document. Ember provides the built-in `{{id}}` helper to assist you with generating unique IDs.
134+
> ```hbs
135+
> <label for="{{id for=this}}-question">
136+
> Ask a question about Ember:
137+
> </label>
138+
> <Input id="{{id for=this}}-question" type="text" @value={{this.val}} />
139+
> ```
140+
>
141+
> You can pass any string, number, object, or other primitive as the named argument `for`, and the `id` helper will return the same ID from every invocation using that same `for` value.
142+
>
143+
> You can also invoke `id` without the `for` argument to get a new unique id from every invocation. This is helpful within template-only components, where `this` is not available.
144+
> ```hbs
145+
> {{#let (id) as |myId|}}
146+
> <label for={{myId}}>
147+
> Ask a question about Ember:
148+
> </label>
149+
> <Input id={{myId}} type="text" @value={{this. val}} />
150+
> {{/let}}
151+
> ```
152+
>
153+
> The aria-label attribute enables developers to label an input element with a string that is not visually rendered, but still available to assistive technology.
154+
> ```hbs
155+
> <Input id="site" @value="How do text fields work?" aria-label="Ember Question"/>
156+
> ```
157+
>
158+
> While it is more appropriate to use a <label> element, the aria-label attribute can be used in instances where visible text content is not possible.
159+
160+
### Accessibility guides
161+
This helper will be an important part of Ember's out-of-the-box accessibility story. Future improvements to Ember's accessibility guides will be able to use this helper when discussing how to build forms and how to work with WAI-ARIA attributes such as `aria-controls` or `aria-describedby`.
162+
163+
164+
## Drawbacks
165+
166+
Adding new helpers increases the surface area of the framework and the code the core team commits to support long term.
167+
168+
Some developers may not prefer the ergonomics of using the `{{id}}` with `let` blocks with-in template-only components.
169+
170+
Optional usage of the `for` argument may make this slightly harder to teach and understand, and may lead to fragmented usage patterns between template-only components and class-backed templates.
171+
172+
There is nothing about this proposal that could not be instead implemented in an add-on.
173+
174+
## Alternatives
175+
176+
1. Do nothing; developers can use backing classes for templates that require an ID, and either use elementId in classic components or import guidFor in glimmer components or via a hand-rolled helper.
177+
178+
2. Introduce a keyword-style syntax that leverages a build-time AST transform to convert this:
179+
```
180+
<label for="{{id}}-toggle">Toggle</label>
181+
<input id="{{id}}-toggle" type="checkbox">
182+
```
183+
into
184+
```
185+
{{#let (id) as |_id|}}
186+
<label for="{{_id}}-toggle">Toggle</label>
187+
<input id="{{_id}}-toggle" type="checkbox">
188+
{{/let}}
189+
```
190+
This approach would eliminate the `for` argument and `{{id}}` would always return a unique id. This approach is implementable in userland or in add-ons, so it may not be appropriate to consider this alternative for the core primitive introduced into Ember.js itself.
191+
192+
This approach may be slightly more ergonomic but relies on a more magical, non-standard keyword-like API that will need to be specifically taught, and will be a larger maintenance burden.
193+
194+
## Unresolved questions
195+
196+
1. **Is `id` the most appropriate name for this helper?** A few alternative names were suggested, most notably `unique-id`. It is my belief that the name `id` is best aligned with the current set of built-in helpers, which are generally as terse as possible. Additionally, I don't believe that the word "unique" is essential to the name of this helper because within the context of HTML/DOM any ID should be assumed to be necessarily unique.
197+
198+
2. The named `for` argument could instead be implemented as a positional param. Would this be a preferable API? We could potentially support both named or positional params for passing a stable reference.

0 commit comments

Comments
 (0)