Skip to content

Commit 33e51a0

Browse files
author
Chris Garrett
committed
Updates based on feedback, convert to official addon
1 parent af106cd commit 33e51a0

File tree

1 file changed

+89
-91
lines changed

1 file changed

+89
-91
lines changed

text/0000-render-element-modifiers.md

Lines changed: 89 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@ an element is setting up or tearing down. Today, this logic conventionally lives
1717
in the `didInsertElement`, `didRender`, `didUpdate`, and `willDestroyElement`
1818
hooks in components, but there are cases where these hooks are not ideal.
1919

20-
This RFC proposes adding two new generic element modifiers, `{{did-render}}` and
21-
`{{will-destroy}}`, which users can use to run code during the most common
22-
phases of any element's lifecycle.
20+
This RFC proposes creating an official Ember addon which provides three new
21+
generic element modifiers: `{{did-insert}}`, `{{did-update}}`, and
22+
`{{will-destroy}}`. Users will be able to use these to run code during the most
23+
common phases of any element's lifecycle.
2324

2425
## Motivation
2526

@@ -87,7 +88,7 @@ without worrying about the overall lifecycle:
8788
```hbs
8889
{{#if this.isOpen}}
8990
<div
90-
{{did-render (action this.setupPopper)}}
91+
{{did-insert (action this.setupPopper)}}
9192
{{will-destroy (action this.teardownPopper)}}
9293
9394
class="popover"
@@ -119,7 +120,7 @@ loop, and the render modifiers can be used to solve them as well:
119120
<ul>
120121
{{#each items as |item|}}
121122
<li
122-
{{did-render (action this.registerElement)}}
123+
{{did-insert (action this.registerElement)}}
123124
{{will-destroy (action this.unregisterElement)}}
124125
>
125126
...
@@ -206,71 +207,106 @@ Developers will be able to define element modifiers in the future with modifier
206207
managers provided by addons. However, the proposed modifier APIs are fairly
207208
verbose (with good reason) and not stabilized.
208209

209-
However, `{{did-render}}` and `{{will-destroy}}` can receive _any_ function as
210-
their first parameter, allowing users to share and reuse common element setup
211-
code with helpers. For instance, a simple `scrollTo` helper could be created to
212-
set the scroll position of an element:
210+
However, the render modifiers can receive _any_ function as their first
211+
parameter, allowing users to share and reuse common element setup code with
212+
helpers. For instance, a simple `scrollTo` helper could be created to set the
213+
scroll position of an element:
213214

214215
```js
215216
// helpers/scroll-to.js
216217
export default function scrollTo() {
217-
(element, scrollPosition) => element.scrollTop = scrollPosition;
218+
return (element, scrollPosition) => element.scrollTop = scrollPosition;
218219
}
219220
```
220221
```hbs
221-
<div {{did-render (scroll-to) @scrollPosition}} class="scroll-container">
222+
<div
223+
{{did-insert (scroll-to) @scrollPosition}}
224+
{{did-update (scroll-to) @scrollPosition}}
225+
class="scroll-container"
226+
>
222227
...
223228
</div>
224229
```
225230

231+
### Official Addon
232+
233+
While these modifiers will be generally useful, modifiers are meant to be a more
234+
generic API that can be used to create libraries for solving specific problems.
235+
Unfortunately, the community hasn't had much time to experiment with modifiers,
236+
since the public API for them hasn't been finalized.
237+
238+
The modifiers in this RFC will provide an basic stepping stone for users who
239+
want to emulate lifecycle hooks and incrementally convert their applications to
240+
modifiers while modifiers in general are being experimented with in the
241+
community. In time, users should be able to pick and choose the modifiers that
242+
suit their needs more directly and effectively, and they shouldn't have to
243+
include these modifiers in the payload. These modifiers should also not be seen
244+
as the "Ember way" - they are just another addon, a basic one supported by
245+
the Ember core team, but one which may or may not be appropriate for a given
246+
application.
247+
226248
## Detailed design
227249

228-
This RFC proposes adding two element modifiers, `{{did-render}}` and
229-
`{{will-destroy}}`. Note that element modifiers do _not_ run in SSR mode - this
230-
code is only run on clients.
250+
This RFC proposes adding three element modifiers:
251+
252+
* `{{did-insert}}`
253+
* `{{did-update}}`
254+
* `{{will-destroy}}`
255+
256+
Note that element modifiers do _not_ run in SSR mode - this code is only run on
257+
clients. Each of these modifiers receives a callback as it's first positional
258+
parameter:
259+
260+
```ts
261+
type RenderModifierCallback = (element: Element, positionalArgs: [any], namedArgs: object): void;
262+
```
263+
264+
The `element` argument is the element that the modifier is applied to,
265+
`positionalArgs` contains any remaining positional arguments passed to the
266+
modifier besides the callback, and `namedArgs` contains any named arguments
267+
passed to the modifier. If the first positional argument is not a callable
268+
function, the modifier will throw an error.
231269

232270
> Note: The timing semantics in the following section were mostly defined in the
233271
> [element modifier manager RFC](https://github.com/emberjs/rfcs/blob/master/text/0373-Element-Modifier-Managers.md)
234272
> and are repeated here for clarity and convenience.
235273
236-
### `{{did-render}}`
274+
### `{{did-insert}}`
237275

238-
This modifier is activated:
239-
240-
1. When The element is inserted in the DOM
241-
2. Whenever any of the arguments passed to it update, including the function
242-
passed as the first argument.
276+
This modifier is activated only when The element is inserted in the DOM.
243277

244278
It has the following timing semantics when activated:
245279

246280
* **Always**
247281
* called after DOM insertion
248-
* called _after_ any child element's `{{did-render}}` modifiers
282+
* called _after_ any child element's `{{did-insert}}` modifiers
249283
* called _after_ the enclosing component's `willRender` hook
250284
* called _before_ the enclosing component's `didRender` hook
251285
* called in definition order in the template
252-
* called after the arguments to the modifier have changed
253286
* **May or May Not**
254287
* be called in the same tick as DOM insertion
255288
* have the sibling nodes fully initialized in DOM
256-
* **Never**
257-
* called if the arguments to the modifier are constants
258289

259290
Note that these statements do not refer to when the modifier is _activated_,
260291
only to when it will be run relative to other hooks and modifiers _should it be
261292
activated_. The modifier is only activated on insertion and arg changes.
262293

263-
`{{did-render}}` receives a function with the following signature as the first
264-
positional parameter:
294+
### `{{did-update}}`
265295

266-
```ts
267-
type DidRenderHandler = (element: Element, ...args): void;
268-
```
296+
This modifier is activated only on _updates_ to it's arguments (both positional
297+
and named). It does _not_ run during or after initial render, or before
298+
element destruction.
299+
300+
It has the following timing semantics when activated:
269301

270-
The `element` argument is the element that the modifier is applied to, and the
271-
rest of the arguments are any remaining positional parameters passed to
272-
`{{did-render}}`. If the first positional parameter is not a callable function,
273-
`{{did-render}}` will throw an error.
302+
* **Always**
303+
* called after the arguments to the modifier have changed
304+
* called _after_ any child element's `{{did-update}}` modifiers
305+
* called _after_ the enclosing component's `willUpdate` hook
306+
* called _before_ the enclosing component's `didUpdate` hook
307+
* called in definition order in the template
308+
* **Never**
309+
* called if the arguments to the modifier are constants
274310

275311
### `{{will-destroy}}`
276312

@@ -285,52 +321,33 @@ It has the following timing semantics when activated:
285321
* called _before_ the enclosing component's `willDestroy` hook
286322
* called in definition order in the template
287323
* **May or May Not**
288-
* be called in the same tick as DOM insertion
289-
290-
`{{will-destroy}}` receives a function with the following signature as the first
291-
positional parameter:
292-
293-
```ts
294-
type WillDestroyHandler = function(element: Element, ...args): void;
295-
```
296-
297-
The `element` argument is the element that the modifier is applied to, and the
298-
rest of the arguments are any remaining positional parameters passed to
299-
`{{will-destroy}}`. If the first positional parameter is not a callable function,
300-
`{{will-destroy}}` will throw an error.
324+
* be called in the same tick as DOM removal
301325

302326
### Function Binding
303327

304328
Functions which are passed to these element modifiers will _not_ be bound to any
305329
context by default. Users can bind them using the `(action)` helper:
306330

307331
```hbs
308-
<div {{did-render (action this.teardownElement)}}></div>
332+
<div {{did-insert (action this.setupElement)}}></div>
309333
```
310334

311-
Currently, neither modifiers nor helpers in Glimmer are given the context of the
312-
template at any point. Both the `{{action}}` helper and modifier are given the
313-
context as an implicit first argument, via an AST transform. The above becomes
314-
the following in the final template, before it is compiled into the Glimmer byte
315-
code:
335+
Or by using the `@action` decorator provided by the
336+
[Decorators RFC](https://github.com/emberjs/rfcs/pull/408) to bind the function
337+
in the class itself:
316338

339+
```js
340+
export default class ExampleComponent extends Component {
341+
@action
342+
setupElement() {
343+
// ...
344+
}
345+
}
346+
```
317347
```hbs
318-
<div {{did-render (action this this.teardownElement)}}></div>
348+
<div {{did-insert this.setupElement}}></div>
319349
```
320350

321-
This gives `{{action}}` the correct context to bind the function it is passed
322-
and was done purely for backwards compatibility, since `{{action}}` existed
323-
before modifiers and helpers were fully rationalized as features.
324-
325-
Adding this implicit context to other helpers and modifiers would require
326-
changes to the Glimmer VM and is a much larger language design problem. As such,
327-
we believe it is out of scope for this RFC. Default binding behavior could be
328-
added in the future, if a context API is decided on.
329-
330-
> Note: It's worth calling out that action's binding behavior can be confusing
331-
> in cases as well, check out [ember-bind-helper](https://github.com/Serabe/ember-bind-helper)
332-
> for an example and alternatives.
333-
334351
## How we teach this
335352

336353
Element modifiers will be new to everyone, so we're starting with a mostly blank
@@ -343,14 +360,18 @@ should be seen as the place for any logic which needs to act directly on an
343360
element, or when an element is added to or removed from the DOM. Modifiers can
344361
be fully independent (for instance, a `scroll-to` modifier that transparently
345362
manages the scroll position of the element) or they can interact with the
346-
component (like the `did-render` and `will-destroy` modifiers). In all cases
363+
component (like the `did-insert` and `will-destroy` modifiers). In all cases
347364
though, they are _tied to the render lifecycle of the element_, and they
348365
generally contain _side-effects_ (though these may be transparent and
349366
declarative, as in the case of `{{action}}` or the theoretical `{{scroll-to}}`).
350367

351368
Second, we should teach the render modifiers specifically. We can do this by
352369
illustrating common use cases which can currently be solved with render hooks,
353-
and comparing them to using modifiers for the same solution.
370+
and comparing them to using modifiers for the same solution. We should also
371+
emphasize that these are an addon, not part of the core framework, and are
372+
useful as solutions for _specific_ problems. As more modifiers become available,
373+
we should create additional guides that focus on using the _best_ modifier for
374+
the job, rather than these generic ones.
354375

355376
One thing we should definitely avoid teaching except in advanced cases is the
356377
_ordering_ of element modifiers. Ideally, element modifiers should be
@@ -548,9 +569,6 @@ Usage:
548569
549570
## Drawbacks
550571
551-
* Element modifiers are a new concept that haven't been fully stabilized as of
552-
yet. It may be premature to add default modifiers to the framework.
553-
554572
* Adding these modifiers means that there are more ways to accomplish similar
555573
goals, which may be confusing to developers. It may be less clear which is the
556574
conventional solution in a given situation.
@@ -564,24 +582,4 @@ Usage:
564582
* Stick with only lifecycle hooks for these situations, and don't add generic
565583
modifiers for them.
566584
567-
* Add an implicit context to modifiers and helpers, instead of relying on users
568-
to bind functions manually. Doing this should take into account a few
569-
constraints and considerations:
570-
571-
* Adding an implicit context may make it more difficult to optimize modifiers
572-
and helpers in the future. If possible, this should be something they opt
573-
_into_, so only helpers which _need_ a context will deoptimize.
574-
575-
* Binding can be counterintuitive in some cases. For instance:
576-
577-
```hbs
578-
<button {{action this.service.reloadData}}>Reload</button>
579-
```
580-
581-
This example will likely error, because the `reloadData` function will be
582-
bound to the _component_, not the service. Likewise, binding helpers doesn't
583-
really make sense, since they should be pure functions. Solutions like the
584-
[`{{bind}}` helper](https://github.com/Serabe/ember-bind-helper) attempt to
585-
address this, but may not be something that can be fully rationalized (what
586-
happens if there are multiple contexts?)
587585

0 commit comments

Comments
 (0)