Charlas

Potencia el desarrollo de su interfaz con componentes web

En esta charla haremos una breve introducción a los web components y cómo estos pueden mejorar la Experiencia de Usuario y unificar la Interfaz de Usuario. Iremos desde ejemplos simples de composición hasta ejemplos complejos con inyección de dependencias para integrarlos en arquitecturas hexagonales o modificar su funcionamiento según dónde los incorporemos. En resumen, un recorrido completo sobre las bases de los web components hasta cómo implementar i18n, signals, context. Todo lo que necesitas saber para empezar a incorporar web components en tus interfaces de usuario.

Javascript
Typescript
Web Components
Foto desde el escenario a la audiencia momentos antes de comenzar la charla.

El evento

La conferencia más importante del mundo tecnológico en Europa

El congreso Codemotion Madrid es uno de los eventos más importantes con una asistencia de más de 1.500 asistentes, más de 100 ponentes y 80 sesiones. Se lleva a cabo en Kinépolis y conecta a los desarrolladores para encontrar inspiración y sentirse parte de algo único.

Las diapositivas

Potencia el desarrollo
de tu interfaz con
Web Components

Lego building

Proyecto que se va de las manos

Jorge del Casar

Head of Tech at ActioGlobal

Google Developer Expert

+20 years working on web

Jorge del Casar Jorge del Casar

¿Qué son los Web Components?

Un conjunto de APIs para crear nuevas etiquetas HTML

  • personalizadas
  • reutilizables
  • encapsuladas

¿De qué se componen?

3 APIs nativas del navegador

  • Custom Elements
  • Shadow DOM
  • HTML Templates

Tu primer Web Component

Código


Importar

<script type="module" src="./simple-greeting.js"></script>

Usar

<simple-greeting name="World"></simple-greeting>

Mostrar

El ciclo de vida

connectedCallback

Invocado cuando se conecta por primera vez al DOM del documento.

disconnectedCallback

Invocado cuando se desconecta del DOM del documento.

adoptedCallback

Invocado cuando se mueve a un nuevo documento.

attributeChangedCallback

Invocado cuando uno de los atributos es añadido, removido o modificado.

Lit es una librería sencilla para crear componentes web rápidos y ligeros.

Código

		import { html, css, LitElement } from 'lit';

class SimpleGreetingLit extends LitElement {
	static styles = css`
		p {
			color: var(--accent-regular);
		}
	`;

	static properties = {
		name: { type: String },
	};

	constructor() {
		super();
		this.name = 'alguien';
	}

	render() {
		return html`

Hola, ${this.name}!

`; } } customElements.define('simple-greeting-lit', SimpleGreetingLit);

Importar

<script type=" module" src="./simple-greeting.lit.js" />

Usar

<simple-greeting-lit name="Lit"></simple-greeting-lit>

Mostrar

Composición

Composición de components

El proceso de construir un componente grande y complejo a partir de componentes más pequeños y simples.

Código

<list-item>
<img
slot="picture"
src="./piet_mondrian.jpg"
/>
<h3>Piet Mondrian</h3>
</list-item>

Resultado

Piet Mondrian

Mixin

Son un patrón para compartir código entre clases utilizando JavaScript estándar.

Mixin de Lista con Imagen

		import { html } from 'lit';
import { listWithPictureStyles } from './listWithPictureStyles.css.js';
import './list-item.js';

export const ListWithPictureMixin = (superClass) => class extends superClass {

	static styles = listWithPictureStyles;

	error() {
		return html`

There was an error

`; } pending() { return html`

Loading...

` } complete = (items) => { if (items.length) { return items.map(this.item); } return this.empty(); } empty() { return html`

There are no content

`; } item = (item) => { return html`

${item.name}

`; } };

Lista con Imagen usando un Mixin

		import { LitElement } from 'lit';
import { ListWithPictureMixin } from './ListWithPictureMixin.js';

export class ListWithPicture extends ListWithPictureMixin(LitElement) {

	static properties = {
		items: { type: Array },
	};

	constructor() {
		super();
		this.items = [];
	}

	render() {
		if (this.items) {
			return this.complete(this.items)
		}
		return this.error();
	}
}
	

Resultado

Controlador

Es un objeto que puede conectarse al ciclo de actualización reactiva de un componente.

Controlador Fetch

		export class FetchController {

	constructor(host, endpointFn) {
		(this.host = host).addController(this);
		this.endpointFn = endpointFn;
	}

	hostUpdated() {
		const endpoint = this.endpointFn();
		if (endpoint !== this._previousEndpoint) {
			this.run(endpoint);
		}
	}

	async run(endpoint) {
		if (endpoint) {
			try {
				this.value = await fetch(endpoint)
					.then(response => response.json());
			} catch (error) {
				console.error(error);
				this.value = null;
			}
		} else {
			this.value = [];
		}
		this._previousEndpoint = endpoint;
		this.host.requestUpdate();
	}
}
	

Lista con imagen y fetch

		import { html, nothing } from 'lit';
import { FetchController } from './FetchController.js';
import './list-with-picture.js';
import { ListWithPicture } from './ListWithPicture.js';

class ListWithPictureFetch extends ListWithPicture {

	data = new FetchController(this, () => this.endpoint);

	static properties = {
		value: { type: String }
	};

	get endpoint() {
		if (!this.value) {
			return '';
		}
		return `/deck/ui-web-components/data/${this.value}.json`;
	}

	update(changedProperties) {
		super.update(changedProperties);
		this.items = this.data.value;
	}

	render() {
		return html`
			${this.renderSelect()}
			${super.render()}
		`;
	}

	renderSelect() {
		return html``;
	}

	handleChange = (event) => {
		this.value = event.target.value;
	}
}
customElements.define('list-with-picture-fetch', ListWithPictureFetch);
	

Resultado

Manejando datos

Tarea Asíncrona

Es una operación asíncrona que realiza una acción para obtener datos y devolverlos en una Promesa.

Code

		import { Task } from '@lit/task';
import { LitElement } from 'lit';

import { ListWithPictureMixin } from '../composition/ListWithPictureMixin.js';

export class ListWithPictureTask extends ListWithPictureMixin(LitElement) {

	static properties = {
		endpoint: { type: String },
	}

	task = new Task(this, {
		args: () => [this.endpoint],
		task: async ([endpoint], { signal }) => {
			const response = await fetch(endpoint, { signal });
			if (!response.ok) { throw new Error(response.status); }
			return response.json();
		}
	});

	render() {
		if (this.task) {
			return this.task.render({
				pending: this.pending,
				complete: this.complete,
				error: this.error
			});
		}
		return this.empty();
	}
}
	

Código

<list-with-picture-task endpoint="/api/artists"></list-with-picture-task>

Resultado

Código

<list-with-picture-task endpoint="/api/ccaa"></list-with-picture-task>

Resultado

Contexto

Es una forma de hacer que los datos estén disponibles para sin tener que vincular manualmente propiedades a cada componente

Crear contexto

		import { createContext } from '@lit/context';

export const contextList = createContext('list');
	

Consumir contexto

		import { ContextConsumer } from '@lit/context';
import { LitElement } from 'lit';
import { contextList } from './contextList.js';
import { ListWithPictureMixin } from '../composition/ListWithPictureMixin.js';

export class ListWithPictureContext extends ListWithPictureMixin(LitElement) {

	context = new ContextConsumer(this, {
		context: contextList,
		subscribe: true,
		callback: (taskFactory) => {
			this.task = taskFactory(this);
		}
	});

	render() {
		if (this.task) {
			return this.task.render(this);
		}
		return this.empty();
	}
}
	

Proveer contexto

		import { ContextProvider } from '@lit/context';
import { Task } from '@lit/task';
import { LitElement, html } from 'lit';
import { contextList } from './contextList.js';

export class ListContextProvider extends LitElement {

	#context = new ContextProvider(this, {
		context: contextList
	});

	context = 'No implementado';

	static properties = {
		context: { type: String },
	};

	task = async () => {
		throw new Error('Task not implemented');
	}

	taskFactory = (host) => new Task(host, {
		task: this.task,
		args: () => []
	});

	constructor() {
		super();
		this.#context.setValue(this.taskFactory);
	}

	render() {
		return html`
			

Drop a component to provide the context

${this.context}

`; } }

Proveer contexto artistas

		import { ListContextProvider } from "./ListContextProvider.js";

const endpoint = '/deck/ui-web-components/data/artists.json';

export class ListContextProviderArtists extends ListContextProvider {

	context = 'Artistas';

	task = async ({ signal }) => {
		const response = await fetch(endpoint, { signal });
		if (!response.ok) { throw new Error(response.status); }
		return response.json();
	}
}
	

Uso

		
	




	

Mismo componentes en diferentes contextos

Signals

Son estructuras de datos para gestionar el estado observable.

Crear signal

		import { signal } from "@lit-labs/signals";

export const count = signal(0);
	

Mostrar signal

		import { SignalWatcher, watch } from '@lit-labs/signals';
import { html, LitElement } from 'lit';
import { count } from './signalCount.js';

export class SelectedItemsSignals extends SignalWatcher(LitElement) {

	render() {
		return html`

Seleccionados: ${watch(count)}

`; } }

Asignar signal

		import { SignalWatcher } from '@lit-labs/signals';
import { html, css } from 'lit';
import { ListWithPictureContext } from './ListWithPictureContext.js';
import { count } from './signalCount.js';

export class ListWithPictureSignals extends SignalWatcher(ListWithPictureContext) {

	static styles = [
		super.styles,
		css`.added { background: var(--surface-2); }`
	]

	item = (item) => {
		return html`
			
			

${item.name}

`; } #onClick(event) { const { classList } = event.currentTarget; if (classList.contains('added')) { count.set(count.get() - 1); classList.remove('added'); } else { count.set(count.get() + 1); classList.add('added'); } } }

Selecciona tus favoritos

Internacionalización

Localization

Es el proceso de soportar múltiples idiomas y regiones en tu app y componentes.

Instalación

Instala la biblioteca de cliente y la CLI


npm i @lit/localize
npm i -D @lit/localize-tools

Envuelve el texto en la función msg

		import { SignalWatcher, watch } from '@lit-labs/signals';
import { html, LitElement } from 'lit';
import { msg, updateWhenLocaleChanges } from '@lit/localize';
import { count } from '../manage-data/signalCount.js';
import './locale-picker.js';

export class SelectedItemsSignalsI18n extends SignalWatcher(LitElement) {

	constructor() {
		super();
		updateWhenLocaleChanges(this);
	}

	render() {
		const num = watch(count);
		return html`
			

${msg(html`Seleccionados: ${num}`)}

`; } }

Crea un fichero de configuración lit-localize.json

{
"$schema": "https://raw.githubusercontent.com/lit/lit/main/packages/localize-tools/config.schema.json",
"sourceLocale": "es",
"targetLocales": ["en"],
"inputFiles": ["public/**/*.js"],
"output": {
"mode": "runtime",
"outputDir": "./public/generated/locales",
"localeCodesModule": "./public/generated/locale-codes.js"
},
"interchange": {
"format": "xliff",
"xliffDir": "./xliff/"
}
}

Ejecuta lit-localize extract para generar los ficheros XLIFF.

Edita el fichero XLIFF generado para añadir la traducción

Ejecuta lit-localize build para generar una versión localizada de tus textos.

Modos de salida

  • Tiempo de ejecución
  • Transformación

Tiempo de ejecución

		import { configureLocalization } from '@lit/localize';
// Generated via output.localeCodesModule
import { sourceLocale, targetLocales } from '../../../generated/locale-codes.js';

export const { getLocale, setLocale } = configureLocalization({
	sourceLocale,
	targetLocales,
	loadLocale: (locale) => import(`/generated/locales/${locale}.js`),
});

export const setLocaleFromUrl = async () => {
	const url = new URL(window.location.href);
	const locale = url.searchParams.get('locale') || sourceLocale;
	await setLocale(locale);
};
	

Selecciona tus favoritos

Conclusión

Referencias

¡Gracias!

¿Preguntas?