1. Introduction
Composants Web
Les composants Web sont un ensemble de normes Web qui permettent aux développeurs d'étendre le langage HTML avec des éléments personnalisés. Dans cet atelier de programmation, vous allez définir l'élément <brick-viewer>, qui pourra afficher des modèles de briques.
lit-element
Pour nous aider à définir notre élément personnalisé <brick-viewer>, nous allons utiliser lit-element. lit-element est une classe de base légère qui ajoute du sucre syntaxique à la norme des composants Web. Cela nous permettra de nous familiariser facilement avec notre élément personnalisé.
Premiers pas
Nous allons coder dans un environnement Stackblitz en ligne. Ouvrez donc ce lien dans une nouvelle fenêtre :
stackblitz.com/edit/brick-viewer
C'est parti !
2. Définir un élément personnalisé
Définition de la classe
Pour définir un élément personnalisé, créez une classe qui étend LitElement et décorez-la avec @customElement. L'argument de @customElement sera le nom de l'élément personnalisé.
Dans brick-viewer.ts, saisissez :
@customElement('brick-viewer')
export class BrickViewer extends LitElement {
}
L'élément <brick-viewer></brick-viewer> est maintenant prêt à être utilisé dans HTML. Mais si vous l'essayez, rien ne s'affichera. Nous allons résoudre ce problème.
Méthode de rendu
Pour implémenter la vue du composant, définissez une méthode nommée "render". Cette méthode doit renvoyer un littéral de modèle balisé avec la fonction html. Placez le code HTML de votre choix dans le littéral du modèle tagué. Il s'affichera lorsque vous utiliserez <brick-viewer>.
Ajoutez la méthode render :
export class BrickViewer extends LitElement {
render() {
return html`<div>Brick viewer</div>`;
}
}
3. Spécifier le fichier LDraw
Définir une propriété
Il serait idéal qu'un utilisateur de <brick-viewer> puisse spécifier le modèle de brique à afficher à l'aide d'un attribut, comme ceci :
<brick-viewer src="path/to/model.ldraw"></brick-viewer>
Comme nous créons un élément HTML, nous pouvons tirer parti de l'API déclarative et définir un attribut source, comme pour une balise <img> ou <video>. Avec lit-element, il suffit de décorer une propriété de classe avec @property. L'option type vous permet de spécifier comment lit-element analyse la propriété pour l'utiliser en tant qu'attribut HTML.
Définissez la propriété et l'attribut src :
export class BrickViewer extends LitElement {
@property({type: String})
src: string|null = null;
}
<brick-viewer> dispose désormais d'un attribut src que nous pouvons définir en HTML. Sa valeur est déjà lisible depuis notre classe BrickViewer grâce à lit-element.
Afficher les valeurs
Nous pouvons afficher la valeur de l'attribut src en l'utilisant dans le littéral du modèle de la méthode de rendu. Interpolez des valeurs dans des littéraux de modèle à l'aide de la syntaxe ${value}.
export class BrickViewer extends LitElement {
render() {
return html`<div>Brick model: ${this.src}</div>`;
}
}
La valeur de l'attribut src de l'élément <brick-viewer> s'affiche dans la fenêtre. Essayez d'ouvrir les outils pour les développeurs de votre navigateur et de modifier manuellement l'attribut src. Allez-y, essayez !
…Avez-vous remarqué que le texte de l'élément se met à jour automatiquement ? lit-element observe les propriétés de classe décorées avec @property et affiche à nouveau la vue pour vous. lit-element fait le gros du travail pour vous.
4. Définir la scène avec Three.js
Lumière, caméra, rendu !
Notre élément personnalisé utilisera three.js pour afficher nos modèles de briques 3D. Il y a certaines choses que nous voulons faire une seule fois pour chaque instance d'un élément <brick-viewer>, comme configurer la scène, la caméra et l'éclairage three.js. Nous les ajouterons au constructeur de la classe BrickViewer. Nous conserverons certains objets en tant que propriétés de classe afin de pouvoir les utiliser ultérieurement : caméra, scène, commandes et moteur de rendu.
Ajoutez la configuration de la scène three.js :
export class BrickViewer extends LitElement {
private _camera: THREE.PerspectiveCamera;
private _scene: THREE.Scene;
private _controls: OrbitControls;
private _renderer: THREE.WebGLRenderer;
constructor() {
super();
this._camera = new THREE.PerspectiveCamera(45, this.clientWidth/this.clientHeight, 1, 10000);
this._camera.position.set(150, 200, 250);
this._scene = new THREE.Scene();
this._scene.background = new THREE.Color(0xdeebed);
const ambientLight = new THREE.AmbientLight(0xdeebed, 0.4);
this._scene.add( ambientLight );
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(-1000, 1200, 1500);
this._scene.add(directionalLight);
this._renderer = new THREE.WebGLRenderer({antialias: true});
this._renderer.setPixelRatio(window.devicePixelRatio);
this._renderer.setSize(this.offsetWidth, this.offsetHeight);
this._controls = new OrbitControls(this._camera, this._renderer.domElement);
this._controls.addEventListener("change", () =>
requestAnimationFrame(this._animate)
);
this._animate();
const resizeObserver = new ResizeObserver(this._onResize);
resizeObserver.observe(this);
}
private _onResize = (entries: ResizeObserverEntry[]) => {
const { width, height } = entries[0].contentRect;
this._renderer.setSize(width, height);
this._camera.aspect = width / height;
this._camera.updateProjectionMatrix();
requestAnimationFrame(this._animate);
};
private _animate = () => {
this._renderer.render(this._scene, this._camera);
};
}
L'objet WebGLRenderer fournit un élément DOM qui affiche la scène three.js rendue. Il est accessible via la propriété domElement. Nous pouvons interpoler cette valeur dans le littéral du modèle de rendu à l'aide de la syntaxe ${value}.
Supprimez le message src que nous avions dans le modèle et insérez l'élément DOM du moteur de rendu :
export class BrickViewer extends LitElement {
render() {
return html`
${this._renderer.domElement}
`;
}
}
Pour que l'élément DOM du moteur de rendu s'affiche dans son intégralité, nous devons également définir l'élément <brick-viewer> lui-même sur display: block. Nous pouvons fournir des styles dans une propriété statique appelée styles, définie sur un littéral de modèle css.
Ajoutez ce style à la classe :
export class BrickViewer extends LitElement {
static styles = css`
/* The :host selector styles the brick-viewer itself! */
:host {
display: block;
}
`;
}
<brick-viewer> affiche désormais une scène Three.js rendue :

Mais… elle est vide. Fournissons-lui un modèle.
Chargeur de briques
Nous transmettrons la propriété src que nous avons définie précédemment à LDrawLoader, qui est fourni avec three.js.
Les fichiers LDraw peuvent séparer un modèle de briques en étapes de construction distinctes. Le nombre total d'étapes et la visibilité de chaque brique sont accessibles via l'API LDrawLoader.
Copiez ces propriétés, la nouvelle méthode _loadModel et la nouvelle ligne dans le constructeur :
@customElement('brick-viewer')
export class BrickViewer extends LitElement {
private _loader = new LDrawLoader();
private _model: any;
private _numConstructionSteps?: number;
step?: number;
constructor() {
// ...
// Add this line right before this._animate();
(this._loader as any).separateObjects = true;
this._animate();
}
private _loadModel() {
if (this.src === null) {
return;
}
this._loader
.setPath('')
// Using our src property!
.load(this.src, (newModel) => {
if (this._model !== undefined) {
this._scene.remove(this._model);
this._model = undefined;
}
this._model = newModel;
// Convert from LDraw coordinates: rotate 180 degrees around OX
this._model.rotation.x = Math.PI;
this._scene.add(this._model);
this._numConstructionSteps = this._model.userData.numConstructionSteps;
this.step = this._numConstructionSteps!;
const bbox = new THREE.Box3().setFromObject(this._model);
this._controls.target.copy(bbox.getCenter(new THREE.Vector3()));
this._controls.update();
this._controls.saveState();
});
}
}
Quand _loadModel doit-il être appelé ? Il doit être appelé chaque fois que l'attribut src change. En décorant la propriété src avec @property, nous avons activé la propriété dans le cycle de vie de mise à jour lit-element. Chaque fois que la valeur de l'une de ces propriétés décorées change, une série de méthodes sont appelées et peuvent accéder aux anciennes et nouvelles valeurs des propriétés. La méthode de cycle de vie qui nous intéresse s'appelle update. La méthode update accepte un argument PropertyValues, qui contient des informations sur les propriétés qui viennent d'être modifiées. C'est l'endroit idéal pour appeler _loadModel.
Ajoutez la méthode update :
export class BrickViewer extends LitElement {
update(changedProperties: PropertyValues) {
if (changedProperties.has('src')) {
this._loadModel();
}
super.update(changedProperties);
}
}
Notre élément <brick-viewer> peut désormais afficher un fichier brick, spécifié avec l'attribut src.

5. Afficher des modèles partiels
À présent, rendons l'étape de construction actuelle configurable. Nous aimerions pouvoir spécifier <brick-viewer step="5"></brick-viewer> et voir à quoi ressemble le modèle de briques à la cinquième étape de construction. Pour ce faire, transformons la propriété step en propriété observée en la décorant avec @property.
Décorez la propriété step :
export class BrickViewer extends LitElement {
@property({type: Number})
step?: number;
}
Nous allons maintenant ajouter une méthode d'assistance qui ne rend visibles que les briques jusqu'à l'étape de construction actuelle. Nous appellerons l'assistant dans la méthode de mise à jour afin qu'il s'exécute chaque fois que la propriété step est modifiée.
Mettez à jour la méthode update et ajoutez la nouvelle méthode _updateBricksVisibility :
export class BrickViewer extends LitElement {
update(changedProperties: PropertyValues) {
if (changedProperties.has('src')) {
this._loadModel();
}
if (changedProperties.has('step')) {
this._updateBricksVisibility();
}
super.update(changedProperties);
}
private _updateBricksVisibility() {
this._model && this._model.traverse((c: any) => {
if (c.isGroup && this.step) {
c.visible = c.userData.constructionStep <= this.step;
}
});
requestAnimationFrame(this._animate);
}
}
Ouvrez maintenant les outils de développement de votre navigateur et inspectez l'élément <brick-viewer>. Ajoutez-y un attribut step, comme ceci :

Regardez ce qui arrive au modèle rendu ! Nous pouvons utiliser l'attribut step pour contrôler la quantité de modèle affichée. Voici à quoi cela devrait ressembler lorsque l'attribut step est défini sur "10" :

6. Navigation dans les ensembles de briques
mwc-icon-button
L'utilisateur final de notre <brick-viewer> doit également pouvoir parcourir les étapes de compilation via l'UI. Ajoutons des boutons pour passer à l'étape suivante, à l'étape précédente et à la première étape. Pour faciliter la tâche, nous allons utiliser le composant Web du bouton Material Design. Comme @material/mwc-icon-button est déjà importé, nous sommes prêts à insérer <mwc-icon-button></mwc-icon-button>. Nous pouvons spécifier l'icône que nous souhaitons utiliser avec l'attribut "icon", comme ceci : <mwc-icon-button icon="thumb_up"></mwc-icon-button>. Vous trouverez toutes les icônes possibles sur material.io/resources/icons.
Ajoutons des boutons d'icône à la méthode de rendu :
export class BrickViewer extends LitElement {
render() {
return html`
${this._renderer.domElement}
<div id="controls">
<mwc-icon-button icon="replay"></mwc-icon-button>
<mwc-icon-button icon="navigate_before"></mwc-icon-button>
<mwc-icon-button icon="navigate_next"></mwc-icon-button>
</div>
`;
}
}
Grâce aux composants Web, il est très facile d'utiliser Material Design sur notre page.
Liaisons d'événements
Ces boutons devraient réellement faire quelque chose. Le bouton "Répondre" doit réinitialiser l'étape de construction sur 1. Le bouton "navigate_before" doit décrémenter l'étape de construction, et le bouton "navigate_next" doit l'incrémenter. lit-element facilite l'ajout de cette fonctionnalité grâce aux liaisons d'événements. Dans votre littéral de modèle HTML, utilisez la syntaxe @eventname=${eventHandler} comme attribut d'élément. eventHandler s'exécutera désormais lorsqu'un événement eventname sera détecté sur cet élément. Par exemple, ajoutons des gestionnaires d'événements de clic à nos trois boutons d'icône :
export class BrickViewer extends LitElement {
private _restart() {
this.step! = 1;
}
private _stepBack() {
this.step! -= 1;
}
private _stepForward() {
this.step! += 1;
}
render() {
return html`
${this._renderer.domElement}
<div id="controls">
<mwc-icon-button @click=${this._restart} icon="replay"></mwc-icon-button>
<mwc-icon-button @click=${this._stepBack} icon="navigate_before"></mwc-icon-button>
<mwc-icon-button @click=${this._stepForward} icon="navigate_next"></mwc-icon-button>
</div>
`;
}
}
Essayez de cliquer sur les boutons maintenant. Bravo !
Styles
Les boutons fonctionnent, mais ils ne sont pas esthétiques. Ils sont tous regroupés en bas. Appliquons-leur un style pour les superposer à la scène.
Pour appliquer des styles à ces boutons, nous revenons à la propriété static styles. Ces styles sont limités, ce qui signifie qu'ils ne s'appliqueront qu'aux éléments de ce composant Web. C'est l'un des avantages de l'écriture de composants Web : les sélecteurs peuvent être plus simples, et le CSS sera plus facile à lire et à écrire. Au revoir, BEM !
Mettez à jour les styles pour qu'ils ressemblent à ceci :
export class BrickViewer extends LitElement {
static styles = css`
:host {
display: block;
position: relative;
}
#controls {
position: absolute;
bottom: 0;
width: 100%;
display: flex;
}
`;
}

Bouton de réinitialisation de la caméra
Les utilisateurs finaux de notre <brick-viewer> peuvent faire pivoter la scène à l'aide des commandes de la souris. Puisque nous ajoutons des boutons, ajoutons-en un pour réinitialiser la position par défaut de la caméra. Un autre <mwc-icon-button> avec une liaison d'événement de clic fera l'affaire.
export class BrickViewer extends LitElement {
private _resetCamera() {
this._controls.reset();
}
render() {
return html`
<div id="controls">
<!-- ... -->
<!-- Add this button: -->
<mwc-icon-button @click=${this._resetCamera} icon="center_focus_strong"></mwc-icon-button>
</div>
`;
}
}
Consultation plus rapide
Certains ensembles de briques comportent de nombreuses étapes. Un utilisateur peut vouloir passer à une étape spécifique. L'ajout d'un curseur avec des numéros d'étape peut faciliter la navigation. Pour ce faire, nous utiliserons l'élément <mwc-slider>.
mwc-slider
L'élément de curseur a besoin de quelques données importantes, comme la valeur minimale et maximale du curseur. La valeur minimale du curseur peut toujours être définie sur "1". La valeur maximale du curseur doit être this._numConstructionSteps si le modèle est chargé. Nous pouvons indiquer à <mwc-slider> ces valeurs grâce à ses attributs. Nous pouvons également utiliser la directive ifDefined lit-html pour éviter de définir l'attribut max si la propriété _numConstructionSteps n'a pas été définie.
Ajoutez un <mwc-slider> entre les boutons "Retour" et "Suivant" :
export class BrickViewer extends LitElement {
render() {
return html`
<div id="controls">
<!-- ... backwards button -->
<!-- Add this slider: -->
<mwc-slider
step="1"
pin
markers
min="1"
max=${ifDefined(this._numConstructionSteps)}
></mwc-slider>
<!-- ... forwards button -->
</div>
`;
}
}
Données "à jour"
Lorsque l'utilisateur déplace le curseur, l'étape de construction actuelle doit changer et la visibilité du modèle doit être mise à jour en conséquence. L'élément de curseur émet un événement d'entrée chaque fois que le curseur est déplacé. Ajoutez une liaison d'événement sur le curseur lui-même pour capturer cet événement et modifier l'étape de construction.
Ajoutez la liaison d'événement :
export class BrickViewer extends LitElement {
render() {
return html`
<div id="controls">
<!-- ... -->
<!-- Add the @input event binding: -->
<mwc-slider
...
@input=${(e: CustomEvent) => this.step = e.detail.value}
></mwc-slider>
<!-- ... -->
</div>
`;
}
}
Bravo ! Nous pouvons utiliser le curseur pour modifier l'étape affichée.
Données "indisponibles"
Il y a encore une chose. Lorsque les boutons "Précédent" et "Suivant" sont utilisés pour changer d'étape, le curseur doit être mis à jour. Associez l'attribut de valeur de <mwc-slider> à this.step.
Ajoutez la liaison value :
export class BrickViewer extends LitElement {
render() {
return html`
<div id="controls">
<!-- ... -->
<!-- Add the value property binding: -->
<mwc-slider
...
value=${ifDefined(this.step)}
></mwc-slider>
<!-- ... -->
</div>
`;
}
}
Nous avons presque terminé le curseur. Ajoutez un style flex pour qu'il s'intègre bien aux autres commandes :
export class BrickViewer extends LitElement {
static styles = css`
/* ... */
mwc-slider {
flex-grow: 1;
}
`;
}
De plus, nous devons appeler layout sur l'élément de curseur lui-même. Nous allons le faire dans la méthode de cycle de vie firstUpdated, qui est appelée une fois que le DOM est mis en page pour la première fois. Le décorateur query peut nous aider à obtenir une référence à l'élément de curseur dans le modèle.
export class BrickViewer extends LitElement {
@query('mwc-slider')
slider!: Slider|null;
async firstUpdated() {
if (this.slider) {
await this.slider.updateComplete
this.slider.layout();
}
}
}
Voici tous les ajouts de curseur réunis (avec des attributs pin et markers supplémentaires sur le curseur pour le rendre plus attrayant) :
export class BrickViewer extends LitElement {
@query('mwc-slider')
slider!: Slider|null;
static styles = css`
/* ... */
mwc-slider {
flex-grow: 1;
}
`;
async firstUpdated() {
if (this.slider) {
await this.slider.updateComplete
this.slider.layout();
}
}
render() {
return html`
${this._renderer.domElement}
<div id="controls">
<mwc-icon-button @click=${this._restart} icon="replay"></mwc-icon-button>
<mwc-icon-button @click=${this._stepBack} icon="navigate_before"></mwc-icon-button>
<mwc-slider
step="1"
pin
markers
min="1"
max=${ifDefined(this._numConstructionSteps)}
?disabled=${this._numConstructionSteps === undefined}
value=${ifDefined(this.step)}
@input=${(e: CustomEvent) => this.constructionStep = e.detail.value}
></mwc-slider>
<mwc-icon-button @click=${this._stepForward} icon="navigate_next"></mwc-icon-button>
<mwc-icon-button @click=${this._resetCamera} icon="center_focus_strong"></mwc-icon-button>
</div>
`;
}
}
Voici le produit final !

7. Conclusion
Nous avons beaucoup appris sur l'utilisation de lit-element pour créer notre propre élément HTML. Nous avons appris à :
- Définir un élément personnalisé
- Déclarer une API d'attribut
- Afficher une vue pour un élément personnalisé
- Encapsuler les styles
- Utiliser des événements et des propriétés pour transmettre des données
Pour en savoir plus sur lit-element, consultez son site officiel.
Vous pouvez afficher un élément brick-viewer terminé sur stackblitz.com/edit/brick-viewer-complete.
brick-viewer est également disponible sur NPM. Vous pouvez consulter la source ici : dépôt GitHub.