1 | import { DEBUG } from '@glimmer/env';
|
2 | import { setComponentManager } from '@ember/component';
|
3 | import GlimmerComponentManager from './-private/ember-component-manager';
|
4 | import _GlimmerComponent, { type Args } from './-private/component';
|
5 | import { setOwner, type default as Owner } from '@ember/owner';
|
6 |
|
7 | /**
|
8 | A component is a reusable UI element that consists of a `.hbs` template and an
|
9 | optional JavaScript class that defines its behavior. For example, someone
|
10 | might make a `button` in the template and handle the click behavior in the
|
11 | JavaScript file that shares the same name as the template.
|
12 |
|
13 | Components are broken down into two categories:
|
14 |
|
15 | - Components _without_ JavaScript, that are based only on a template. These
|
16 | are called Template-only or TO components.
|
17 | - Components _with_ JavaScript, which consist of a template and a backing
|
18 | class.
|
19 |
|
20 | Ember ships with two types of JavaScript classes for components:
|
21 |
|
22 | 1. Glimmer components, imported from `@glimmer/component`, which are the
|
23 | default components for Ember Octane (3.15) and more recent editions.
|
24 | 2. Classic components, imported from `@ember/component`, which were the
|
25 | default for older editions of Ember (pre 3.15).
|
26 |
|
27 | Below is the documentation for Template-only and Glimmer components. If you
|
28 | are looking for the API documentation for Classic components, it is
|
29 | [available here](/ember/release/classes/Component). The source code for
|
30 | Glimmer components can be found in [`@glimmer/component`](https://github.com/glimmerjs/glimmer.js/tree/master/packages/%40glimmer/component).
|
31 |
|
32 | ## Defining a Template-only Component
|
33 |
|
34 | The simplest way to create a component is to create a template file in
|
35 | `app/templates/components`. For example, if you name a template
|
36 | `app/templates/components/person-profile.hbs`:
|
37 |
|
38 | ```app/templates/components/person-profile.hbs
|
39 | <h1>{{@person.name}}</h1>
|
40 | <img src={{@person.avatar}}>
|
41 | <p class='signature'>{{@person.signature}}</p>
|
42 | ```
|
43 |
|
44 | You will be able to use `<PersonProfile />` to invoke this component elsewhere
|
45 | in your application:
|
46 |
|
47 | ```app/templates/application.hbs
|
48 | <PersonProfile @person={{this.currentUser}} />
|
49 | ```
|
50 |
|
51 | Note that component names are capitalized here in order to distinguish them
|
52 | from regular HTML elements, but they are dasherized in the file system.
|
53 |
|
54 | While the angle bracket invocation form is generally preferred, it is also
|
55 | possible to invoke the same component with the `{{person-profile}}` syntax:
|
56 |
|
57 | ```app/templates/application.hbs
|
58 | {{person-profile person=this.currentUser}}
|
59 | ```
|
60 |
|
61 | Note that with this syntax, you use dashes in the component name and
|
62 | arguments are passed without the `@` sign.
|
63 |
|
64 | In both cases, Ember will render the content of the component template we
|
65 | created above. The end result will be something like this:
|
66 |
|
67 | ```html
|
68 | <h1>Tomster</h1>
|
69 | <img src="https://emberjs.com/tomster.jpg">
|
70 | <p class='signature'>Out of office this week</p>
|
71 | ```
|
72 |
|
73 | ## File System Nesting
|
74 |
|
75 | Components can be nested inside sub-folders for logical groupping. For
|
76 | example, if we placed our template in
|
77 | `app/templates/components/person/short-profile.hbs`, we can invoke it as
|
78 | `<Person::ShortProfile />`:
|
79 |
|
80 | ```app/templates/application.hbs
|
81 | <Person::ShortProfile @person={{this.currentUser}} />
|
82 | ```
|
83 |
|
84 | Or equivalently, `{{person/short-profile}}`:
|
85 |
|
86 | ```app/templates/application.hbs
|
87 | {{person/short-profile person=this.currentUser}}
|
88 | ```
|
89 |
|
90 | ## Using Blocks
|
91 |
|
92 | You can use `yield` inside a template to include the **contents** of any block
|
93 | attached to the component. For instance, if we added a `{{yield}}` to our
|
94 | component like so:
|
95 |
|
96 | ```app/templates/components/person-profile.hbs
|
97 | <h1>{{@person.name}}</h1>
|
98 | {{yield}}
|
99 | ```
|
100 |
|
101 | We could then invoke it like this:
|
102 |
|
103 | ```handlebars
|
104 | <PersonProfile @person={{this.currentUser}}>
|
105 | <p>Admin mode</p>
|
106 | </PersonProfile>
|
107 | ```
|
108 |
|
109 | or with curly syntax like this:
|
110 |
|
111 | ```handlebars
|
112 | {{#person-profile person=this.currentUser}}
|
113 | <p>Admin mode</p>
|
114 | {{/person-profile}}
|
115 | ```
|
116 |
|
117 | And the content passed in between the brackets of the component would be
|
118 | rendered in the same place as the `{{yield}}` within it, replacing it.
|
119 |
|
120 | Blocks are executed in their original context, meaning they have access to the
|
121 | scope and any in-scope variables where they were defined.
|
122 |
|
123 | ### Passing parameters to blocks
|
124 |
|
125 | You can also pass positional parameters to `{{yield}}`, which are then made
|
126 | available in the block:
|
127 |
|
128 | ```app/templates/components/person-profile.hbs
|
129 | <h1>{{@person.name}}</h1>
|
130 | {{yield @person.signature}}
|
131 | ```
|
132 |
|
133 | We can then use this value in the block like so:
|
134 |
|
135 | ```handlebars
|
136 | <PersonProfile @person={{this.currentUser}} as |signature|>
|
137 | {{signature}}
|
138 | </PersonProfile>
|
139 | ```
|
140 |
|
141 | ### Passing multiple blocks
|
142 |
|
143 | You can pass multiple blocks to a component by giving them names, and
|
144 | specifying which block you are yielding to with `{{yield}}`. For instance, if
|
145 | we wanted to add a way for users to customize the title of our
|
146 | `<PersonProfile>` component, we could add a named block inside of the header:
|
147 |
|
148 | ```app/templates/components/person-profile.hbs
|
149 | <h1>{{yield to="title"}}</h1>
|
150 | {{yield}}
|
151 | ```
|
152 |
|
153 | This component could then be invoked like so:
|
154 |
|
155 | ```handlebars
|
156 | <PersonProfile @person={{this.currentUser}}>
|
157 | <:title>{{this.currentUser.name}}</:title>
|
158 | <:default>{{this.currentUser.signature}}</:default>
|
159 | </PersonProfile>
|
160 | ```
|
161 |
|
162 | When passing named blocks, you must name every block, including the `default`
|
163 | block, which is the block that is defined if you do not pass a `to` parameter
|
164 | to `{{yield}}`. Whenever you invoke a component without passing explicitly
|
165 | named blocks, the passed block is considered the `default` block.
|
166 |
|
167 | ### Passing parameters to named blocks
|
168 |
|
169 | You can also pass parameters to named blocks:
|
170 |
|
171 | ```app/templates/components/person-profile.hbs
|
172 | <h1>{{yield @person.name to="title"}}</h1>
|
173 | {{yield @person.signature}}
|
174 | ```
|
175 |
|
176 | These parameters can then be used like so:
|
177 |
|
178 | ```handlebars
|
179 | <PersonProfile @person={{this.currentUser}}>
|
180 | <:title as |name|>{{name}}</:title>
|
181 | <:default as |signature|>{{signature}}</:default>
|
182 | </PersonProfile>
|
183 | ```
|
184 |
|
185 | ### Checking to see if a block exists
|
186 |
|
187 | You can also check to see if a block exists using the `(has-block)` keyword,
|
188 | and conditionally use it, or provide a default template instead.
|
189 |
|
190 | ```app/templates/components/person-profile.hbs
|
191 | <h1>
|
192 | {{#if (has-block "title")}}
|
193 | {{yield @person.name to="title"}}
|
194 | {{else}}
|
195 | {{@person.name}}
|
196 | {{/if}}
|
197 | </h1>
|
198 |
|
199 | {{#if (has-block)}}
|
200 | {{yield @person.signature}}
|
201 | {{else}}
|
202 | {{@person.signature}}
|
203 | {{/if}}
|
204 | ```
|
205 |
|
206 | With this template, we can then optionally pass in one block, both blocks, or
|
207 | none at all:
|
208 |
|
209 | ```handlebars
|
210 | {{! passing both blocks }}
|
211 | <PersonProfile @person={{this.currentUser}}>
|
212 | <:title as |name|>{{name}}</:title>
|
213 | <:default as |signature|>{{signature}}</:default>
|
214 | </PersonProfile>
|
215 |
|
216 | {{! passing just the title block }}
|
217 | <PersonProfile @person={{this.currentUser}}>
|
218 | <:title as |name|>{{name}}</:title>
|
219 | </PersonProfile>
|
220 |
|
221 | {{! passing just the default block }}
|
222 | <PersonProfile @person={{this.currentUser}} as |signature|>
|
223 | {{signature}}
|
224 | </PersonProfile>
|
225 |
|
226 | {{! not passing any blocks }}
|
227 | <PersonProfile @person={{this.currentUser}}/>
|
228 | ```
|
229 |
|
230 | ### Checking to see if a block has parameters
|
231 |
|
232 | We can also check if a block receives parameters using the `(has-block-params)`
|
233 | keyword, and conditionally yield different values if so.
|
234 |
|
235 | ```app/templates/components/person-profile.hbs
|
236 | {{#if (has-block-params)}}
|
237 | {{yield @person.signature}}
|
238 | {{else}}
|
239 | {{yield}}
|
240 | {{/if}}
|
241 | ```
|
242 |
|
243 | ## Customizing Components With JavaScript
|
244 |
|
245 | To add JavaScript to a component, create a JavaScript file in the same
|
246 | location as the template file, with the same name, and export a subclass
|
247 | of `Component` as the default value. For example, to add Javascript to the
|
248 | `PersonProfile` component which we defined above, we would create
|
249 | `app/components/person-profile.js` and export our class as the default, like
|
250 | so:
|
251 |
|
252 | ```app/components/person-profile.js
|
253 | import Component from '@glimmer/component';
|
254 |
|
255 | export default class PersonProfileComponent extends Component {
|
256 | get displayName() {
|
257 | let { title, firstName, lastName } = this.args.person;
|
258 |
|
259 | if (title) {
|
260 | return `${title} ${lastName}`;
|
261 | } else {
|
262 | return `${firstName} ${lastName}`;
|
263 | }
|
264 | })
|
265 | }
|
266 | ```
|
267 |
|
268 | You can add your own properties, methods, and lifecycle hooks to this
|
269 | subclass to customize its behavior, and you can reference the instance of the
|
270 | class in your template using `{{this}}`. For instance, we could access the
|
271 | `displayName` property of our `PersonProfile` component instance in the
|
272 | template like this:
|
273 |
|
274 | ```app/templates/components/person-profile.hbs
|
275 | <h1>{{this.displayName}}</h1>
|
276 | {{yield}}
|
277 | ```
|
278 |
|
279 | ## `constructor`
|
280 |
|
281 | params: `owner` object and `args` object
|
282 |
|
283 | Constructs a new component and assigns itself the passed properties. The
|
284 | constructor is run whenever a new instance of the component is created, and
|
285 | can be used to setup the initial state of the component.
|
286 |
|
287 | ```javascript
|
288 | import Component from '@glimmer/component';
|
289 |
|
290 | export default class SomeComponent extends Component {
|
291 | constructor(owner, args) {
|
292 | super(owner, args);
|
293 |
|
294 | if (this.args.displayMode === 'list') {
|
295 | this.items = [];
|
296 | }
|
297 | }
|
298 | }
|
299 | ```
|
300 |
|
301 | Service injections and arguments are available in the constructor.
|
302 |
|
303 | ```javascript
|
304 | import Component from '@glimmer/component';
|
305 | import { service } from '@ember/service';
|
306 |
|
307 | export default class SomeComponent extends Component {
|
308 | @service myAnimations;
|
309 |
|
310 | constructor(owner, args) {
|
311 | super(owner, args);
|
312 |
|
313 | if (this.args.fadeIn === true) {
|
314 | this.myAnimations.register(this, 'fade-in');
|
315 | }
|
316 | }
|
317 | }
|
318 | ```
|
319 |
|
320 | ## `willDestroy`
|
321 |
|
322 | `willDestroy` is called after the component has been removed from the DOM, but
|
323 | before the component is fully destroyed. This lifecycle hook can be used to
|
324 | cleanup the component and any related state.
|
325 |
|
326 | ```javascript
|
327 | import Component from '@glimmer/component';
|
328 | import { service } from '@ember/service';
|
329 |
|
330 | export default class SomeComponent extends Component {
|
331 | @service myAnimations;
|
332 |
|
333 | willDestroy() {
|
334 | super.willDestroy(...arguments);
|
335 |
|
336 | this.myAnimations.unregister(this);
|
337 | }
|
338 | }
|
339 | ```
|
340 |
|
341 | ## `args`
|
342 |
|
343 | The `args` property of Glimmer components is an object that contains the
|
344 | _arguments_ that are passed to the component. For instance, the
|
345 | following component usage:
|
346 |
|
347 | ```handlebars
|
348 | <SomeComponent @fadeIn={{true}} />
|
349 | ```
|
350 |
|
351 | Would result in the following `args` object to be passed to the component:
|
352 |
|
353 | ```javascript
|
354 | { fadeIn: true }
|
355 | ```
|
356 |
|
357 | `args` can be accessed at any point in the component lifecycle, including
|
358 | `constructor` and `willDestroy`. They are also automatically marked as tracked
|
359 | properties, and they can be depended on as computed property dependencies:
|
360 |
|
361 | ```javascript
|
362 | import Component from '@glimmer/component';
|
363 | import { computed } from '@ember/object';
|
364 |
|
365 | export default class SomeComponent extends Component {
|
366 |
|
367 | @computed('args.someValue')
|
368 | get computedGetter() {
|
369 | // updates whenever args.someValue updates
|
370 | return this.args.someValue;
|
371 | }
|
372 |
|
373 | get standardGetter() {
|
374 | // updates whenever args.anotherValue updates (Ember 3.13+)
|
375 | return this.args.anotherValue;
|
376 | }
|
377 | }
|
378 | ```
|
379 |
|
380 | ## `isDestroying`
|
381 |
|
382 | A boolean flag to tell if the component is in the process of destroying. This is set to
|
383 | true before `willDestroy` is called.
|
384 |
|
385 | ## `isDestroyed`
|
386 | A boolean to tell if the component has been fully destroyed. This is set to true
|
387 | after `willDestroy` is called.
|
388 |
|
389 | @module @glimmer/component
|
390 | @public
|
391 | */
|
392 | export default class GlimmerComponent<S = unknown> extends _GlimmerComponent<S> {
|
393 | constructor(owner: Owner, args: Args<S>) {
|
394 | super(owner, args);
|
395 |
|
396 | if (DEBUG && !(owner !== null && typeof owner === 'object')) {
|
397 | throw new Error(
|
398 | `You must pass both the owner and args to super() in your component: ${this.constructor.name}. You can pass them directly, or use ...arguments to pass all arguments through.`
|
399 | );
|
400 | }
|
401 |
|
402 | setOwner(this, owner);
|
403 | }
|
404 | }
|
405 |
|
406 | setComponentManager((owner: Owner) => {
|
407 | return new GlimmerComponentManager(owner);
|
408 | }, GlimmerComponent);
|