UNPKG

12.4 kBPlain TextView Raw
1import { DEBUG } from '@glimmer/env';
2import { setComponentManager } from '@ember/component';
3import GlimmerComponentManager from './-private/ember-component-manager';
4import _GlimmerComponent, { type Args } from './-private/component';
5import { 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*/
392export 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
406setComponentManager((owner: Owner) => {
407 return new GlimmerComponentManager(owner);
408}, GlimmerComponent);