|
| 1 | +An HTMLBars runtime environment implements a series of hooks (and |
| 2 | +keywords) that are responsible for guaranteeing the most important |
| 3 | +property of an HTMLBars template: idempotence. |
| 4 | + |
| 5 | +This means that a template that is re-rendered with the same dynamic |
| 6 | +environment will result in the same DOM nodes (with the same identity) |
| 7 | +as the first render. |
| 8 | + |
| 9 | +HTMLBars comes with support for idempotent helpers. This means that a |
| 10 | +helper implemented using the HTMLBars API is guaranteed to fulfill the |
| 11 | +idempotence requirement. That is because an HTMLBars template is a "pure |
| 12 | +function"; it takes in data parameters and returns data values. |
| 13 | + |
| 14 | +> Block helpers also have access to `this.yield()`, which allows them to |
| 15 | +> render the block passed to the block helper, but they do not have |
| 16 | +> access to the block itself, nor the ability to directly insert the |
| 17 | +> block into the DOM. As long as `this.yield()` is invoked in two |
| 18 | +> successive renders, HTMLBars guarantees that the second call |
| 19 | +> effectively becomes a no-op and does not tear down the template. |
| 20 | +
|
| 21 | +HTMLBars environments are expected to implement an idempotent component |
| 22 | +implementation. What this means is that they are responsible for |
| 23 | +exposing a public API that ensures that users can write components with |
| 24 | +stable elements even when their attributes change. Ember.js has an |
| 25 | +implementation, but it's fairly involved. |
| 26 | + |
| 27 | +## Hooks |
| 28 | + |
| 29 | +An HTMLBars environment exposes a series of hooks that a runtime |
| 30 | +environment can use to define the behavior of templates. These hooks |
| 31 | +are defined on the `env` passed into an HTMLBars `render` function, |
| 32 | +and are invoked by HTMLBars as the template's dynamic portions are |
| 33 | +reached. |
| 34 | + |
| 35 | +### The Scope Hooks |
| 36 | + |
| 37 | +Scope management: |
| 38 | + |
| 39 | +* `createFreshScope`: create a new, top-level scope. The default |
| 40 | + implementation of this hook creates a new scope with a `self` slot |
| 41 | + for the dynamic context and `locals`, a dictionary of local |
| 42 | + variables. |
| 43 | +* `createShadowScope`: create a new scope for a template that is |
| 44 | + being rendered in the middle of the render tree with a new, |
| 45 | + top-level scope (a "shadow root"). |
| 46 | +* `createChildScope`: create a new scope that inherits from the parent |
| 47 | + scope. The child scope must reflect updates to `self` or `locals` on |
| 48 | + the parent scope automatically, so the default implementation of this |
| 49 | + hook uses `Object.create` on both the scope object and the locals. |
| 50 | +* `bindSelf`: a fresh `self` value has been provided for the scope |
| 51 | +* `bindLocal`: a specific local variable has been provided for |
| 52 | + the scope (through block arguments). |
| 53 | + |
| 54 | +Scope lookup: |
| 55 | + |
| 56 | +* `getRoot`: get the reference for the first identifier in a path. By |
| 57 | + default, this first looks in `locals`, and then looks in `self`. |
| 58 | +* `getChild`: gets the reference for subsequent identifiers in a path. |
| 59 | +* `getValue`: get the JavaScript value from the reference provided |
| 60 | + by the final call to `getChild`. Ember.js uses this series of |
| 61 | + hooks to create stable streams for each reference that remain |
| 62 | + stable across renders. |
| 63 | + |
| 64 | +> All hooks other than `getValue` operate in terms of "references", |
| 65 | +> which are internal values that can be evaluated in order to get a |
| 66 | +> value that is suitable for use in user hooks. The default |
| 67 | +> implementation simply uses JavaScript values, making the |
| 68 | +> "references" simple pass-throughs. Ember.js uses stable "stream" |
| 69 | +> objects for references, and evaluates them on an as-needed basis. |
| 70 | +
|
| 71 | +### The Helper Hooks |
| 72 | + |
| 73 | +* `hasHelper`: does a helper exist for this name? |
| 74 | +* `lookupHelper`: provide a helper function for a given name |
| 75 | + |
| 76 | +### The Expression Hooks |
| 77 | + |
| 78 | +* `concat`: takes an array of references and returns a reference |
| 79 | + representing the result of concatenating them. |
| 80 | +* `subexpr`: takes a helper name, a list of positional parameters |
| 81 | + and a hash of named parameters (as references), and returns a |
| 82 | + reference that, when evaluated, produces the result of invoking the |
| 83 | + helper with those *evaluated* positional and named parameters. |
| 84 | + |
| 85 | +User helpers simply take positional and named parameters and return the |
| 86 | +result of doing some computation. They are intended to be "pure" |
| 87 | +functions, and are not provided with any other environment information, |
| 88 | +nor the DOM being built. As a result, they satisfy the idempotence |
| 89 | +requirement. |
| 90 | + |
| 91 | +Simple example: |
| 92 | + |
| 93 | +```hbs |
| 94 | +<p>{{upcase (format-person person)}}</p> |
| 95 | +``` |
| 96 | + |
| 97 | +```js |
| 98 | +helpers.upcase = function(params) { |
| 99 | + return params[0].toUpperCase(); |
| 100 | +}; |
| 101 | + |
| 102 | +helpers['format-person'] = function(params) { |
| 103 | + return person.salutation + '. ' + person.first + ' ' + person.last; |
| 104 | +}; |
| 105 | +``` |
| 106 | + |
| 107 | +The first time this template is rendered, the `subexpr` hook is invoked |
| 108 | +once for the `format-person` helper, and its result is provided to the |
| 109 | +`upcase` helper. The result of the `upcase` helper is then inserted into |
| 110 | +the DOM. |
| 111 | + |
| 112 | +The second time the template is rendered, the same hooks are called. |
| 113 | +HTMLBars compares the result value with the last value inserted into the |
| 114 | +DOM, and if they are the same, does nothing. |
| 115 | + |
| 116 | +Because HTMLBars is responsible for updating the DOM, and simply |
| 117 | +delegates to "pure helpers" to calculate the values to insert, it can |
| 118 | +guarantee idempotence. |
| 119 | + |
| 120 | +## Keywords |
| 121 | + |
| 122 | +HTMLBars allows a host environment to define *keywords*, which receive |
| 123 | +the full set of environment information (such as the current scope and a |
| 124 | +reference to the runtime) as well as all parameters as unevaluated |
| 125 | +references. |
| 126 | + |
| 127 | +Keywords can be used to implement low-level behaviors that control the |
| 128 | +DOM being built, but with great power comes with great responsibility. |
| 129 | +Since a keyword has the ability to influence the ambient environment and |
| 130 | +the DOM, it must maintain the idempotence invariant. |
| 131 | + |
| 132 | +To repeat, the idempotence requirement says that if a given template is |
| 133 | +executed multiple times with the same dynamic environment, it produces |
| 134 | +the same DOM. This means the exact same DOM nodes, with the same |
| 135 | +internal state. |
| 136 | + |
| 137 | +This is also true for all child templates. Consider this template: |
| 138 | + |
| 139 | +```hbs |
| 140 | +<h1>{{title}}</h1> |
| 141 | +
|
| 142 | +{{#if subtitle}} |
| 143 | + <h2>{{subtitle}}</h2> |
| 144 | +{{/if}} |
| 145 | +
|
| 146 | +<div>{{{body}}}</div> |
| 147 | +``` |
| 148 | + |
| 149 | +If this template is rendered first with a `self` that has a title, |
| 150 | +subtitle and body, and then rendered again with the same title and body |
| 151 | +but no subtitle, the second render will produce the same `<h1>` and same |
| 152 | +`<div>`, even though a part of the environment changes. |
| 153 | + |
| 154 | +The general goal is that for a given keyword, if all of the inputs to |
| 155 | +the keyword have stayed the same, the produced DOM will stay the same. |
| 156 | + |
| 157 | +## Lifecycle Example |
| 158 | + |
| 159 | +To implement an idempotent keyword, you need to understand the basic |
| 160 | +lifecycle of a render node. |
| 161 | + |
| 162 | +Consider this template: |
| 163 | + |
| 164 | +```js |
| 165 | +{{#if subtitle}} |
| 166 | + <h2>{{subtitle}}</h2> |
| 167 | +{{/if}} |
| 168 | +``` |
| 169 | + |
| 170 | +The first time this template is rendered, the `{{#if}}` block receives a |
| 171 | +fresh, empty render node. |
| 172 | + |
| 173 | +It evaluates `subtitle`, and if the value is truthy, yields to the |
| 174 | +block. HTMLBars creates the static parts of the template (the `<h2>`) |
| 175 | +and inserts them into the DOM). |
| 176 | + |
| 177 | +When it descends into the block, it creates a fresh, empty render node |
| 178 | +and evaluates `subtitle`. It then sets the value of the render node to |
| 179 | +the evaluated value. |
| 180 | + |
| 181 | +The second time the template is rendered, the `{{#if}}` block receives |
| 182 | +the same render node again. |
| 183 | + |
| 184 | +It evaluates `subtitle`, and if the value is truthy, yields to the |
| 185 | +block. HTMLBars sees that the same block as last time was yielded, and |
| 186 | +**does not** replace the static portions of the block. |
| 187 | + |
| 188 | +(If the value is falsy, it does not yield to the block. HTMLBars sees |
| 189 | +that the block was not yielded to, and prunes the DOM produced last |
| 190 | +time, and does not descend.) |
| 191 | + |
| 192 | +It descends into the previous block, and repeats the process. It fetches |
| 193 | +the previous render node, instead of creating a fresh one, and evaluates |
| 194 | +`subtitle`. |
| 195 | + |
| 196 | +If the value of `subtitle` is the same as the last value of `subtitle`, |
| 197 | +nothing happens. If the value of `subtitle` has changed, the render node |
| 198 | +is updated with the new value. |
| 199 | + |
| 200 | +This example shows how HTMLBars itself guarantees idempotence. The |
| 201 | +easiest way for a keyword to satisfy these requirements are to implement |
| 202 | +a series of functions, as the next section will describe. |
| 203 | + |
| 204 | +## Lifecycle More Precisely |
| 205 | + |
| 206 | +```js |
| 207 | +export default { |
| 208 | + willRender: function(node, env) { |
| 209 | + // This function is always invoked before any other hooks, |
| 210 | + // giving the keyword an opportunity to coordinate with |
| 211 | + // the external environment regardless of whether this is |
| 212 | + // the first or subsequent render, and regardless of |
| 213 | + // stability. |
| 214 | + }, |
| 215 | + |
| 216 | + setupState: function(state, env, scope, params, hash) { |
| 217 | + // This function is invoked before `isStable` so that it can update any |
| 218 | + // internal state based on external changes. |
| 219 | + }, |
| 220 | + |
| 221 | + isEmpty: function(state, env, scope, params, hash) { |
| 222 | + // If `isStable` returns false, or this is the first render, |
| 223 | + // this function can return true to indicate that the morph |
| 224 | + // should be empty (and `render` should not be called). |
| 225 | + } |
| 226 | + |
| 227 | + isPaused: function(state, env, scope, params, hash) { |
| 228 | + // This function is invoked on renders after the first render; if |
| 229 | + // it returns true, the entire subtree is assumed valid, and dirty |
| 230 | + // checking does not continue. This is useful during animations, |
| 231 | + // and in some cases, as a performance optimization. |
| 232 | + }, |
| 233 | + |
| 234 | + isStable: function(state, env, scope, params, hash) { |
| 235 | + // This function is invoked after the first render; it checks to see |
| 236 | + // whether the node is "stable". If the node is unstable, its |
| 237 | + // existing content will be removed and the `render` function is |
| 238 | + // called again to produce new values. |
| 239 | + }, |
| 240 | + |
| 241 | + rerender: function(morph, env, scope, params, hash, template, inverse |
| 242 | +visitor) { |
| 243 | + // This function is invoked if the `isStable` check returns true. |
| 244 | + // Occasionally, you may have a bit of work to do when a node is |
| 245 | + // stable even though you aren't tearing it down. |
| 246 | + }, |
| 247 | + |
| 248 | + render: function(node, env, scope, params, hash, template, inverse, visitor) { |
| 249 | + // This function is invoked on the first render, and any time the |
| 250 | + // isStable function returns false. |
| 251 | + } |
| 252 | +} |
| 253 | +``` |
| 254 | + |
| 255 | +For any given render, a keyword can end up in one of these states: |
| 256 | + |
| 257 | +* **initial**: this is the first render for a given render node |
| 258 | +* **stable**: the DOM subtree represented by the render node do not |
| 259 | + need to change; continue revalidating child nodes |
| 260 | +* **unstable**: the DOM subtree represented by the render node is no |
| 261 | + longer valid; do a new initial render and replace the subtree |
| 262 | +* **prune**: remove the DOM subtree represented by the render node |
| 263 | +* **paused**: do not make any changes to this node or the DOM subtree |
| 264 | + |
| 265 | +It is the keyword's responsibility to ensure that a node whose direct |
| 266 | +inputs have not changed remains **stable**. This does not mean that no |
| 267 | +descendant node will not be replaced, but only the precise nodes that |
| 268 | +have changed will be updated. |
| 269 | + |
| 270 | +Note that these details should generally **not** be exposed to the user |
| 271 | +code that interacts with the keyword. Instead, the user code should |
| 272 | +generally take in inputs and produce outputs, and the keyword should use |
| 273 | +those outputs to determine whether the associated render node is stable |
| 274 | +or not. |
| 275 | + |
| 276 | +Ember `{{outlet}}`s are a good example of this. The internal |
| 277 | +implementation of `{{outlet}}` is careful to avoid replacing any nodes |
| 278 | +if the current route has not changed, but the user thinks in terms of |
| 279 | +transitioning to a new route and rendering anew. |
| 280 | + |
| 281 | +If the transition was to the same page (with a different model, say), |
| 282 | +the `{{outlet}}` keyword will make sure to consider the render node |
| 283 | +stable. |
| 284 | + |
| 285 | +From the user's perspective, the transition always results in a complete |
| 286 | +re-render, but the keyword is responsible for maintaining the |
| 287 | +idempotence invariant when appropriate. |
| 288 | + |
| 289 | +This also means that it's possible to precisely describe what |
| 290 | +idempotence guarantees exist. HTMLBars defines the guarantees for |
| 291 | +built-in constructs (including invoked user helpers), and each keyword |
| 292 | +defines the guarantees for the keyword. Since those are the only |
| 293 | +constructs that can directly manipulate the lexical environment or the |
| 294 | +DOM, that's all you need to know! |
0 commit comments