Halo!
👋

Implementasi Custom Elements di Aplikasi Next.js

Titik Temu dari Dua Ekosistem Berbeda

  • Dipublikasikan pada 
  • 5 menit waktu baca
Artikel ini juga tersedia dalam bahasa .

Sejak awal pembuatan, blog saya hanya menggunakan React. Cara saya menyimpan artikel sudah berubah—mulai dari HTML statis di basis data hosted, hingga file markdown di satu repository git—namun saya tetap menggunakan mesin rendering yang sama.

Tidak seperti kebanyakan blog yang menggunakan React, saya tidak menggunakan MDX. Saya menulis markdown seperti biasa, tetapi saya menyebutnya MDC.

MDC

Mudahnya, MDC adalah markdown dengan tambahan berupa Custom Elements di dalamnya. Saya sengaja menggunakan custom elements daripada JSX untuk membatasi kustomisasi dari artikel itu sendiri. Jika saya memang ingin membuat artikel yang interaktif, saya akan membuat halaman next.js lain (dengan atau tanpa MDX).

Custom elements digunakan untuk menyediakan cara yang standar untuk melampirkan widget pihak ketiga, salah satunya twitter-card.

Saya menulis markdown seperti **ini** dan menggunakan custom elements seperti berikut
<twitter-card src="https://twitter.com/pveyes/status/1401182609917448196" caption="Teaser tweet untuk artikel ini"></twitter-card>

Kemudian twitter-card akan ditampilkan seperti berikut:

Plot twist? Saat di-render, custom elements sebenernya diubah menjadi komponen React menggunakan htmr.

import htmr from 'htmr';
import TwitterCard from './post/TwitterCard';
function Blog({ html }) {
return htmr(html, {
transform: {
'twitter-card': TwitterCard,
},
});
}

Itu sampai hari ini.

Custom Elements Asli

Memang rencana saya dari awal untuk menggunakan custom elements asli pada widget pihak ketiga, karena komponen ini tidak membutuhkan server rendering, tapi saya belum ada waktu untuk melakukannya.

Saya memiliki persyaratan untuk implementasinya:

  1. Tidak ada perubahan pada build system
  2. Memuat kode custom elements secara dinamis sesuai permintaan
  3. Interoperabilitas dengan sistem tema yang saya miliki saat ini

Persyaratan ini memastikan saya hanya perlu mengubah sedikit struktur blog.

Tidak Ada Konfigurasi Build

Ada beberapa abstraksi untuk membuat custom elements, yang paling popular adalah Lit

note

Artikel ini tidak akan menjelaskan dasar penggunaan & penulisan custom elements / Lit. Jika diperlukan, bacaan singkat tentang dasar penggunaan custom elements atau Lit sudah cukup.

Lit sangat menekankan penggunaan fitur decorators di JavaScript yang masih dalam tahap eksperimen, artinya kalian harus mengkonfigurasi build system. Setelah membaca dokumentasi secara menyeluruh, saya menemukan bahwa kita bisa menggunakan Lit tanpa decorators.

  1. Alih-alih menggunakan @custom-element(), kita bisa memanggil customElements.define
  2. Alih-alih menggunakan @property(), kita dapat mendefinisikannya di dalam static properties
  3. Alih-alih menggunakan @state(), kita dapat mendefinisikan sebuah property dan menggunakan opsi state: true
import { LitElement, html, css } from 'lit';
export default class TwitterCard extends LitElement {
static properties = {
src: { attribute: true },
caption: { attribute: true },
data: { state: true },
};
// for TypeScript
src: string;
caption: string;
data: any;
static styles = css`
figcaption {
font-size: 0.875rem;
}
`;
render() {
return html`
<figure>
<figcaption>${this.caption}</figcaption>
</figure>
`;
}
}
customElements.define('twitter-card', TwitterCard);

Tantangan lain yang berhubungan dengan langkah build adalah penggunaan class polyfill. Seperti yang mungkin kalian ketahui, custom elements hanya bisa didefinisikan menggunakan class JavaScript yang asli, artinya kita tidak bisa melakukan transpile ke ES5. Kalian harus menambahkan custom-elements-es5-adapter polyfill untuk memperbaiki isu ini, tetapi polyfill itu sendiri tidak bisa di-transpile.

Ada beberapa solusi dari masalah ini, salah satunya dengan menggunakan vendor-copy yang tidak sesuai persyaratan saya. Solusi saya adalah dengan melakukan inline kode polyfill menggunakan raw.macro di dalam file _document:

import Document, { Html, Head, Main, NextScript } from 'next/document';
import raw from 'raw.macro';
export default class CustomDocument extends Document {
render() {
return (
<Html>
<Head>
<script
type="text/javascript"
dangerouslySetInnerHTML={{
__html: raw(
'@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js',
),
}}
/>
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}

Impor Dinamis

Persyaratan selanjutnya adalah untuk memuat kode custom elements hanya jika digunakan pada sebuah artikel. Untuk melakukan ini, saya membuat sebuah komponen loader dan memanfaatkan sintaks impor dinamis.

import htmr from 'htmr';
import CustomElements from './post/custom-elements/Loader';
function Blog({ html }) {
return htmr(html, {
transform: {
'twitter-card': CustomElements('twitter-card', () => import('./post/custom-elements/TwitterCard))
}
});
}

Komponen loader ini bertanggung jawab untuk mendaftarkan custom elements, dan juga memuat kode yang dibutuhkan untuk me-render komponen.

import { createElement, useEffect } from 'react';
export default function CustomElementLoader(
name: string,
importFn: () => Promise<any>,
) {
return function Component(props: any) {
useEffect(() => {
Promise.race([
importFn().then((mod) => {
customElements.define(name, mod.default);
}),
customElements.whenDefined(name),
]);
}, [name]);
return createElement(name, props);
};
}

Interoperabilitas dengan Sistem Tema

Saya menggunakan theme-in-css untuk membuat sitem tema yang memanfaatkan CSS Custom Properties. Karena ini adalah kerangka kerja agnostik, saya seharusnya bisa menggunakannya pada deklarasi style di LitElement. Untuk melakukan itu, saya harus menggunakan unsafeCSS karena nilainya didefinisikan di variabel terpisah.

Karena saya hanya menggunakannya untuk merujuk ke variabel CSS, saya menggunakan alias cv pada impor.

import { LitElement, css, unsafeCSS as cv } from 'lit';
import { Theme } from '@paper/origami';
export default class TwitterCard extends LitElement {
static styles = css`
figure > *:first-child {
margin-bottom: ${cv(Theme.spacing.s)};
}
`;
}

Begitulah. Secara keseluruhan saya puas dengan susunan ini. Jika kalian berencana untuk integrasi custom elements (dengan atau tanpa Lit), semoga artikel ini bisa berguna sebagai referensi tambahan.

Namun, ada beberapa hal yang menurut saya cukup mengganggu dari Lit.

  1. Isu CLS. Karena custom elements di-render di sisi klien, pemuatan terjadi melalui tiga tahap: sebelum kode dimuat, sebelum data diterima, dan siap. Hal ini buruk untuk CLS, terutama jika komponen ini ditampilkan di bagian atas artikel. Saya masih melakukan eksperimen untuk mendapatkan solusi yang optimal.
  2. Tidak bisa menggunakan sintaks nested CSS selector. Ini artinya saya harus menulis selector yang sama berulang kali jika menggunakan pseudo class seperti :hover atau kombinasi selector lain. Mengingat semua varian CSS-in-JS mendukung fitur ini, saya sangat berharap fitur ini juga dapat didukung.
  3. Untuk menggunakan tag constructor khusus (createElement(tag, props) di React), saya harus menggunakan unsafeStatic dan html tagged template yang berbeda. Saya mengerti ini kasus penggunaaan yang jarang dan maju, tapi menurut saya lebih baik jika hanya menggunakan unsafeStatic dengan fungsi html yang sama.
  4. Tidak adanya dukungan sintaks yang mempermudah seperti spread props <Component {...props} />, dan self closing tag <twitter-card />.

Ini bukanlah kritik terhadap Lit atau custom elements, kita bisa berdebat apakah "fitur" ini dibutuhkan atau tidak. Saya hanya membagikan persepektif dari pengembang yang sehari-hari bekerja menggunakan React. Mungkin saya sedikit lebih paham mengapa beberapa pengembang React ragu-ragu untuk menggunakan web components. Mengingat preact + goober digabung bisa jadi lebih powerful dan masih memiliki ukuran bundle yang lebih kecil dari Lit, akan sangat susah untuk justifikasi penggunaan custom elements.

Pada akhirnya, saya percaya bahwa semua turun ke pilihan pribadi. Saya akan tetap menggunakan custom elements untuk widget pihak ketiga di artikel saya, sedangkan sisanya masih menggunakan JSX.

Dikategorikan dalam

Webmentions

Jika kalian pikir artikel ini berguna