1. Pengantar
Komponen web
Komponen web adalah kumpulan standar web yang memungkinkan pengembang untuk memperluas HTML dengan elemen khusus. Dalam codelab ini, Anda akan menentukan elemen <brick-viewer>
, yang akan dapat menampilkan model brick.
lit-element
Untuk membantu menentukan elemen kustom <brick-viewer>
, kita akan menggunakan lit-element. lit-element adalah class dasar ringan yang menambahkan beberapa sugar sintaksis ke standar komponen web. Hal ini akan memudahkan kita untuk memulai dan menjalankan elemen kustom.
Mulai
Kita akan melakukan coding di lingkungan Stackblitz online, jadi buka link ini di jendela baru:
stackblitz.com/edit/brick-viewer
Mari kita mulai!
2. Menentukan Elemen Khusus
Definisi class
Untuk menentukan elemen kustom, buat class yang memperluas LitElement
dan hias dengan @customElement
. Argumen untuk @customElement
akan menjadi nama elemen kustom.
Di brick-viewer.ts, masukkan:
@customElement('brick-viewer')
export class BrickViewer extends LitElement {
}
Sekarang, elemen <brick-viewer></brick-viewer>
siap digunakan di HTML. Namun, jika Anda mencobanya, tidak ada yang akan dirender. Ayo perbaiki.
Metode render
Untuk mengimplementasikan tampilan komponen, definisikan metode bernama render. Metode ini akan menampilkan literal template yang diberi tag dengan fungsi html
. Masukkan HTML apa pun yang Anda inginkan dalam literal template yang diberi tag. Ini akan dirender saat Anda menggunakan <brick-viewer>
.
Tambahkan metode render
:
export class BrickViewer extends LitElement {
render() {
return html`<div>Brick viewer</div>`;
}
}
3. Menentukan File LDraw
Menentukan properti
Akan sangat baik jika pengguna <brick-viewer>
dapat menentukan model bata mana yang akan ditampilkan menggunakan atribut, seperti ini:
<brick-viewer src="path/to/model.ldraw"></brick-viewer>
Karena sedang membuat elemen HTML, kita dapat memanfaatkan API deklaratif dan menentukan atribut sumber, sama seperti tag <img>
atau <video>
. Dengan elemen lit, keduanya sama mudahnya dengan mendekorasi properti class dengan @property
. Opsi type
memungkinkan Anda menentukan cara lit-element mengurai properti untuk digunakan sebagai atribut HTML.
Tentukan properti dan atribut src
:
export class BrickViewer extends LitElement {
@property({type: String})
src: string|null = null;
}
<brick-viewer>
sekarang memiliki atribut src
yang dapat kita setel di HTML. Nilainya sudah dapat dibaca dari dalam class BrickViewer
berkat elemen lit.
Menampilkan nilai
Kita dapat menampilkan nilai atribut src
dengan menggunakannya dalam literal template metode render. Lakukan interpolasi nilai ke dalam literal template menggunakan sintaksis ${value}
.
export class BrickViewer extends LitElement {
render() {
return html`<div>Brick model: ${this.src}</div>`;
}
}
Sekarang, kita melihat nilai atribut src di elemen <brick-viewer>
di jendela. Coba ini: buka alat developer browser Anda dan ubah atribut src secara manual. Silakan, cobalah...
...Apakah Anda melihat bahwa teks di elemen diperbarui secara otomatis? lit-element mengamati properti class yang dihiasi dengan @property
dan merender ulang tampilan untuk Anda. lit-element melakukan pekerjaan berat sehingga Anda tidak perlu melakukannya.
4. Menetapkan Scene dengan Three.js
Lampu, Kamera, Render!
Elemen kustom kita akan menggunakan three.js untuk merender model bata 3D. Ada beberapa hal yang ingin kita lakukan hanya sekali untuk setiap instance elemen <brick-viewer>
, seperti menyiapkan tampilan, kamera, dan pencahayaan Three.js. Kita akan menambahkannya ke konstruktor class BrickViewer. Kita akan menyimpan beberapa objek sebagai properti class sehingga dapat digunakan nanti: kamera, scene, kontrol, dan perender.
Tambahkan penyiapan scene 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);
};
}
Objek WebGLRenderer
menyediakan elemen DOM yang menampilkan tampilan three.js yang dirender. Fungsi ini diakses melalui properti domElement
. Kita dapat menginterpolasi nilai ini ke dalam literal template render, menggunakan sintaksis ${value}
.
Hapus pesan src
yang kita miliki di template, dan masukkan elemen DOM perender:
export class BrickViewer extends LitElement {
render() {
return html`
${this._renderer.domElement}
`;
}
}
Agar elemen dom perender ditampilkan secara keseluruhan, kita juga perlu menetapkan elemen <brick-viewer>
itu sendiri ke display: block
. Kita dapat menyediakan gaya di properti statis yang disebut styles
, yang ditetapkan ke literal template css
.
Tambahkan gaya ini ke class:
export class BrickViewer extends LitElement {
static styles = css`
/* The :host selector styles the brick-viewer itself! */
:host {
display: block;
}
`;
}
Sekarang <brick-viewer>
menampilkan scene Three.js yang dirender:
Tapi... kosong. Mari kita berikan model.
Loader brick
Kita akan meneruskan properti src
yang telah ditentukan sebelumnya ke LDrawLoader, yang dikirimkan dengan tiga.js.
File LDraw dapat memisahkan model Brick menjadi langkah-langkah pembuatan yang terpisah. Jumlah total langkah dan visibilitas balok individual yang dapat diakses melalui API LDrawLoader.
Salin properti ini, metode _loadModel
baru, dan baris baru di 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();
});
}
}
Kapan sebaiknya _loadModel
dipanggil? Fungsi ini harus dipanggil setiap kali atribut src berubah. Dengan mendekorasi properti src
dengan @property
, kita telah mengikutsertakan properti ke dalam siklus proses update lit-element. Setiap kali salah satu nilai properti yang didekorasi ini berubah, serangkaian metode akan dipanggil yang dapat mengakses nilai baru dan lama properti. Metode siklus proses yang kita minati disebut update
. Metode update
menggunakan argumen PropertyValues
, yang akan berisi informasi tentang properti apa pun yang baru saja berubah. Ini adalah tempat yang tepat untuk memanggil _loadModel
.
Tambahkan metode update
:
export class BrickViewer extends LitElement {
update(changedProperties: PropertyValues) {
if (changedProperties.has('src')) {
this._loadModel();
}
super.update(changedProperties);
}
}
Elemen <brick-viewer>
sekarang dapat menampilkan file brick, yang ditentukan dengan atribut src
.
5. Menampilkan Model Sebagian
Sekarang, mari kita buat langkah konstruksi saat ini dapat dikonfigurasi. Kita ingin dapat menentukan <brick-viewer step="5"></brick-viewer>
, dan kita akan melihat seperti apa model bata pada langkah konstruksi ke-5. Untuk melakukannya, mari kita buat properti step
sebagai properti yang diamati dengan mendekorasinya dengan @property
.
Hiasi properti step
:
export class BrickViewer extends LitElement {
@property({type: Number})
step?: number;
}
Sekarang, kita akan menambahkan metode helper yang hanya membuat brick hingga langkah build saat ini terlihat. Kita akan memanggil helper dalam metode update sehingga helper tersebut berjalan setiap kali properti step
diubah.
Update metode update
, dan tambahkan metode _updateBricksVisibility
baru:
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);
}
}
Oke, sekarang buka devtools browser Anda, dan periksa elemen <brick-viewer>
. Tambahkan atribut step
ke atribut tersebut, seperti ini:
Lihat apa yang terjadi pada model yang dirender. Kita dapat menggunakan atribut step
untuk mengontrol jumlah model yang ditampilkan. Berikut adalah tampilannya saat atribut step
disetel ke "10"
:
6. Navigasi Setelan Bata
tombol-ikon-mwc
Pengguna akhir <brick-viewer>
juga harus dapat menavigasi langkah-langkah build melalui UI. Mari kita tambahkan tombol untuk menuju ke langkah berikutnya, langkah sebelumnya, dan langkah pertama. Kita akan menggunakan komponen web tombol Desain Material untuk mempermudahnya. Karena @material/mwc-icon-button
sudah diimpor, kita siap menghapus <mwc-icon-button></mwc-icon-button>
. Kita dapat menentukan ikon yang ingin digunakan dengan atribut ikon, seperti ini: <mwc-icon-button icon="thumb_up"></mwc-icon-button>
. Semua kemungkinan ikon dapat ditemukan di sini: material.io/resources/icons.
Mari kita tambahkan beberapa tombol ikon ke metode render:
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>
`;
}
}
Menggunakan Desain Material di halaman kita semudah itu, berkat komponen web.
Binding peristiwa
Tombol-tombol ini seharusnya
melakukan sesuatu. Tombol "balas" akan mereset langkah konstruksi ke 1. Tombol "navigate_before" akan mengurangi langkah konstruksi, dan tombol "navigate_next" akan menambahnya. lit-element memudahkan penambahan fungsi ini, dengan binding peristiwa. Dalam literal template HTML, gunakan sintaksis @eventname=${eventHandler}
sebagai atribut elemen. eventHandler
kini akan berjalan saat peristiwa eventname
terdeteksi pada elemen tersebut. Sebagai contoh, mari kita tambahkan pengendali peristiwa klik ke tiga tombol ikon kita:
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>
`;
}
}
Coba klik tombol sekarang. Bagus!
Gaya
Tombol berfungsi, tetapi tidak terlihat bagus. Semuanya berkumpul di bagian bawah. Mari kita beri gaya untuk menempatkannya di tampilan.
Untuk menerapkan gaya ke tombol ini, kita kembali ke properti static styles
. Gaya ini memiliki cakupan, yang berarti gaya tersebut hanya akan berlaku pada elemen dalam komponen web ini. Itulah salah satu kesenangan menulis komponen web: pemilih bisa lebih sederhana, dan CSS akan lebih mudah dibaca dan ditulis. Sampai jumpa, BEM!
Perbarui gaya agar terlihat seperti ini:
export class BrickViewer extends LitElement {
static styles = css`
:host {
display: block;
position: relative;
}
#controls {
position: absolute;
bottom: 0;
width: 100%;
display: flex;
}
`;
}
Tombol reset kamera
Pengguna akhir <brick-viewer>
dapat memutar adegan menggunakan kontrol mouse. Saat kita menambahkan tombol, mari tambahkan satu tombol untuk mereset kamera ke posisi defaultnya. <mwc-icon-button>
lain dengan binding peristiwa klik akan menyelesaikan tugas.
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>
`;
}
}
Navigasi lebih cepat
Beberapa balok memiliki banyak anak tangga. Pengguna mungkin ingin melewati ke langkah tertentu. Menambahkan penggeser dengan nomor langkah dapat membantu navigasi cepat. Kita akan menggunakan elemen <mwc-slider>
untuk ini.
penggeser-mwc
Elemen penggeser memerlukan beberapa data penting, seperti nilai penggeser minimum dan maksimum. Nilai penggeser minimum selalu dapat berupa "1". Nilai penggeser maksimum harus this._numConstructionSteps
, jika model telah dimuat. Kita dapat memberi tahu <mwc-slider>
nilai ini melalui atributnya. Kita juga dapat menggunakan perintah lit-html ifDefined
untuk menghindari penetapan atribut max
jika properti _numConstructionSteps
belum ditentukan.
Tambahkan <mwc-slider>
di antara tombol "kembali" dan "maju":
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>
`;
}
}
Data "up"
Saat pengguna memindahkan penggeser, langkah konstruksi saat ini akan berubah, dan visibilitas model harus diperbarui. Elemen penggeser akan memunculkan peristiwa input setiap kali penggeser ditarik. Tambahkan binding peristiwa pada penggeser itu sendiri untuk menangkap peristiwa ini dan mengubah langkah konstruksi.
Tambahkan binding acara:
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>
`;
}
}
Hore! Kita dapat menggunakan {i>slider <i}untuk mengubah langkah mana yang ditampilkan.
Data "down"
Ada satu hal lagi. Saat tombol "kembali" dan "berikutnya" digunakan untuk mengubah langkah, handle penggeser perlu diperbarui. Ikat atribut nilai <mwc-slider>
ke this.step
.
Tambahkan binding 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>
`;
}
}
Kita hampir selesai dengan {i>slider<i}. Tambahkan gaya yang fleksibel agar dapat berfungsi dengan baik dengan kontrol lain:
export class BrickViewer extends LitElement {
static styles = css`
/* ... */
mwc-slider {
flex-grow: 1;
}
`;
}
Selain itu, kita perlu memanggil layout
pada elemen penggeser itu sendiri. Kita akan melakukannya dalam metode siklus proses firstUpdated
, yang dipanggil setelah DOM pertama kali ditata. Dekorator query
dapat membantu kita mendapatkan referensi ke elemen penggeser di template.
export class BrickViewer extends LitElement {
@query('mwc-slider')
slider!: Slider|null;
async firstUpdated() {
if (this.slider) {
await this.slider.updateComplete
this.slider.layout();
}
}
}
Berikut adalah semua penambahan penggeser yang digabungkan (dengan atribut pin
dan markers
tambahan pada penggeser agar terlihat keren):
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>
`;
}
}
Ini produk akhirnya!
7. Kesimpulan
Kita telah belajar banyak tentang cara menggunakan lit-element untuk membuat elemen HTML sendiri. Kita telah mempelajari cara:
- Menentukan elemen khusus
- Mendeklarasikan API atribut
- Merender tampilan untuk elemen kustom
- Mengenkapsulasi gaya
- Menggunakan peristiwa dan properti untuk meneruskan data
Jika ingin mempelajari lit-element lebih lanjut, Anda dapat membaca selengkapnya di situs resminya.
Anda dapat melihat elemen brick-viewer yang sudah selesai di stackblitz.com/edit/brick-viewer-complete.
brick-viewer juga dikirimkan di NPM, dan Anda dapat melihat sumbernya di sini: repo GitHub.