Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 37 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,53 @@
<p align="center">
<img src="./assets/logo.jpg" alt="Curso WebComponents" width="50%">
</p>

# Temario: WebComponents de Cero a Dios (Clean Code & Modern Standard)

![Curso WebComponents](./assets/logo.jpg)

Este es el plan de estudios estructurado de forma progresiva. Conforme avanzamos, iré marcando los módulos completados.

- [x] 📌 Módulo 1: Setup del Entorno & Fundamentos Clave (Actual).
- 📍 **[Apuntes](./src/components/P-001/)**
- 🎟️ **[P-001 | Crear componente de insignia de estado (status-badge)](https://github.com/Code-Dojo-Labs/curso-webcomponents/issues/18)**
- 📝 **[Apuntes](./src/components/P-001/)**

---

- [x] 📌 Módulo 2: Custom Elements (v1) & Lifecycle Callbacks.
- 📍 **[Apuntes](./src/components/P-002/)**
- 🎟️ **[🏗️ P-002 | Hacer reactivo el componente status-badge a cambios de estado en vivo](https://github.com/Code-Dojo-Labs/curso-webcomponents/issues/22)**
- [ ] 📌 Módulo 3: Shadow DOM & Encapsulamiento Visual.
- 📍 **[Apuntes](./src/components/P-003/)**
- 📝 **[Apuntes](./src/components/P-002/)**

---

- [x] 📌 Módulo 3: Shadow DOM & Encapsulamiento Visual.
- 🎟️ **[🏗️ P-003 | Encapsular el componente status-badge usando Shadow DOM y Adopted StyleSheets](https://github.com/Code-Dojo-Labs/curso-webcomponents/issues/26)**
- 📝 **[Apuntes](./src/components/P-003/)**

---

- [x] 📌 Módulo 4: `<template>` y `<slot>` (Composición de UI).
- 🎟️ **[P-004 | Crear un componente contenedor de perfil (user-profile-card) utilizando slots](https://github.com/Code-Dojo-Labs/curso-webcomponents/issues/30)**
- 📝 **[Apuntes](./src/components/P-004/)**

---

- [ ] 📌 Módulo 4: `<template>` y `<slot>` (Composición de UI).
- [ ] 📌 Módulo 5: Custom Events & Arquitectura de Comunicación Unidireccional.

---

- [ ] 📌 Módulo 6: Clean Code & Patrones de Diseño aplicados a WebComponents.

---

- [ ] 📌 Módulo 7: Herramientas del Ecosistema Moderno (Lit / FastElement).

---

- [ ] 📌 Módulo 8: Testing Unitario y E2E (Web Test Runner / Playwright).

---

- [ ] 📌 Módulo 9: Integración de IA en el Flujo de Desarrollo (IA-Driven Development).

---

- [ ] 📌 Módulo 10: Proyecto Final, Publicación NPM y Arquitectura Nivel Dios.

---
12 changes: 12 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,18 @@ <h1>P-003 : Status Badge DOM</h1>
<status-badge-dom seconds="3"></status-badge-dom>
</div>
</div>
<div>
<h1>P-004 : Usando Templates y Slots</h1>
<div>
<user-profile>
<img slot="avatar" src="./src/assets/gato.png" alt="User Profile">
<span slot="name">Gato</span>
<status-badge-dom slot="status" state></status-badge-dom>
</user-profile>
</div>
</div>


</main>
<!-- Invocador principal de webcomponents -->
<script type="module" src="./src/index.js"></script>
Expand Down
Binary file added src/assets/gato.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
178 changes: 178 additions & 0 deletions src/components/P-004/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
**[◀️ Regresar](../../../README.md)**

# 📕 Lección 4: `<template>` y `<slot>` (Composición de UI)

Hasta el momento, tus componentes han renderizado su contenido insertando directamente cadenas de texto HTML mediante innerHTML. Aunque es funcional, en componentes complejos reescribir todo el marcado mediante strings puede penalizar el rendimiento y dificulta la composición.

La especificación de WebComponents resuelve esto con dos elementos nativos:

1. Etiqueta `<template>`: Permite declarar fragmentos de HTML inertes que el navegador parsea una sola vez pero no renderiza hasta que son clonados e instanciados en JS.

2. Mecanismo de Proyección `<slot>`: Actúa como un placeholder o "puerto" dentro de tu Shadow DOM donde el usuario del componente puede inyectar su propio marcado HTML desde el documento principal.

## Tipos de Slots

- Default Slot (`<slot></slot>`): Captura todo el contenido hijo que no tenga un atributo slot especificado.

- Named Slots (`<slot name="header"></slot>`): Capturan únicamente los elementos que coincidan con slot="header".

## Adopted StyleSheets (Práctica Profesional)

En lugar de inyectar etiquetas `<style>` dentro de cada instancia como un string (lo cual consume memoria innecesaria), la especificación moderna utiliza CSSStyleSheet() y adoptedStyleSheets para compartir estilos compilados entre múltiples instancias.

## Ejemplo Práctico

A continuación se muestra la forma profesional y limpia de adjuntar un Shadow DOM utilizando attachShadow y hojas de estilo adoptadas:

```javascript
//Archivo: src/components/ui-card.js
const template = document.createElement("template");
template.innerHTML = `
<style>
:host {
display: block;
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
font-family: system-ui, sans-serif;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
}
.header {
background-color: #f5f5f5;
padding: 12px 16px;
border-bottom: 1px solid #e0e0e0;
font-weight: bold;
}
.body {
padding: 16px;
}
.footer {
padding: 12px 16px;
background-color: #fafafa;
border-top: 1px solid #e0e0e0;
text-align: right;
}
</style>

<div class="card">
<div class="header">
<!-- Slot con nombre para el título -->
<slot name="title">Título por defecto</slot>
</div>
<div class="body">
<!-- Slot por defecto para el contenido principal -->
<slot></slot>
</div>
<div class="footer">
<!-- Slot con nombre para acciones/botones -->
<slot name="actions"></slot>
</div>
</div>
`;

export class UICard extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
// Clonamos el contenido del template inerte de forma eficiente
this.shadowRoot.appendChild(template.content.cloneNode(true));
}
}

customElements.define("ui-card", UICard);
```

## EJEMPLO PRÁCTICO

A continuación verás un contador que reacciona a cambios de atributo y limpia sus timers al desmontarse.

```javascript
/**
* Componente temporizador reactivo.
* Demuestra el uso completo del ciclo de vida y reactividad nativa.
* Archivo: src/components/timer-counter.js
*/
export class TimerCounter extends HTMLElement {
constructor() {
super();
this._count = 0;
this._timerId = null;
}

// 1. Especificamos qué atributos queremos observar
static get observedAttributes() {
return ["step"];
}

// 2. Hook al insertarse en el DOM
connectedCallback() {
this.render();
this.startTimer();
}

// 3. Hook al cambiar atributos observados
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue !== newValue && name === "step") {
console.log(`[TimerCounter] El paso cambió de ${oldValue} a ${newValue}`);
}
}

// 4. Hook de limpieza cuando se elimina del DOM
disconnectedCallback() {
console.log("[TimerCounter] Removiendo componente del DOM. Limpiando timers...");
clearInterval(this._timerId);
}

get step() {
return parseInt(this.getAttribute("step") || "1", 10);
}

startTimer() {
this._timerId = setInterval(() => {
this._count += this.step;
this.render();
}, 1000);
}

render() {
this.innerHTML = `
<div class="timer-box">
<p>Segundos transcurridos: <strong>${this._count}</strong> (Incremento: ${this.step})</p>
</div>
`;
}
}

customElements.define("timer-counter", TimerCounter);
```

---

```html
<!-- index.html -->
<!doctype html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>WebComponents Mastery</title>
</head>
<body>
<!-- Uso de nuestro WebComponent nativo -->
<ui-card>
<h2 slot="title">Perfil de Usuario</h2>
<p>Esta información es proyectada dentro del slot por defecto del Shadow DOM.</p>
<button slot="actions">Editar</button>
</ui-card>
<script type="module" src="./src/components/ui-card.js"></script>
</body>
</html>
```

Como podemos ver este ejemplo base de un webcomponent.

### [USO DE IA EN DESARROLLO] Asistencia para la Generación de Templates

> Nota de IA: En el desarrollo moderno con WebComponents, puedes apoyarte en un asistente de IA para solicitar layouts de templates optimizados utilizando el siguiente Prompt Pattern:

"Genera un `<template>` HTML nativo de WebComponent encapsulado con Shadow DOM que contenga slots para [Header, Body, Footer]. Aplica estilos CSS estructurados con Flexbox/Grid y CSS variables para personalización."
14 changes: 14 additions & 0 deletions src/components/P-004/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import CardStyles from "./styles.js";
import CardTemplate from "./template.js";

class UserProfile extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.adoptedStyleSheets = [CardStyles];
this.shadowRoot.appendChild(CardTemplate.content.cloneNode(true));
}
}

// Registration of the custom element
window.customElements.define("user-profile", UserProfile);
39 changes: 39 additions & 0 deletions src/components/P-004/styles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
const STYLES = new CSSStyleSheet();

STYLES.replaceSync(`
:host * {
margin: 0;
padding: 0;
}
:host section {
border-radius: 10px;
border: 1px solid rgb(253,151,31, 0.1);
box-shadow: 4px 4px 5px rgba(253,151,31, 0.1);
padding: 20px;
width: auto;
}
:host section figure {
display: flex;
gap: 20px;
width: 100%;
}
:host section figure figcaption {
display: flex;
gap:5px;
flex-direction: column;
justify-content: center;
}
::slotted(img) {
background-color: rgb(166,226,46);
object-fit: cover;
width: 100px;
border-radius: 50%;
}
::slotted(span) {
color: white;
font-weight: bold;
font-size: 16px;
}
`);

export default STYLES;
15 changes: 15 additions & 0 deletions src/components/P-004/template.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const TEMPLATE = document.createElement("template");

TEMPLATE.innerHTML = `
<section>
<figure>
<slot name="avatar"></slot>
<figcaption>
<slot name="name"></slot>
<slot name="status"></slot>
</figcaption>
</figure>
</section>
`;

export default TEMPLATE;
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import "./components/P-001/index.js";
import "./components/P-002/index.js";
import "./components/P-003/index.js";
import "./components/P-004/index.js";
Loading