1. Wprowadzenie
Komponenty internetowe
Komponenty sieciowe to zbiór standardów internetowych, które pozwalają programistom rozszerzać kod HTML o elementy niestandardowe. W ramach tego ćwiczenia w Codelabs zdefiniujesz element <brick-viewer>
, który będzie mógł wyświetlać modele ceglane.
element świetlny
Aby łatwiej zdefiniować element niestandardowy <brick-viewer>
, użyjemy elementu litowego. lit-element to lekka klasa bazowa, która dodaje cukier składowy do standardu komponentów sieciowych. Ułatwi nam to rozpoczęcie korzystania z elementu niestandardowego.
Rozpocznij
Będziemy kodować w środowisku online Stackblitz, więc otwórz ten link w nowym oknie:
stackblitz.com/edit/brick-viewer
Zaczynajmy!
2. Definiowanie elementu niestandardowego
Definicja klasy
Aby zdefiniować element niestandardowy, utwórz klasę, która rozszerza zakres LitElement
, i dekoruj ją za pomocą @customElement
. Argument @customElement
będzie nazwą elementu niestandardowego.
W pliku brick-viewer.ts wpisz:
@customElement('brick-viewer')
export class BrickViewer extends LitElement {
}
Możesz już użyć elementu <brick-viewer></brick-viewer>
w kodzie HTML. Jeśli jednak spróbujesz to zrobić, nic się nie wyświetli. Zajmijmy się tym.
Metoda renderowania
Aby zaimplementować widok komponentu, zdefiniuj metodę o nazwie „renderowanie”. Ta metoda powinna zwracać literał szablonu otagowany funkcją html
. Umieść dowolny kod HTML w odpowiednim literaale szablonu. Obraz zostanie wyrenderowany, gdy użyjesz <brick-viewer>
.
Dodaj metodę render
:
export class BrickViewer extends LitElement {
render() {
return html`<div>Brick viewer</div>`;
}
}
3. Określanie pliku LDraw
Zdefiniuj usługę
Najlepiej, gdyby użytkownik <brick-viewer>
mógł określić, który model klocków ma być wyświetlany, za pomocą atrybutu na przykład:
<brick-viewer src="path/to/model.ldraw"></brick-viewer>
Tworzymy element HTML, więc możemy wykorzystać interfejs API deklaratywnego i zdefiniować atrybut źródła, tak jak w przypadku tagu <img>
lub <video>
. Z elementem oświetleniowym jest to równie proste jak udekorowanie obiektu klasowego za pomocą @property
. Opcja type
pozwala określić sposób analizowania właściwości przez element świetlny na potrzeby użycia jej jako atrybutu HTML.
Zdefiniuj właściwość i atrybut src
:
export class BrickViewer extends LitElement {
@property({type: String})
src: string|null = null;
}
<brick-viewer>
ma teraz atrybut src
, który można ustawić w kodzie HTML. Jego wartość jest już czytelna z poziomu klasy BrickViewer
dzięki elementowi świetlnemu.
Wyświetlam wartości
Możemy wyświetlić wartość atrybutu src
, używając jej w literalu szablonu metody renderowania. Interpoluj wartości na literały szablonów z użyciem składni ${value}
.
export class BrickViewer extends LitElement {
render() {
return html`<div>Brick model: ${this.src}</div>`;
}
}
Teraz widzimy wartość atrybutu src w elemencie <brick-viewer>
w oknie. Spróbuj wykonać to działanie: otwórz narzędzia dla programistów w przeglądarce i ręcznie zmień atrybut src. Śmiało, wypróbuj...
...Czy zauważyłeś, że tekst w elemencie jest aktualizowany automatycznie? lit-element obserwuje właściwości klasy ozdobione znakiem @property
i ponownie renderuje widok. i wykonuje ciężką pracę za Ciebie.
4. Twórz otoczenie za pomocą Three.js
Światła, kamera, renderowanie!
Nasz element niestandardowy użyje pliku trzy.js do renderowania klocków 3D. W niektórych przypadkach musimy robić tylko raz na każde wystąpienie elementu <brick-viewer>
, na przykład skonfigurować scenę, kamerę i oświetlenie. Dodamy je do konstruktora klasy BrickViewer. Niektóre obiekty zachowamy jako właściwości klasy, żeby móc ich później użyć: kamera, scena, elementy sterujące i mechanizm renderowania.
Dodaj konfigurację sceny trzy.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);
};
}
Obiekt WebGLRenderer
zawiera element DOM, który wyświetla wyrenderowaną scenę trzy.js. Dostęp do niego uzyskuje się przez usługę domElement
. Możemy interpolować tę wartość do literału szablonu renderowania przy użyciu składni ${value}
.
Usuń wiadomość src
z szablonu i wstaw element DOM mechanizmu renderowania:
export class BrickViewer extends LitElement {
render() {
return html`
${this._renderer.domElement}
`;
}
}
Aby element DOM mechanizmu renderowania był wyświetlany w całości, trzeba również ustawić wartość display: block
w samym elemencie <brick-viewer>
. Możemy dostarczyć style w właściwości statycznej o nazwie styles
ustawionej na literał szablonu css
.
Dodaj ten styl do klasy:
export class BrickViewer extends LitElement {
static styles = css`
/* The :host selector styles the brick-viewer itself! */
:host {
display: block;
}
`;
}
Teraz <brick-viewer>
wyświetla wyrenderowaną scenę trzy.js:
Ale jest pusta. Przedstawmy mu model.
Ładowarka cegła
Zdefiniowaną wcześniej właściwość src
przekażemy do klasy LDrawLoader, która jest wysyłana z 3.js.
Pliki LDraw pozwalają rozdzielać model ceglany na osobne etapy budynku. Łączna liczba kroków i widoczność poszczególnych klocków są dostępne przez interfejs LDrawLoader API.
Skopiuj te właściwości, nową metodę _loadModel
i nowy wiersz w konstruktorze:
@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();
});
}
}
Kiedy należy zadzwonić pod numer _loadModel
? Musi być wywoływany za każdym razem, gdy zmienia się atrybut src. Dekorując właściwość src
za pomocą parametru @property
, zdecydowaliśmy się włączyć ją do cyklu aktualizacji elementu świetlnego. Gdy któreś z tych udekorowanych posiadłości zmian wartości, wywoływany jest szereg metod, które mogą uzyskać dostęp do nowych i starych wartości właściwości. Metoda cyklu życia, która nas interesuje, to update
. Metoda update
przyjmuje argument PropertyValues
, który zawiera informacje o wszelkich właściwościach, które właśnie się zmieniły. To idealne miejsce, by zadzwonić pod numer _loadModel
.
Dodaj metodę update
:
export class BrickViewer extends LitElement {
update(changedProperties: PropertyValues) {
if (changedProperties.has('src')) {
this._loadModel();
}
super.update(changedProperties);
}
}
Nasz element <brick-viewer>
może teraz wyświetlać plik ceglany określony za pomocą atrybutu src
.
5. Wyświetlanie modeli cząstkowych
Teraz skonfigurujmy bieżący etap tworzenia. Chcemy określić <brick-viewer step="5"></brick-viewer>
, co pozwoli nam zobaczyć, jak będzie wyglądać model z cegły na piątym kroku budowy. W tym celu ustawmy właściwość step
jako właściwość obserwowaną, dekorując ją atrybutem @property
.
Dekoruj właściwość step
:
export class BrickViewer extends LitElement {
@property({type: Number})
step?: number;
}
Teraz dodamy metodę pomocniczą, dzięki której widoczne będą tylko klocki aż do bieżącego etapu kompilacji. Będziemy wywołać pomocnik w metodzie aktualizacji, tak aby uruchamiał się po każdej zmianie właściwości step
.
Zaktualizuj metodę update
i dodaj nową metodę _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);
}
}
Teraz otwórz narzędzia deweloperskie w przeglądarce i sprawdź element <brick-viewer>
. Dodaj do niego atrybut step
w ten sposób:
Zobacz, co dzieje się z wyrenderowanym modelem. Możemy użyć atrybutu step
, aby kontrolować, jaka część modelu ma być wyświetlana. Gdy atrybut step
ma wartość "10"
, powinno to wyglądać tak:
6. Nawigacja po zestawie klocków
Ikona-przycisk-mwc
Użytkownik <brick-viewer>
powinien też być w stanie poruszać się po kolejnych krokach kompilacji w interfejsie. Dodajmy przyciski przechodzenia do następnego, poprzedniego kroku i pierwszego kroku. Aby ułatwić ten proces, użyjemy komponentu internetowego przycisków w stylu Material Design. Domena @material/mwc-icon-button
została już zaimportowana, więc możemy przejść do <mwc-icon-button></mwc-icon-button>
. W atrybucie ikony możemy określić ikonę, której chcemy używać z atrybutem ikony, na przykład: <mwc-icon-button icon="thumb_up"></mwc-icon-button>
. Wszystkie możliwe ikony znajdziesz tutaj: material.io/resources/icons.
Dodajmy do metody renderowania kilka przycisków ikon:
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>
`;
}
}
Dzięki komponentom internetowym zastosowanie na naszej stronie stylu Material Design jest naprawdę proste.
Powiązania zdarzeń
Te przyciski powinny właściwie działać. Opcja „Odpowiedz” powinien zresetować krok tworzenia do 1. Parametr „Navigation_before” powinien zmniejszyć liczbę kroków do wykonania, a parametr „Navigation_next”, powinien ją zwiększyć. lit-element ułatwia dodanie tej funkcji wraz z powiązaniami zdarzeń. W literalu szablonu HTML użyj składni @eventname=${eventHandler}
jako atrybutu elementu. eventHandler
będzie się teraz uruchamiać, gdy w tym elemencie zostanie wykryte zdarzenie eventname
. Na przykład dodajmy moduły obsługi zdarzeń kliknięcia do trzech przycisków ikon:
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>
`;
}
}
Spróbuj kliknąć przyciski. Świetnie!
Style
Przyciski działają, ale nie wygląda to dobrze. Wszystkie są zebrane na dole. Nadaj im styl, aby nałożyć je na scenę.
Aby zastosować style do tych przycisków, wracamy do właściwości static styles
. Style te mają ograniczony zakres, co oznacza, że będą stosowane tylko do elementów w tym komponencie internetowym. To jedna z największych zalet tworzenia komponentów sieciowych: selektory mogą być prostsze, a CSS – czytanie i pisanie. Do widzenia, BEM!
Zmień style, aby wyglądały tak:
export class BrickViewer extends LitElement {
static styles = css`
:host {
display: block;
position: relative;
}
#controls {
position: absolute;
bottom: 0;
width: 100%;
display: flex;
}
`;
}
Przycisk resetowania aparatu
Użytkownicy naszego narzędzia <brick-viewer>
mogą obracać scenę za pomocą myszy. Dodajmy teraz przyciski, które służą do resetowania kamery do pozycji domyślnej. Kolejny <mwc-icon-button>
z powiązaniem zdarzenia kliknięcia wystarczy, by wykonać zadanie.
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>
`;
}
}
Szybsza nawigacja
Niektóre klocki mają wiele schodków. Użytkownik może pominąć dany krok. Dodanie suwaka z numerami kroków może przyspieszyć nawigację. Wykorzystamy do tego element <mwc-slider>
.
mwc-slider
Suwak wymaga kilku ważnych danych, np. minimalnej i maksymalnej wartości suwaka. Minimalna wartość suwaka zawsze wynosi „1”. Maksymalna wartość suwaka powinna wynosić this._numConstructionSteps
, jeśli model został wczytany. Możemy rozpoznać te wartości za pomocą atrybutów <mwc-slider>
. Możemy też użyć dyrektywy ifDefined
lit-html, aby uniknąć ustawiania atrybutu max
, jeśli właściwość _numConstructionSteps
nie została zdefiniowana.
Dodaj <mwc-slider>
między „wstecz” i „dalej” Przyciski:
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>
`;
}
}
Dane są aktywne
Gdy użytkownik przesunie suwak, bieżący etap tworzenia powinien się zmienić, a widoczność modelu zostanie odpowiednio zaktualizowana. Suwak będzie wywoływać zdarzenie wejściowe po każdym przeciągnięciu suwaka. Dodaj powiązanie zdarzenia na samym suwaku, aby wychwytywać to zdarzenie i zmienić krok tworzenia.
Dodaj powiązanie wydarzenia:
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>
`;
}
}
Super! Suwak pozwala zmienić wyświetlany krok.
Dane nie działają
Jeszcze jedno. Jeśli przycisk „Wstecz” i „Dalej” służą do zmiany kroku. Uchwyt suwaka trzeba zaktualizować. Powiąż atrybut wartości <mwc-slider>
z obiektem this.step
.
Dodaj powiązanie 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>
`;
}
}
Suwak jest prawie skończony. Dodaj styl elastyczny, aby lepiej współgrał z innymi elementami sterującymi:
export class BrickViewer extends LitElement {
static styles = css`
/* ... */
mwc-slider {
flex-grow: 1;
}
`;
}
Musisz też wywołać funkcję layout
w samym elemencie suwaka. Zrobimy to w metodzie cyklu życia firstUpdated
, która jest wywoływana zaraz po skonfigurowaniu DOM. Dekorator query
może pomóc nam znaleźć odwołanie do elementu suwaka w szablonie.
export class BrickViewer extends LitElement {
@query('mwc-slider')
slider!: Slider|null;
async firstUpdated() {
if (this.slider) {
await this.slider.updateComplete
this.slider.layout();
}
}
}
Oto wszystkie dodatki z suwakiem (oraz dodatkowe atrybuty pin
i markers
na suwaku, dzięki czemu wygląda świetnie):
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>
`;
}
}
Oto efekt końcowy.
7. Podsumowanie
Dowiedzieliśmy się dużo o tym, jak wykorzystać element lit-element do stworzenia własnego elementu HTML. Nauczyliśmy się, jak:
- Definiowanie elementu niestandardowego
- Deklarowanie interfejsu API atrybutów
- Renderowanie widoku elementu niestandardowego
- Uzupełnij style
- Używanie zdarzeń i właściwości do przekazywania danych
Więcej informacji o elementach świetlnych znajdziesz na oficjalnej stronie.
Ukończony element ceglany znajdziesz na stronie stackblitz.com/edit/brick-viewer-complete.
Usługa Brick-viewer jest też wysyłana w ramach NPM. Źródło możesz zobaczyć tutaj: repozytorium GitHub.