Design System com StencilJS para seus web components
Uma única ferramenta para criar componentes e utilizar em qualquer framework
Dias atrás estava pensando sobre Design system e sua importância e como ele ajuda no desenvolvimento frontend de cada de dia. De repente me deparei com uma questão: todos os design systems que trabalhei até hoje, NÃO são agnósticos a framework. Isso pode não parecer um problema, mas quando começamos a trabalhar com micro frontends e cada time escolhe a tecnologia que deseja trabalhar, um DS(design system) não agnóstico a framework pode ser bem desvantajoso para os times e a empresa no geral.
Diante disso, conhecendo a alternativa mais coerente, pensei automaticamente em Web Components, mas logo depois já desisti, porquê convenhamos, criar componentes na unha com javascript é bem trabalhoso.
E foi nesse momento que eu lemebrei dele, o famigerado StencilJS.
Explicando o porquê das coisas
Vamos entender primeiramente porquê o stencilJS pode te ajudar a criar seus web components mais facilmente.
Vamos para o código.
Suponhamos que queremos criar um componente com javascript puro. Vamos adicionar passagem de props para deixar uma pouquinho mais complexo:
// Javascript para criar o componente
<script>
var NovoComponente = document.registerElement('loading-componente', {
prototype: Object.create(HTMLElement.prototype, {
createdCallback: {
value: function(){
var
link = document.querySelector('link[rel=import]'),
t = link.import.querySelector('#tpl-loading'),
clone = document.importNode(t.content, true);
element = this,
text = this.getAttribute('text'),
animates = clone.querySelectorAll('.load-anime div');
clone.querySelector('.load-anime').classList.add("circle");
clone.querySelector('.loading-text').innerHTML = text;
element.createShadowRoot().appendChild(clone);
}
}
})
});
</script>
// HTML utlizando o componente
<loading-componente text="Loading"></loading-componente>
Ref:https://www.devmedia.com.br/web-components-na-pratica/32476
Imagina um componente maior ainda que precise de mais props, e funções e observabiliade. Só de pensar já me dói a cabeça. E é por isso que pensar em algo que ajude a pessoa a desenvolver de forma mais rápida e igualmente funcional, é o motivo por de trás de queremos usar StencilJS.
Criando um componente com StencilJS,
Agora vamos ver como seu criar um componente com StencilJS fica muito mais simples. Primeiramente vamos criar um projeto monorepo para não precisamos fazer deploy da lib que vamos criar. Neste exemplo vou tilizar Turborepo, pnpm e vite para construir nossa aplicação. A estruturas de pasta ficará assim:
packages/
└──app-react
└──app-vue
└──react-lib
└──stencil-library
└──vue-lib
├── pnpm.workspace.yaml
├── turbo.json
├── tsconfig.json
Após criar o monorepo vamos agora criar a pasta stencil-library. Para isso vamos criar dentro de packages com o comando:
npm init stencil
Seguindo as orientações da CLI, você irá criar seu pacote da lib de componentes. Feito isso, podemos ver que já temos uma pasta com componente criado para nosso exemplo.
Dentro da pasta src/components temos o componente my-component. Vamos analisar um pouco esse componente
import { Component, Prop, h } from '@stencil/core';
import { format } from '../../utils/utils';
@Component({
tag: 'my-component',
styleUrl: 'my-component.css',
shadow: true,
})
export class MyComponent {
/**
* The first name
*/
@Prop() first: string;
/**
* The middle name
*/
@Prop() middle: string;
/**
* The last name
*/
@Prop() last: string;
private getText(): string {
return format(this.first, this.middle, this.last);
}
render() {
return <div>Hello, World! I'm {this.getText()}</div>;
}
}
Olha que maravilha de componente. Basicamente temos uma classe que informamos o nome do componente, onde esta o CSS e se utilizará o shadow DOM. Observe que temos as props para utilizarmos em nosso componente de uma forma muito mais simples do que em web componentes sem StencilJS.
Criando nossa lib de componentes para Vue3
Agora que já temos um parâmetro de como é um componente full web components, e um componente criado a partir do StencilJS, podemos agora ver como exportar esse componete para podemos consumir ele. Antes disso vamos criar agora o pacote que será para receber nossos componentes via Stencil e podemos usar como lib para qual frameworks quisermos. Neste artigo vamos criar para VueJS e ReactJS. Crie agora um projeto simples dentro de package onde você dara um npm init e instalará as dependencias necessárias. Para vue, vamos rodar o seguinte comando:
npm install vue@3 --save-dev
Como estou exemplificando com monorepo, o package.json do projeto ficará assim:
{
"version": "0.0.0",
"name": "vue-lib",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"test": "echo \"Error: run tests from root\" && exit 1",
"build": "npm run tsc",
"tsc": "tsc -p ."
},
"publishConfig": {
"access": "public"
},
"dependencies": {
"stencil-library": "*"
},
"devDependencies": {
"vue": "3"
}
}
Observe que a dependênci astencil-library ela é necessária para podermos ter nossos componentes importados aqui na vue-lib (pensa na vue-lib como a biblioteca de design systema em Vue).
Rode no terminal um npm install para que possamos importar a lib.
Na raiz do seu projeto vue-lib crie um arquivo tsconfig.json e adicione:
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"lib": ["dom", "es2020"],
"module": "es2015",
"moduleResolution": "node",
"target": "es2017",
"skipLibCheck": true
},
"include": ["lib"],
"exclude": ["node_modules"]
}
O extends na primeira linha irá olhar para o tscofig.json que esta no projeto raiz do monorepo.
Agora vamos voltar na nossa lib stencil-lib para exportarmos o componente para dentro da nossa lib de componentes em VueJS.
No arquivo stencil.config.ts vamos adicionar a seguinte configuração:
import { Config } from '@stencil/core/internal';
import { vueOutputTarget } from '@stencil/vue-output-target';
export const config: Config = {
namespace: 'stencil-library',
outputTargets: [
{
type: 'dist',
esmLoaderPath: '../loader'
},
vueOutputTarget({
componentCorePackage: 'stencil-library',
proxiesFile: '../vue-lib/lib/components.ts',
}),
],
};
Adicione o vue-output-target rodando o seguinte comando no terminal:
npm i @stencil/vue-output-target
Agora sim, vamos ver. Observe que em vueOutputTarget temos o caminho apontado para nossa vue-lib. Agora podemos rodar o seguinte comando no terminal e ver mágica acontecer:
npm run build
Esse comando de build irá criar os componentes lá dentro da nossa pasta onde está a vue-lib. Podemos conferir lá que foi criado uma pasta lib e dentro temos nosso componente. Algo assim:
/* eslint-disable */
/* tslint:disable */
/* auto-generated vue proxies */
import { defineContainer } from './vue-component-lib/utils';
import type { JSX } from 'stencil-library';
export const MyComponent = /*@__PURE__*/ defineContainer<JSX.MyComponent>('my-component', undefined, [
'first',
'middle',
'last'
]);
Agora o que temos que fazer é deixar essa lib disponivel para ser consumida em nosso projeto. Dentro da pasta lib crie dois arquivos. Um index.ts e outro com o nome de plugin.ts. No plugin.ts vamos adicionar:
import { Plugin } from 'vue';
import { applyPolyfills, defineCustomElements } from 'stencil-library/loader';
export const ComponentLibrary: Plugin = {
async install() {
applyPolyfills().then(() => {
defineCustomElements();
});
},
};
E no index.ts vamos apenas exportar o componente e o plugin:
export * from './components';
export * from './plugin';
Agora já podemos também, buidar nossa vue-lib com o comando:
npm run build
Se estiver usando o monorepo com turborepo, não se esqueça de rodar pnpm install no workspace para o turborepo gerenciar as dependências de cada pacote
Utilizando nosso componente em um App Vue3
Agora, seguindo o mesmo modelo de monorepo, vamos criar nosso app com VueJS. Neste exemplo utilizei Vue3 com Vite. Dentro do nosso package vamos adicionar o app-vue, que será nada mais do que um app vue criado com vite. Vamos adicionar no package.json nossa dependência vue-lib:
"dependencies": {
"vue": "^3.2.47",
"vue-lib": "*"
},
Executar um npm install.
Agora vamos no arquivo main.ts e adicionamos:
import { ComponentLibrary } from 'vue-lib';
createApp(App).use(ComponentLibrary).mount('#app');
Prontinho, agora já podemos utilizar nosso componente criado lá no StencialJS.
<my-component first="dev" middle="Rebeca" last="Lopes"></my-component>
Ficando dessa forma. O grifado em vermelho é exatamente o que está criado lá na nossa stencil-library.
Criando nossa lib de componentes para React
Mas eu quero utililizar o mesmo componente mas em ReactJs. Sem problemas. Vamos fazer o mesmo processo. Criar uma pasta dentro de packages com o nome de react-lib. Criamos novamente um projeto simples com npm init e adicionamos o que precisamos com o comando:
npm install react react-dom typescript @types/react --save-dev
Ficando dessa forma nosso package.json
{
"version": "0.0.0",
"name": "react-library-reb",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/types/index.d.ts",
"scripts": {
"test": "node ./__tests__/react-library.test.js",
"build": "npm run tsc",
"tsc": "tsc -p ."
},
"files": [
"lib",
"dist"
],
"publishConfig": {
"access": "public"
},
"dependencies": {
"stencil-library": "*"
},
"devDependencies": {
"@types/node": "^18.15.11",
"@types/react": "^18.0.31",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"typescript": "^5.0.3"
}
}
Novamente, observe a importação da nossao stencil-library. Nao esqueça de adiciona-la e rodar npm install para que a lib seja instalada no projeto. Agora na raiz desse projeto (react-lib) vamos adicionar o nosso arquivo tsconfig.json
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"lib": ["dom", "es2015"],
"module": "es2015",
"moduleResolution": "node",
"target": "es2015",
"skipLibCheck": true,
"jsx": "react",
"allowSyntheticDefaultImports": true,
"declarationDir": "./dist/types"
},
"include": ["lib"],
"exclude": ["node_modules"]
}
Lemebre-se que o extends faz referencia ao arquivo tsconfig.json que esta no workspace do monorepo.
Agora, vamos voltar em nossa lib stencil-library. E modificar um pouco o arquivo stencil.config.ts:
import { Config } from '@stencil/core/internal';
import { reactOutputTarget } from '@stencil/react-output-target';
export const config: Config = {
namespace: 'stencil-library',
outputTargets: [
{
type: 'dist',
esmLoaderPath: '../loader'
},
reactOutputTarget({
componentCorePackage: 'stencil-library',
proxiesFile: '../react-lib/lib/components/stencil-generated/index.ts',
})
],
};
E vamos instalar a seguinte dependência:
npm i @stencil/react-output-target
Observe o reactOutputTarget agora ele aponta para nosso outro pacote, o react-lib. Podemos fazer novamente o build com npm run build, e veremos nosso componente ser criado dentro do pacote react-lib/lib.
Agora, vamos abrir o arquivo index.ts que foi criado dentro de react-lib/lib/components/stencil-generated/index.ts. E adicionar a seguinte linhas logo abaixo de tudo:
export * from "./react-component-lib";
export { defineCustomElements } from "stencil-library/loader"
Dessa forma o arquivo final ficará assim:
/* eslint-disable */
/* tslint:disable */
/* auto-generated react proxies */
import { createReactComponent } from './react-component-lib';
import type { JSX } from 'stencil-library';
export const ButtonDs = /*@__PURE__*/createReactComponent<JSX.ButtonDs, HTMLButtonDsElement>('button-ds');
export const MyComponent = /*@__PURE__*/createReactComponent<JSX.MyComponent, HTMLMyComponentElement>('my-component');
export * from "./react-component-lib";
export { defineCustomElements } from "stencil-library/loader"
Essas duas linhas a mais são necessárias para fazermos nosso export do componente.
Vamos rodar um npm run build para construir nosso react-lib e podermos assim importar em nossa aplicação react.
Utilizando nosso componente em um App React
Vamos criar outro pacote em nosos monorepo, e será o app-react. Esse projeto você pode criar utilizando vite também (recomendável). Após criar teremos um package.json assim:
{
"name": "app-react",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-lib": "*"
},
"devDependencies": {
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@vitejs/plugin-react": "^3.1.0",
"typescript": "^4.9.3",
"vite": "^4.2.0"
}
}
Mais uma vez, não se esqueça de importar agora nosssa lib de componentes react nas dependências, react-lib, e rodar npm install.
Agora podemos importar nosso componente dentro da lib e utilizar normalmente em um App React.
Observe que não precisamos trocar nada no componente para poder ser adaptado para VueJS ou ReactJs. É a mesma base de código o StencilJS builda para ambos os frameworks.
Em src/App.tsx vamos importar dessa forma:
import { MyComponent, defineCustomElements } from 'react-lib';
defineCustomElements();
E agora usamos:
<MyComponent first="dev" middle="Rebeca" last="Lopes" />
O resultado ficará dessa forma. A parte grifada em vermelho é a renderização do nosso componente criado com StencilJS.
Precauções
Como nem tudo são flores, devos destacar aqui alguns contras que pode ser levado em consideração. Web components ainda não é muito compatívelcom questões de acessibilidade. Ainda está em fase de aprimoramento para utilizar SSR. Não funciona com SVG e compartilhamento global de namespace e ao invés de ser modular
Conclusão
Web components para Design System pode ser uma boa ideia se tratando de projetos que utilizam frameworks diferentes. Mas criar um web component pode ser bem trabalhoso, por isso podemos optar por utilizar o StencilJS para abstrair boa parte dos conceitos de web components e poder renderizar tranquilamente seus componentes em qual framework preferir. Claro que nem tudo são flores, de deve ser uma escolha feita com cuidado, sabendo de algumas limitações que poderão ocorrer.