1. Introduzione
Componenti web
I componenti web sono una raccolta di standard web che consentono agli sviluppatori di estendere l'HTML con elementi personalizzati. In questo codelab, definirai l'elemento <brick-viewer>
, che consente di visualizzare i modelli dei mattoncini.
elemento-lit
Per definire l'elemento personalizzato <brick-viewer>
, utilizzeremo lit-element. lit-element è una classe base leggera che aggiunge un po' di zucchero sintattico allo standard dei componenti web. In questo modo sarà più semplice iniziare a utilizzare il nostro elemento personalizzato.
Inizia
Faremo il codice in un ambiente Stackblitz online, quindi apri questo link in una nuova finestra:
stackblitz.com/edit/brick-viewer
Iniziamo.
2. Definisci un elemento personalizzato
Definizione del corso
Per definire un elemento personalizzato, crea una classe che amplia LitElement
e decoralo con @customElement
. L'argomento per @customElement
sarà il nome dell'elemento personalizzato.
In Brick-viewer.ts, metti:
@customElement('brick-viewer')
export class BrickViewer extends LitElement {
}
Ora l'elemento <brick-viewer></brick-viewer>
è pronto per essere utilizzato in HTML. Tuttavia, se provi, non verrà eseguito alcun rendering. Risolviamo il problema.
Metodo di rendering
Per implementare la vista del componente, definisci un metodo denominato rendering. Questo metodo dovrebbe restituire un valore letterale di modello con la funzione html
. Inserisci il codice HTML che desideri nel valore letterale del modello con tag. Verrà visualizzata quando usi <brick-viewer>
.
Aggiungi il metodo render
:
export class BrickViewer extends LitElement {
render() {
return html`<div>Brick viewer</div>`;
}
}
3. Specifica del file LDraw
Definisci una proprietà
Sarebbe bello se un utente dell'<brick-viewer>
potesse specificare il modello di mattoncino da mostrare utilizzando un attributo, ad esempio:
<brick-viewer src="path/to/model.ldraw"></brick-viewer>
Poiché stiamo creando un elemento HTML, possiamo sfruttare l'API dichiarativa e definire un attributo sorgente, proprio come un tag <img>
o <video>
. Con l'elemento lit-element, è facile come decorare una proprietà di classe con @property
. L'opzione type
consente di specificare in che modo lit-element analizza la proprietà per l'utilizzo come attributo HTML.
Definisci la proprietà e l'attributo src
:
export class BrickViewer extends LitElement {
@property({type: String})
src: string|null = null;
}
Ora <brick-viewer>
ha un attributo src
che possiamo impostare in HTML. Il suo valore è già leggibile nella nostra classe BrickViewer
grazie a lit-element.
Visualizzazione dei valori
Possiamo visualizzare il valore dell'attributo src
utilizzandolo nel valore letterale del modello del metodo di rendering. Interpola i valori nei valori letterali del modello utilizzando la sintassi ${value}
.
export class BrickViewer extends LitElement {
render() {
return html`<div>Brick model: ${this.src}</div>`;
}
}
Ora vediamo il valore dell'attributo src nell'elemento <brick-viewer>
della finestra. Prova questa procedura: apri gli strumenti per sviluppatori del browser e modifica manualmente l'attributo src. Prova...
...hai notato che il testo nell'elemento si aggiorna automaticamente? lit-element osserva le proprietà della classe decorate con @property
e visualizza nuovamente la vista. lit-element si occupa del lavoro pesante al posto tuo.
4. Imposta lo scenario con Three.js
Luci, fotocamera, rendering!
Il nostro elemento personalizzato utilizzerà tre.js per eseguire il rendering dei nostri modelli di mattoncini 3D. Ci sono alcune cose che vogliamo fare una sola volta per ogni istanza di un elemento <brick-viewer>
, ad esempio configurare la scena terza.js, la fotocamera e l'illuminazione. Aggiungeremo questi elementi al costruttore alla classe BrickViewer. Conserviamo alcuni oggetti come proprietà di classe per poterli utilizzare in seguito: fotocamera, scena, controlli e renderer.
Aggiungi la configurazione della scena tre.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'oggetto WebGLRenderer
fornisce un elemento DOM che mostra la scena terza.js sottoposta a rendering. Si accede tramite la proprietà domElement
. Possiamo interpolare questo valore nel valore letterale del modello di rendering, utilizzando la sintassi ${value}
.
Rimuovi il messaggio src
presente nel modello e inserisci l'elemento DOM del renderer:
export class BrickViewer extends LitElement {
render() {
return html`
${this._renderer.domElement}
`;
}
}
Per consentire la visualizzazione completa dell'elemento dom del renderer, è necessario impostare anche l'elemento <brick-viewer>
su display: block
. Possiamo fornire gli stili in una proprietà statica denominata styles
, impostata su un valore letterale di modello css
.
Aggiungi questo stile al corso:
export class BrickViewer extends LitElement {
static styles = css`
/* The :host selector styles the brick-viewer itself! */
:host {
display: block;
}
`;
}
Ora <brick-viewer>
sta mostrando una scena terza.js sottoposta a rendering:
Ma... è vuota. Ora forniamo un modello.
Caricatore per mattoncini
Passeremo la proprietà src
che abbiamo definito in precedenza a LDrawLoader, che viene fornito con tre.js.
I file LDraw possono separare un modello Brick in passaggi di costruzione distinti. Il numero totale di passaggi e la visibilità dei singoli mattoncini sono accessibili tramite l'API LDrawLoader.
Copia queste proprietà, il nuovo metodo _loadModel
e la nuova riga nel costruttore:
@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();
});
}
}
Quando devo chiamare _loadModel
? Deve essere richiamato ogni volta che l'attributo src viene modificato. Decorando la proprietà src
con @property
, abbiamo attivato il ciclo di vita dell'aggiornamento degli elementi accesi. Ogni volta che una di queste strutture decorate modifiche ai valori, viene chiamata una serie di metodi per accedere ai valori vecchi e nuovi delle proprietà. Il metodo del ciclo di vita che ci interessa si chiama update
. Il metodo update
prende un argomento PropertyValues
, che conterrà informazioni su tutte le proprietà che sono state appena modificate. È il posto perfetto per chiamare _loadModel
.
Aggiungi il metodo update
:
export class BrickViewer extends LitElement {
update(changedProperties: PropertyValues) {
if (changedProperties.has('src')) {
this._loadModel();
}
super.update(changedProperties);
}
}
Il nostro elemento <brick-viewer>
ora può visualizzare un file di mattoni, specificato con l'attributo src
.
5. Visualizzazione di modelli parziali
Ora rendi configurabile il passaggio di costruzione corrente. Vorremmo essere in grado di specificare <brick-viewer step="5"></brick-viewer>
e dovremmo vedere come appare il modello del mattone nel quinto passaggio della costruzione. Per farlo, rendiamo la proprietà step
una proprietà osservata decorandola con @property
.
Decora la proprietà step
:
export class BrickViewer extends LitElement {
@property({type: Number})
step?: number;
}
Ora aggiungeremo un metodo di supporto che rende visibili solo i mattoncini fino al passaggio di costruzione corrente. Chiameremo l'helper nel metodo di aggiornamento in modo che venga eseguito ogni volta che la proprietà step
viene modificata.
Aggiorna il metodo update
e aggiungi il nuovo metodo _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);
}
}
Ora apri devtools del browser e ispeziona l'elemento <brick-viewer>
. Aggiungi un attributo step
, in questo modo:
Guarda cosa succede al modello sottoposto a rendering. Possiamo utilizzare l'attributo step
per controllare in che misura viene mostrato il modello. Ecco come dovrebbe apparire quando l'attributo step
è impostato su "10"
:
6. Navigazione sul set di mattoncini
pulsante-icona-mwc
L'utente finale del nostro <brick-viewer>
deve anche essere in grado di navigare nei passaggi della build tramite UI. Aggiungiamo pulsanti per andare al passaggio successivo, al passaggio precedente e al primo passaggio. Per semplificare l'operazione, utilizzeremo il componente web dei pulsanti di Material Design. Poiché @material/mwc-icon-button
è già importato, possiamo inserirlo in <mwc-icon-button></mwc-icon-button>
. Possiamo specificare l'icona che vogliamo utilizzare con l'attributo icona, in questo modo: <mwc-icon-button icon="thumb_up"></mwc-icon-button>
. Tutte le possibili icone sono disponibili qui: material.io/resources/icons.
Aggiungiamo alcuni pulsanti icona al metodo di rendering:
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>
`;
}
}
Utilizzare Material Design nella nostra pagina è semplicissimo grazie ai componenti web.
Associazioni di eventi
Questi pulsanti dovrebbero effettivamente fare qualcosa. La "risposta" dovrebbe reimpostare il passaggio di costruzione su 1. Il comando "navigazione_prima" dovrebbe diminuire il passaggio di costruzione e il comando "nav_next" dovrebbe incrementarlo. lit-element semplifica l'aggiunta di questa funzionalità, grazie alle associazioni di eventi. Nel valore letterale del modello HTML, utilizza la sintassi @eventname=${eventHandler}
come attributo dell'elemento. Ora eventHandler
verrà eseguito quando verrà rilevato un evento eventname
su quell'elemento. Ad esempio, aggiungiamo i gestori di eventi clic ai tre pulsanti icona:
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>
`;
}
}
Prova a fare clic sui pulsanti ora. Bene!
Stili
I pulsanti funzionano, ma non hanno un bell'aspetto. Sono tutti rannicchiati sul fondo. Scegli uno stile per sovrapporle alla scena.
Per applicare stili a questi pulsanti, torniamo alla proprietà static styles
. Questi stili sono limitati, il che significa che verranno applicati solo agli elementi all'interno di questo componente web. Questa è una delle gioie della scrittura dei componenti web: i selettori possono essere più semplici e i CSS sono più facili da leggere e scrivere. Ciao BEM!
Aggiorna gli stili in modo che abbiano il seguente aspetto:
export class BrickViewer extends LitElement {
static styles = css`
:host {
display: block;
position: relative;
}
#controls {
position: absolute;
bottom: 0;
width: 100%;
display: flex;
}
`;
}
Pulsante Reimposta fotocamera
Gli utenti finali di <brick-viewer>
possono ruotare la scena utilizzando i controlli del mouse. Mentre aggiungiamo i pulsanti, aggiungine uno per reimpostare la posizione predefinita della fotocamera. Un altro <mwc-icon-button>
con un'associazione di eventi di clic completerà il lavoro.
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>
`;
}
}
Navigazione più rapida
Alcuni mattoncini hanno molti gradini. Un utente potrebbe voler passare a un passaggio specifico. L'aggiunta di un cursore con numeri di passaggi può essere utile per la navigazione rapida. A questo scopo, utilizzeremo l'elemento <mwc-slider>
.
cursore-mwc
L'elemento di scorrimento richiede alcuni dati importanti, come il valore minimo e massimo del cursore. Il valore minimo del dispositivo di scorrimento può sempre essere "1". Il valore massimo del cursore deve essere this._numConstructionSteps
, se il modello è stato caricato. Possiamo distinguere questi valori <mwc-slider>
tramite i relativi attributi. Possiamo anche utilizzare l'istruzione lit-html ifDefined
per evitare di impostare l'attributo max
se la proprietà _numConstructionSteps
non è stata definita.
Aggiungi un <mwc-slider>
tra "retro" e "in avanti" pulsanti:
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>
`;
}
}
Dati "su"
Quando un utente sposta il dispositivo di scorrimento, il passaggio di costruzione corrente deve cambiare e la visibilità del modello deve essere aggiornata di conseguenza. L'elemento del cursore emetterà un evento di input ogni volta che viene trascinato. Aggiungi un'associazione di eventi sul dispositivo di scorrimento per individuare l'evento e modificare il passaggio di costruzione.
Aggiungi l'associazione di eventi:
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>
`;
}
}
Evviva! Possiamo usare il cursore per cambiare il passaggio visualizzato.
Dati "in basso"
C'è un'altra cosa. Quando il pulsante "Indietro" e "Avanti" vengono utilizzati per cambiare il passaggio, il punto di manipolazione del cursore deve essere aggiornato. Associa l'attributo del valore di <mwc-slider>
a this.step
.
Aggiungi l'associazione 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>
`;
}
}
Abbiamo quasi finito con il cursore. Aggiungi uno stile flessibile per far sì che si adatti bene agli altri controlli:
export class BrickViewer extends LitElement {
static styles = css`
/* ... */
mwc-slider {
flex-grow: 1;
}
`;
}
Inoltre, dobbiamo chiamare layout
sull'elemento di scorrimento stesso. Lo facciamo nel metodo del ciclo di vita firstUpdated
, che viene chiamato dopo la configurazione iniziale del DOM. Il decoratore query
può aiutarci a ottenere un riferimento all'elemento di scorrimento nel modello.
export class BrickViewer extends LitElement {
@query('mwc-slider')
slider!: Slider|null;
async firstUpdated() {
if (this.slider) {
await this.slider.updateComplete
this.slider.layout();
}
}
}
Di seguito sono riportate tutte le aggiunte dei dispositivi di scorrimento (con attributi pin
e markers
aggiuntivi sul cursore per conferire un aspetto accattivante):
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>
`;
}
}
Ecco il prodotto finale.
7. Conclusione
Abbiamo imparato molto su come utilizzare lit-element per creare il nostro elemento HTML. Abbiamo imparato a:
- Definisci un elemento personalizzato
- Dichiara un'API degli attributi
- Visualizzare una vista per un elemento personalizzato
- Stili di incapsulamento
- Utilizzare eventi e proprietà per trasmettere i dati
Per saperne di più su lit-element, puoi leggere il loro sito ufficiale.
Puoi vedere un elemento completo di Brick-viewer all'indirizzo stackblitz.com/edit/brick-viewer-complete.
Brick-viewer viene anche spedito su Gestione dei partner di rete e puoi visualizzare il codice qui: Repository GitHub.