Un composant est une unité réutilisable. Il peut être inclus dans un template sous la forme d’un tag personnalisé (<MonComposant />
) pour s’occuper de la logique et contenu d’un bloc.
Un composant a, a minima, un template.
Créer un template dans components
app/components/hello-world.hbs:
<p>Hello World</p>
Utiliser le composant dans un autre template
app/templates/application.hbs:
{{page-title "Emberjs App"}}
<h1>Mon Application</h1>
<HelloWorld />
{{outlet}}
On peut soit invoquer un composant sous forme de tag, soit sous forme de helper. Les exemples ci-dessous sont équivalents:
<MyChild @childClickCount={{this.totalClicks}} />
{{my-child childClickCount=totalClicks}}
On peut mettre un composant dans un sous-répertoire de components
app/components/util/lorem.hbs:
<p>Lorem ipsum dolor sit amet</p>
Pour l’inclure, on utilise deux fois deux-points (::) comme séparateur:
<Util::Lorem />
Chaque composant peut définir sa propre logique, de la même manière qu’un controlleur (avec des variables, des actions et des variables suivies). La différence:
app/components/hello-world.js:
import Component from '@glimmer/component';
export default class HelloWorld extends Component {
username = "Bob";
}
app/components/hello-world.hbs:
<p>Hello {{username}}</p>
La propriété layout
peut être utilisée pour spécifier un template différent du template par défaut.
export default class HelloWorld extends Component {
layoutName = 'components/my-temp';
}
Le parent peut définir du contenu entre les balises du composant
app/templates/application.hbs:
<HelloWorld>
<h2>Lorem ipsum</h2>
</HelloWorld>
Et le composant peut afficher ce contenu en appelant yield
:
app/components/hello-world.hbs:
<h1>Hello World</h1>
{{yield}}
Le parent peut définir le contenu de différents emplacements comme suit:
app/templates/application.hbs:
<Popup>
<:title>
<h3>Header</h3>
</:title>
<:default>
<img src="//placehold.it/300x100" />
</:default>
</Popup>
Et le composant peut afficher un bloc nommé en appelant yield to="blocname"
app/components/popup.hbs:
<div class="popup">
<header class="popup-header">
{{yield to="title"}}
</header>
<article class="popup-body">
{{yield}}
</article>
</div>
On peut vérifier si un bloc a été définit par le parent ou non avec has-block
, ce qui peut permettre d’ajouter du contenu par défaut.
<div class="popup">
<header class="popup-header">
{{#if (has-block "title")}}
{{yield to="title"}}
{{else}}
Default title
{{/if}}
</header>
<article class="popup-body">
{{#if (has-block)}}
{{yield}}
{{else}}
Default content
{{/if}}
</article>
<footer id="popup-footer">
{{#if (has-block "footer")}}
{{yield to="footer"}}
{{else}}
Default footer
{{/if}}
</footer>
</div>
Le parent peut passer des attributs HTML:
app/templates/application.hbs:
<HelloWorld id="custom-id" />
Et le composant, peut ajouter ces attributs sur un élément avec ...attributes
<h1 ...attributes>Hello World</h1>
Le parent peut également passer des arguments:
<HelloWorld @username={{"Bob"}} />
Et le composant peut accéder aux arguments avec this.args
ou @:
app/components/hello-world.hbs:
<h1>Hello {{this.args.username}}</h1>
<h1>Hello {{@username}}</h1>
Dans le cas d’un composant sans instance de classe (il y a uniquement un template, pas de fichier .js) alors this
sera null: il faut nécessairement utiliser @.
Toutes les variables définies par le parent sont automatiquement suivies.
import Component from '@glimmer/component';
export default class ImageComponent extends Component {
get aspectRatio() {
return this.args.width / this.args.height;
}
}
constructor
et willDestroy
.did-insert
et will-destroy
du package ember-render-modifiersNativement, Ember définit deux composants: <Input>
et <Textarea>
.
<label for="input-name">Name:</label>
<Input
@id="input-name"
@value={{this.name}}
disabled={{this.isReadOnly}}
maxlength="50"
{{on "input" this.validateName}}
{{on-key "Enter" this.doSomething}}
{{on-key "Escape" this.doSomethingElse event="keydown"}}
/>
<label for="user-comment">Comment:</label>
<Textarea
@id="user-comment"
@value={{this.userComment}}
rows="6"
cols="80"
/>
Les composants présentés ci-dessus sont des composants Glimmer, importés de @glimmer/component
, qui sont les composants par défaut depuis Ember 3.15.
Auparavant, les composants par défaut étaient des composants Classic, importés depuis @ember/component
.
Les composants Classic disposent de lifecycle hooks, permettant d’executer du code à divers moments de la vie d’un composant, avec entre autres init
, didReceiveAttrs
, didRender
et willDestroyElement
.
Plus d’infos: The Component Lifecycle
Les composants Glimmer n’ont que deux lifecycle hooks: constructor
et willDestroy
. Les autres hooks n’ont pas équivalents, il est nécessaire d’utiliser des variables suivies à la place.
Lorsqu’on insère un composant Classic, celui-ci est automatiquement encapsulé par un élément racine (par défaut, il s’agit d’un div
).
<div id="ember180" class="ember-view">
<h1>My Component</h1>
</div>
Les attributs définis par le parent sont automatiquement appliqués sur cet élément.
<MyComponent id="custom-id" />
Pour modifier le type de l’élément racine, spécifier la propriété tagName
Si tagName est vide (tagName=''
), Ember utilisera le premier élément du template comme élément racine.
import Component from '@ember/component';
export default Component.extend({
tagName: 'nav'
});
Pour appliquer des classes sur l’élément racine, spécifier la propriété classNames
export default Component.extend({
classNames: ['primary']
});
Pour appliquer des classes dynamiques, spécifier la propriété classNameBindings
import { notEmpty, alias } from '@ember/object/computed';
export default Component.extend({
classNameBindings: [
'priority',
'hasWarning:has-warning',
'hasError:has-error',
'isEnabled:enabled:disabled',
],
priority: 'highestPriority',
hasWarning: notEmpty('warnings'),
hasError: notEmpty('errors'),
isEnabled: false,
init() {
this._super(...arguments);
this.setupBindings();
},
setupBindings() {
defineProperty(this, 'errors', alias('model.errors.' + this.model.property));
defineProperty(this, 'warnings', alias('model.warnings.' + this.model.property));
},
});
Pour appliquer des attributs sur l’élément racine, spécifier la propriété attributeBindings
export default Component.extend({
attrTitle: 'Ember JS',
attributeBindings: ['attrTitle:title'],
});
Les arguments définis par le parent sont affectés directement au composant, en écrasant les propriétés du composant — on y accède donc via this et non via this.args ou @.
Le parent:
<HelloWorld @username={{"Bob"}} />
Le composant (Classic):
app/components/hello-world.hbs:
<h1>Hello {{username}}</h1>
Les arguments d’un composant Classic sont des références: si le composant modifie la valeur de l’argument, alors la valeur est également modifiée dans le composant parent.