1. Einführung
Webkomponenten
Webkomponenten sind eine Sammlung von Webstandards, mit denen Entwickler HTML mit benutzerdefinierten Elementen erweitern können. In diesem Codelab definieren Sie das <brick-viewer>
-Element, mit dem Steinmodelle angezeigt werden können.
lit-element
Zur Definition des benutzerdefinierten Elements <brick-viewer>
verwenden wir lit-element. lit-element ist eine einfache Basisklasse, die dem Webkomponenten-Standard etwas syntaktischen Zucker hinzufügt. Dies erleichtert uns den Einstieg in unser benutzerdefiniertes Element.
Jetzt starten
Wir werden in einer Online-Stackblitz-Umgebung programmieren. Öffnen Sie dazu diesen Link in einem neuen Fenster:
stackblitz.com/edit/brick-viewer
Los gehts!
2. Benutzerdefiniertes Element definieren
Klassendefinition
Wenn Sie ein benutzerdefiniertes Element definieren möchten, erstellen Sie eine Klasse, die LitElement
erweitert, und versehen Sie sie mit @customElement
. Das Argument für @customElement
ist der Name des benutzerdefinierten Elements.
Fügen Sie in „brick-viewer.ts“ Folgendes ein:
@customElement('brick-viewer')
export class BrickViewer extends LitElement {
}
Jetzt kann das <brick-viewer></brick-viewer>
-Element in HTML verwendet werden. Wenn Sie es jedoch versuchen, wird nichts gerendert. Das sollte nicht sein.
Renderingmethode
Um die Ansicht der Komponente zu implementieren, definieren Sie eine Methode mit dem Namen „render“. Diese Methode sollte ein Vorlagenliteral zurückgeben, das mit der Funktion html
getaggt ist. Geben Sie den gewünschten HTML-Code in das getaggte Vorlagenliteral ein. Dieser wird gerendert, wenn Sie <brick-viewer>
verwenden.
Fügen Sie die Methode render
hinzu:
export class BrickViewer extends LitElement {
render() {
return html`<div>Brick viewer</div>`;
}
}
3. LDraw-Datei angeben
Property definieren
Es wäre großartig, wenn ein Nutzer von <brick-viewer>
mithilfe eines Attributs angeben könnte, welches Ziegelmodell angezeigt werden soll:
<brick-viewer src="path/to/model.ldraw"></brick-viewer>
Da wir ein HTML-Element erstellen, können wir die deklarative API nutzen und ein Quellattribut definieren, genau wie ein <img>
- oder <video>
-Tag. Mit lit-element können Sie eine Klasseneigenschaft ganz einfach mit @property
ausstatten. Mit der Option type
können Sie angeben, wie die Property von lit-Elementen für die Verwendung als HTML-Attribut geparst wird.
Definieren Sie die Property und das Attribut src
:
export class BrickViewer extends LitElement {
@property({type: String})
src: string|null = null;
}
<brick-viewer>
hat jetzt ein src
-Attribut, das wir in HTML festlegen können. Sein Wert ist dank des Lit-Elements bereits innerhalb unserer BrickViewer
-Klasse lesbar.
Werte anzeigen
Wir können den Wert des Attributs src
anzeigen, indem wir es im Vorlagenliteral der Rendermethode verwenden. Werte mithilfe der ${value}
-Syntax in Vorlagenliterale einfügen
export class BrickViewer extends LitElement {
render() {
return html`<div>Brick model: ${this.src}</div>`;
}
}
Jetzt sehen wir im Fenster den Wert des Attributs „src“ im Element <brick-viewer>
. Öffnen Sie die Entwicklertools Ihres Browsers und ändern Sie das src-Attribut manuell. Probieren Sie es aus…
…Haben Sie bemerkt, dass sich der Text im Element automatisch aktualisiert? lit-element überwacht die mit @property
verzierten Klasseneigenschaften und rendert die Ansicht für Sie neu. lit-element erledigt die schwere Arbeit, damit Sie es nicht tun müssen.
4. Mit Three.js die Szene festlegen
Licht, Kamera, Rendering!
Unser benutzerdefiniertes Element verwendet 3.js, um unsere 3D-Bausteinmodelle zu rendern. Einige Dinge möchten wir nur einmal für jede Instanz eines <brick-viewer>
-Elements tun, z. B. die Three.js-Szene, die Kamera und die Beleuchtung einrichten. Wir fügen sie dem Konstruktor der Klasse „BrickViewer“ hinzu. Wir behalten einige Objekte als Klasseneigenschaften, damit wir sie später wiederverwenden können: Kamera, Szene, Steuerelemente und Renderer.
Fügen Sie die 3.js-Szeneneinrichtung hinzu:
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);
};
}
Das WebGLRenderer
-Objekt stellt ein DOM-Element bereit, in dem die gerenderte three.js-Szene angezeigt wird. Der Zugriff erfolgt über die Property domElement
. Wir können diesen Wert mithilfe der ${value}
-Syntax in das Render-Template-Literal einfügen.
Entfernen Sie die src
-Meldung aus der Vorlage und fügen Sie das DOM-Element des Renderers ein:
export class BrickViewer extends LitElement {
render() {
return html`
${this._renderer.domElement}
`;
}
}
Damit das dom-Element des Renderers vollständig angezeigt wird, müssen wir auch das <brick-viewer>
-Element selbst auf display: block
setzen. Wir können Stile in einer statischen Property namens styles
angeben, die auf ein css
-Vorlagenliteral festgelegt ist.
Fügen Sie der Klasse diesen Stil hinzu:
export class BrickViewer extends LitElement {
static styles = css`
/* The :host selector styles the brick-viewer itself! */
:host {
display: block;
}
`;
}
Jetzt zeigt <brick-viewer>
eine gerenderte Three.js-Szene an:
Aber… er ist leer. Wir stellen dafür ein Modell bereit.
Brick-Ladeprogramm
Wir übergeben die zuvor definierte Eigenschaft src
an den LDrawLoader, der mit three.js geliefert wird.
Mit LDraw-Dateien können Sie ein Ziegelmodell in separate Gebäudeschritte unterteilen. Die Gesamtzahl der Schritte und die Sichtbarkeit einzelner Steine sind über die LDrawLoader API zugänglich.
Kopieren Sie diese Eigenschaften, die neue Methode _loadModel
und die neue Zeile im Konstruktor:
@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();
});
}
}
Wann soll _loadModel
aufgerufen werden? Sie muss jedes Mal aufgerufen werden, wenn sich das src-Attribut ändert. Durch das Dekorieren der src
-Eigenschaft mit @property
haben wir die Eigenschaft für den Aktualisierungszyklus des Litigation-Elements aktiviert. Immer wenn sich der Wert einer dieser dekorierten Eigenschaften ändert, wird eine Reihe von Methoden aufgerufen, die auf die neuen und alten Werte der Eigenschaften zugreifen können. Die Lebenszyklusmethode, an der wir interessiert sind, heißt update
. Die Methode update
verwendet ein PropertyValues
-Argument, das Informationen zu allen gerade geänderten Attributen enthält. Hier können Sie _loadModel
anrufen.
Fügen Sie die Methode update
hinzu:
export class BrickViewer extends LitElement {
update(changedProperties: PropertyValues) {
if (changedProperties.has('src')) {
this._loadModel();
}
super.update(changedProperties);
}
}
Das Element <brick-viewer>
kann jetzt eine mit dem Attribut src
angegebene Brick-Datei anzeigen.
5. Teilmodelle anzeigen
Lassen Sie uns nun den aktuellen Konstruktionsschritt konfigurierbar machen. Wir möchten <brick-viewer step="5"></brick-viewer>
angeben können und sollten sehen, wie das Modell aus Legosteinen im fünften Bauschritt aussieht. Dazu machen wir die Eigenschaft step
zu einer beobachteten Eigenschaft, indem wir sie mit @property
dekorieren.
Dekorieren Sie das Attribut step
:
export class BrickViewer extends LitElement {
@property({type: Number})
step?: number;
}
Jetzt fügen wir eine Hilfsmethode hinzu, mit der nur die Blöcke bis zum aktuellen Bauschritt sichtbar sind. Wir rufen das Hilfsprogramm in der Aktualisierungsmethode auf, damit sie jedes Mal ausgeführt wird, wenn das Attribut step
geändert wird.
Aktualisieren Sie die update
-Methode und fügen Sie die neue _updateBricksVisibility
-Methode hinzu:
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);
}
}
Öffnen Sie jetzt die devtools Ihres Browsers und prüfen Sie das Element <brick-viewer>
. Fügen Sie dazu ein step
-Attribut hinzu:
Beobachten Sie, was mit dem gerenderten Modell geschieht. Mit dem Attribut step
können Sie festlegen, wie viel vom Modell angezeigt wird. So sollte es aussehen, wenn das step
-Attribut auf "10"
festgelegt ist:
6. Navigation im Set
mwc-icon-button
Der Endnutzer unseres <brick-viewer>
sollte auch in der Lage sein, die Build-Schritte über die Benutzeroberfläche aufzurufen. Fügen wir Schaltflächen zum Wechseln zum nächsten, vorherigen und ersten Schritt hinzu. Wir verwenden die Schaltflächen-Webkomponente von Material Design, um es einfacher zu machen. Da @material/mwc-icon-button
bereits importiert ist, können wir <mwc-icon-button></mwc-icon-button>
einfügen. Wir können das gewünschte Symbol mit dem Attribut „icon“ angeben, z. B. so: <mwc-icon-button icon="thumb_up"></mwc-icon-button>
. Alle möglichen Symbole finden Sie hier: material.io/resources/icons.
Fügen wir der Rendermethode einige Symbolschaltflächen hinzu:
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>
`;
}
}
Dank Webkomponenten ist es so einfach, Material Design auf unserer Seite zu verwenden.
Ereignisbindungen
Diese Schaltflächen sollten tatsächlich etwas tun. Über die Schaltfläche „Antworten“ sollte der Erstellungsschritt auf 1 zurückgesetzt werden. Die Schaltfläche „navigate_before“ sollte den Bauschritt verringern und die Schaltfläche „navigate_next“ sollte ihn erhöhen. Mithilfe von Ereignisbindungen lässt sich diese Funktion mit lit-element ganz einfach hinzufügen. Verwenden Sie in Ihrem HTML-Vorlagenliteral die Syntax @eventname=${eventHandler}
als Elementattribut. eventHandler
wird jetzt ausgeführt, wenn ein eventname
-Ereignis für dieses Element erkannt wird. Als Beispiel fügen wir unseren drei Symbolschaltflächen Click-Event-Handler hinzu:
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>
`;
}
}
Versuche jetzt, auf die Schaltflächen zu klicken. Sehr gut!
Stile
Die Schaltflächen funktionieren, sehen aber nicht gut aus. Sie sitzen alle unten. Passen wir sie an, damit sie über der Szene liegen.
Um Stile auf diese Schaltflächen anzuwenden, kehren wir zur Eigenschaft static styles
zurück. Da diese Stile auf einen Umfang beschränkt sind, können sie nur auf Elemente innerhalb dieser Webkomponente angewendet werden. Das ist einer der großen Freuden beim Schreiben von Webkomponenten: Selektoren können einfacher sein und CSS ist einfacher zu lesen und zu schreiben. Viele Grüße
BEM
Aktualisieren Sie die Stile so:
export class BrickViewer extends LitElement {
static styles = css`
:host {
display: block;
position: relative;
}
#controls {
position: absolute;
bottom: 0;
width: 100%;
display: flex;
}
`;
}
Taste zum Zurücksetzen der Kamera
Endnutzer unserer <brick-viewer>
können die Szene mit der Maus drehen. Während wir weitere Schaltflächen hinzufügen, füge ich eine hinzu, mit der die Kamera auf die Standardposition zurückgesetzt werden kann. Eine weitere <mwc-icon-button>
mit einer Klickereignisbindung reicht aus.
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>
`;
}
}
Schnellere Navigation
Einige Bausteine haben viele Stufen. Ein Nutzer möchte möglicherweise zu einem bestimmten Schritt springen. Ein Schieberegler mit Schrittnummern kann die Navigation erleichtern. Dazu verwenden wir das Element <mwc-slider>
.
MWC-Schieberegler
Für das Schiebereglerelement sind einige wichtige Daten erforderlich, z. B. der Mindest- und Höchstwert des Schiebereglers. Der Mindestwert des Schiebereglers kann immer „1“ sein. Der maximale Schiebereglerwert sollte this._numConstructionSteps
sein, wenn das Modell geladen wurde. Diese Werte können wir <mwc-slider>
anhand ihrer Attribute mitteilen. Wir können auch die ifDefined
-Anweisung „lit-html“ verwenden, um das Attribut max
nicht festzulegen, wenn die Eigenschaft _numConstructionSteps
nicht definiert wurde.
Fügen Sie zwischen den Schaltflächen „Zurück“ und „Weiter“ ein <mwc-slider>
ein:
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>
`;
}
}
Daten „nach oben“
Wenn ein Nutzer den Schieberegler bewegt, sollte sich der aktuelle Konstruktionsschritt ändern und die Sichtbarkeit des Modells entsprechend aktualisiert werden. Das Schiebereglerelement gibt jedes Mal ein Eingabeereignis aus, wenn der Schieberegler gezogen wird. Fügen Sie dem Schieberegler selbst eine Ereignisbindung hinzu, um dieses Ereignis zu erfassen und den Konstruktionsschritt zu ändern.
Fügen Sie die Ereignisbindung hinzu:
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>
`;
}
}
Sehr gut! Mit dem Schieberegler können Sie ändern, welcher Schritt angezeigt wird.
„Daten nicht verfügbar“
Es gibt noch etwas. Wenn Sie den Schritt über die Schaltflächen „Zurück“ und „Weiter“ ändern, muss der Schieberegler aktualisiert werden. Binden Sie das Attribut „Wert“ von <mwc-slider>
an this.step
.
Fügen Sie die value
-Bindung hinzu:
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>
`;
}
}
Wir sind fast fertig mit dem Schieberegler. Fügen Sie einen Flex-Stil hinzu, damit er gut zu den anderen Steuerelementen passt:
export class BrickViewer extends LitElement {
static styles = css`
/* ... */
mwc-slider {
flex-grow: 1;
}
`;
}
Außerdem müssen wir layout
auf das Schiebereglerelement selbst anwenden. Das geschieht in der Lebenszyklusmethode firstUpdated
, die aufgerufen wird, sobald das DOM zum ersten Mal dargestellt wurde. Mit dem query
-Decorator können wir einen Verweis auf das Schiebereglerelement in der Vorlage abrufen.
export class BrickViewer extends LitElement {
@query('mwc-slider')
slider!: Slider|null;
async firstUpdated() {
if (this.slider) {
await this.slider.updateComplete
this.slider.layout();
}
}
}
Hier sehen Sie alle Ergänzungen der Schieberegler zusammen (mit zusätzlichen pin
- und markers
-Attributen auf dem Schieberegler, damit der Schieberegler cooler wirkt):
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>
`;
}
}
Hier ist das Endprodukt.
7. Fazit
Wir haben viel darüber gelernt, wie wir mithilfe von Lit-Elementen unser eigenes HTML-Element erstellen können. Wir haben gelernt, wie man:
- Benutzerdefiniertes Element definieren
- Attribut-API deklarieren
- Ansicht für ein benutzerdefiniertes Element rendern
- Stile einschließen
- Daten mithilfe von Ereignissen und Properties übergeben
Weitere Informationen zu Licht-Elementen findest du auf der offiziellen Website.
Ein fertiges Brick-Viewer-Element finden Sie unter stackblitz.com/edit/brick-viewer-complete.
„brick-viewer“ ist auch über NPM verfügbar. Den Quellcode finden Sie hier: Github-Repository.