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