No tienes acceso a esta clase

¡Continúa aprendiendo! Únete y comienza a potenciar tu carrera

Inyección de Datos en Componentes Web Reutilizables

20/22
Recursos

¿Cómo completar un componente web para un eCommerce?

¿Alguna vez te has preguntado cómo se diseñan y desarrollan esos atractivos y funcionales componentes en los sitios de eCommerce? Hoy vamos a sumergirnos en la creación de una tarjeta de producto que puede utilizarse en cualquier tienda en línea. Este componente no solo es estéticamente atractivo, también es reutilizable y puede integrarse fácilmente en cualquier catálogo de productos mediante la interacción con un API. Analizaremos cómo diseñar estilos y cómo inyectar datos dinámicos en nuestro componente, todo a través de un enfoque práctico.

¿Cómo integrar los estilos en el componente?

Comencemos con los estilos. Aunque en este curso no nos adentraremos en profundidad en CSS, es crucial entender los cambios visuales que podemos lograr. Aquí tienes algunos puntos clave sobre cómo implementar estilos básicos:

  • Márgenes y paddings: Ajusta los márgenes y paddings globales para neutralizar los valores por defecto que puedan afectar a tu diseño.

  • Flexbox en el body: Utilizar display: flex facilita la alineación y distribución de tus elementos en el layout.

  • Estilos globales: Implementa estilos globales para elementos recurrentes usando clases y selects CSS como el body.

  • Variables CSS: Implementar variables para colores y fondos permite que tu aplicación sea más dinámica y fácil de personalizar.

¿Cómo popular dinámicamente el componente con datos?

El dinamismo en una aplicación web es esencial, especialmente cuando trabajamos con datos que provienen de un API. Para esto, vamos a necesitar trabajar con atributos que llenarán de contenido nuestra tarjeta de producto. Veamos cómo hacerlo:

  1. Definición de atributos: Estableceremos los atributos en nuestro componente con el método static get. Algunos de los atributos que podemos definir son:

    • img
    • title
    • price
    • description
    • collection
  2. Método attributeChangedCallback: Este método es fundamental para detectar los cambios en los atributos definidos y actualizar los elementos del DOM cuando ocurren. Recibe tres parámetros: el nombre del atributo, el valor antiguo, y el nuevo valor.

  3. Interpolación de variables: Dentro de nuestro HTML, usaremos la interpolación para colocar valores dinámicos en el template. Por ejemplo:

    <img src="${this.img}" alt="zapatos deportivos para correr color azul">
    <h2>${this.title} <span>${this.collection}</span></h2>
    

¿Cómo personalizar el fondo según el producto?

Para dar un toque final, podemos hacer que el color de fondo del componente coincida con los productos mostrados. Utilizamos variables CSS para modificar el background, permitiendo que el componente sea responsive y se adapte visualmente según el producto.

  • Cambiar el fondo: Usamos una variable práctica primary-background para ajustar el color. Puedes modificar este valor basándote en la información recibida del API, haciendo que cada producto tenga su propia identidad visual:

    :host {
      --primary-background: green;
    }
    

Al finalizar estos pasos, habrás adquirido la capacidad de crear componentes personalizados y dinámicos para tu sitio de eCommerce. ¡Sigue experimentando y mejorando tus habilidades! Con el conocimiento que tienes ahora, puedes crear componentes más complejos y deslumbrantes. Además, en clase te dejaremos algunos retos para seguir practicando. ¡Continuemos este emocionante viaje de aprendizaje!

Aportes 17

Preguntas 7

Ordenar por:

¿Quieres ver más aportes, preguntas y respuestas de la comunidad?

Si observaron la consola, lo más probable es que les haya salido este error

Uncaught RangeError: Maximum call stack size exceeded.
    at HTMLElement.attributeChangedCallback (product-card.js:16)
    at HTMLElement.attributeChangedCallback (product-card.js:16)
    at HTMLElement.attributeChangedCallback (product-card.js:16)
    at HTMLElement.attributeChangedCallback (product-card.js:16)
    at HTMLElement.attributeChangedCallback (product-card.js:16)
    at HTMLElement.attributeChangedCallback (product-card.js:16)
    at HTMLElement.attributeChangedCallback (product-card.js:16)
    at HTMLElement.attributeChangedCallback (product-card.js:16)
    at HTMLElement.attributeChangedCallback (product-card.js:16)
    at HTMLElement.attributeChangedCallback (product-card.js:16)

Esto se debe a que estamos utilizando el atributo “title” y este ya es un atributo nativo de HTML por lo que la solución es simplemente cambiar el nombre de nuestro atributo.

Para ahorrarse hacer muchos if pueden hacer esto:

    static get observedAttribute() {
        return ['img', 'name', 'price', 'description', 'colllection'];
    }
    attributeChangeCallback(attr, oldValue, newVal) {
        const attributes = productCard.observedAttribute;
        if (attributes.includes(attr)) {
            this[attr] = newVal;
        }
    }

Si queremos agregar otro atributo al componente solo sería agregarlo al observedAttribute y no tener que añadir otra condición.

No hay link para los retos 🙋🏽‍♂️

Hecho

Aquí están, tranquilo, sé que los estabas buscando

    getStyles() {
        return `
            <style>
            :host{
                --primary-bg: #5a6cb2;
                width: 90%;
                max-width: 900px;
                min-width: 280px;
            }
            
            .container {
                position: relative;
                display: flex;
                flex-wrap: wrap;
                justify-content: space-between;
                width: 900px;
                height: 600px;
                margin: 20px;
                background-color: #fff;
            }
            
            .container .imgBox{
                position: relative;
                display: flex;
                justify-content: center;
                width: 50%;
                height: 100%;
                background-color: var(--primary-bg);
            }
            
            .container .imgBox:before {
                position: absolute;
                top: 20px;
                left: 20px;
                font-size: 3em;
                font-weight: 800;
                color: #000;
                content: "Nike";
                opacity: 0.1;
            }
            
            .container .imgBox img{
                position: relative;
                top: 100px;
                left: -50px;
                width: 720px;
                height: 480px;
                transform: rotate(-30deg);
            }
            
            .container .details{
                display: flex;
                justify-content: center;
                align-items: center;
                width: 50%;
                height: 100%;
                box-sizing: border-box;
                padding: 80px;
            }
            
            .container .details h2{
                margin-bottom: 25px;
                font-size: 2.5em;
                line-height: 0.8em;
                color: #444;
            }

            .container .details button{
                margin-top: 35px;
                float: right;
                padding: 15px 20px;
                font-size: 16px;
                color: #fff;
                letter-spacing: 1px;
                font-weight: 600;
                text-transform: uppercase;
                border-radius: 40px;
                background-color: #5a6cb2;
                cursor: pointer;
            }

            @media (max-width: 1023px) {
                .container{
                    height: auto;
                    width: auto;
                }

                .container .imgBox{
                    padding: 40px;
                    width: 100%;
                    box-sizing: border-box;
                    height: auto;
                    text-align: center;
                }

                .container .imgBox img{
                    width: 430px;
                    top:80px;
                    height: auto;
                    transform: rotate(-38deg);
                }

                .container .details{
                    width:100%;
                    height: auto;
                    padding: 20px;
                }

                .container .details p{
                    max-width: 100%;
                    margin-left: 0;
                }
            }
            </style>
        `

Les comparto el código del componente al inicio de la clase:

class productCard extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: "open" });
  }
  getTemplate(){
    const template = document.createElement("template");
    template.innerHTML = `
    <main class="container">
      <section class="imgBox">
        <img src="./imgs/nike-blue.png" alt="Zapatos deportivos para correr color azul"/>
      </section>
      <section class="details">
        <div class="content">
          <h2>Hola mundo</h2>
          <p>Lorem, ipsum dolor sit amet consectetur adipisicing elit. Assumenda quod deserunt itaque alias delectus earum, quis repellat veritatis ad temporibus ex recusandae quasi rem! Cumque sequi rem nam vero saepe!</p>
          <h3>$300 USD</h3>
          <button>Comprar</button>
        </div>
      </section>
    </main>
    ${this.getStyles()}
    `;
    return template;
  }
  getStyles() {
    return `
      <style>
      :host {
        --primary-background: #5a6cb2;
          width: 80%;
          max-width: 900px;
          min-width: 280px;
          margin: 0 auto;
      }
      .container {
          position: relative;
          display: flex;
          flex-wrap: wrap;
          justify-content: space-between;
          width: 900px;
          height: 600px;
          margin: 20px;
          background-color: #fff;
      }
      .container .imgBox {
          position: relative;
          display: flex;
          justify-content: center;
          width: 50%;
          height: 100%;
          background-color: var(--primary-background)
      }
      .container .imgBox:before {
          position: absolute;
          top: 20px;
          left: 20px;
          font-size: 8em;
          font-weight: 800;
          color: #000;
          content: 'Nike';
          opacity: 0.1;
      }
      .container .imgBox img {
          position: relative;
          top: 100px;
          left: -50px;
          width: 720px;
          height: 480px;
          transform: rotate(-30deg);
      }
      .container .details {
          display: flex;
          justify-content: center;
          align-items: center;
          width: 50%;
          height: 100%;
          box-sizing: border-box;
          padding: 40px;

      }
      .container .details h2 {
          margin-bottom: 25px;
          font-size: 2.5em;
          line-height: 0.8em;
          color: #444;
      }
      .container .details h2 span {
          font-size: 0.4em;
          text-transform: uppercase;
          letter-spacing: 2px;
          color: #999;
      }
      .container .details p {
          max-width: 85%;
          margin-left: 15%;
          margin-bottom: 35px;
          color: #333;
          font-size: 15px;
      }
      .container .details h3 {
          float: left;
          font-size: 2.5em;
          color: #a2a2a2;
      }
      .container .details button {
          margin-top: 35px;
          float: right;
          padding: 15px 20px;
          font-size: 16px;
          color: #fff;
          letter-spacing: 1px;
          font-weight: 600;
          text-transform: uppercase;
          border-radius: 40px;
          background-color: #5a6cb2;
          cursor: pointer;
      }
      @media (max-width: 1080px) {
          .container {
              height: auto;
              width: auto;
          }
          .container .imgBox {
              padding: 40px;
              width: 100%;
              box-sizing: border-box;
              height: auto;
              text-align: center;
          }
          .container .imgBox img {
              left: initial;
              width: 100%;
              height: auto;
              transform: rotate(0deg);
          }

          .container .details {
              width: 100%;
              height: auto;
              padding: 20px;
          }

          .container .details p {
              max-width: 100%;
              margin-left: 0;
          }
      }
      </style>
    `;
  }
  render(){
    this.shadowRoot.appendChild(this.getTemplate().content.cloneNode(true));
  }
  connectedCallback(){
    this.render();
  }
}

customElements.define("product-card", productCard);

Como en nuestro componente de proyecto solo estamos recibiendo las propiedades desde el html, lo hice de una manera mas simple, pero igualmente valida, simplemente obteniendo los atributos de la data que necesito en el constructor, y funciona bien para el resultado que esperamos.

Que genial 😃

Creo que seria mejor si lo hiciéramos desde 0

Hey!, al momento de utilizar atributos nativos de html para nuestro componente se suele generar este error en la consola como lo muestran algunos compañeros en otros comentarios

Uncaught RangeError: Maximum call stack size exceeded.

si bien se puede cambiar simplemente el atributo por otro En este enlace pueden encontrar algunas explicaciones de porque se genera ese problema y como solucionarlo 😉

Deje el link de la rama equivocada este es el bueno Nike

![](https://static.platzi.com/media/user_upload/image-6e5a70fa-3413-49af-a92c-b3275b4594b0.jpg)
¿como puedo mantener las convenciones de kebabcase y camel case en html y js respectivamente al usar componentes web? se me habia ocurrido con esta funcion pero no esta siendo tan straigth forward como habia pensado ```js kebabToCamel(str) { return str.replace(/-./g, (match) => match.charAt(1).toUpperCase()); } ```
Aquí les dejo una manera para importar los estilos desde una hoja de estilos. para esto se debe tener conceptos de asincronismo. el método lo llame loadStyles ```js class MyComponent extends HTMLElement { constructor() { // Aquí se pueden inicializar las propiedades y el estado del componente super(); this.attachShadow({ mode: "open" }); } connectedCallback() { // Aquí se pueden agregar los elementos HTML y la lógica de inicialización del componente this.render(); } disconnectedCallback() { // Aquí se pueden realizar tareas de limpieza y eliminación de elementos HTML console.log("El componente ha sido eliminado del DOM"); } static get observedAttributes() { // Aquí se pueden definir los atributos que se quieren observar y reaccionar a cambios en ellos return ["titulo", "parrafo"]; } attributeChangedCallback(attr, oldValue, newValue) { // Aquí se puede definir la lógica que se quiere ejecutar cuando un atributo observado cambia if (attr === "titulo") { this.titulo = newValue; } if (attr === "parrafo") { this.parrafo = newValue; } } getTemplate() { const template = document.createElement("template"); template.innerHTML = ` <section>

${this.titulo}

${this.parrafo}

<slot name="texto1"></slot> <slot name="texto2"></slot> </section>`; return template; } async loadStyles() { const res = await fetch("./my-component.css"); const css = await res.text(); const style = document.createElement("style"); style.textContent = css; this.shadowRoot.appendChild(style); } render() { // Aquí se pueden agregar los elementos HTML y la lógica de inicialización del componente this.shadowRoot.appendChild(this.getTemplate().content.cloneNode(true)); this.loadStyles(); } // Aquí se pueden definir otros métodos y propiedades del componente } customElements.define("my-component", MyComponent); ```

Pueden utilizar este codigo para no tener que hacer tantos if, sobre todo si la cantidad de dependencia va aumentando

attributeChangedCallback(attr, oldValue, newValue) {
        const attrValidation = {
            img: () => this.img = newValue,
            name: () => this.name = newValue,
            price: () => this.price = newValue,
            description: () => this.description = newValue,
            collection: () => this.collection = newValue,
        }
        attrValidation[attr]()
    }    

dejo mi aporte pues estuve dando vueltas a los tributos y me encontré con lo siguiente:
- deben nombrarse diferente a etiquetas HTML
-no deben contener caracteres en mayúsculas
-no deben contener numeros
espero te sirva

Les dejo mi v:

class CardComponent extends HTMLElement {

    constructor() {
        super();
        this.attachShadow({ mode: "open" });
    }

    connectedCallback() {
        this.render();
    }

    attributeChangedCallback(attr, oldValue, newValue) {
        if (newValue !== oldValue) {
            this[attr] = newValue;
        }
    }

    render() {
        this.shadowRoot.append(this.templates.content.cloneNode(true));
    }

    get templates() {
        const container = document.createElement('template');
        container.innerHTML = `
            <div class="card-container">
                <section class="card-container__img-content">
                    <h2>${this.mark}</h2>
                    <img src="${this.img}" alt="image product ${this.mark}">
                </section>
                <section class="card-container__main-content">
                    <h3>${this.product} <span>${this.category}</span></h3>
                    <h4>${this.title}</h4>
                    <p>${this.text}</p>
                </section>
                <section class="card-container__footer-content">
                    <h3>${this.price}</h3>
                    <button type="button">${this.button}</button>
                </section>
            </div>
            
            ${this.styles}
        `;
        return container;
    }

    get styles() {
        return `
            <style> 
            
            :host {
                --background-color-main-content: #758cf1;;
                --text-color-img-content: #00000054;
                --title-color-main-content: grey;
                --button-color-footer-content: white;
                --font-family: sans-serif, "Roboto Light";
                width: 100%;
                height: 100%;
                font-family: var(--font-family);
            }
            :host .card-container {
                width: 100%;
                height: 100%;
                display: grid;
                position: relative;
                grid-template-columns: 1fr;
                grid-template-rows: 250px auto 100px;
            }
            :host .card-container__img-content {
                width: 100%;
                height: 250px;
                background-color: var(--background-color-main-content);
            }
            :host .card-container__img-content h2 {
                color: var(--text-color-img-content);
                font-size: 100px;
                margin: 0;
                padding: 15px;
            }
            :host .card-container__img-content img {
                width: 100%;
                height: 250px;
                position: absolute;
                transform: translateY(-85px);
            }     
            :host .card-container__main-content {
                width: 100%;
            }
            :host .card-container__main-content h3 {
                font-size: 25px;
                font-weight: bold;
                margin: 30px 20px 10px 20px;
            }
            :host .card-container__main-content span {
                font-size: 15px;
                text-transform: uppercase;
                color: var(--title-color-main-content);
            }
            :host .card-container__main-content h4 {
                text-transform: uppercase;
                color: var(--title-color-main-content);
                margin: 0 10px 10px 20px;
                font-size: 15px;
            }
            :host .card-container__main-content p {
                padding: 0 20px 0 20px;
                margin-bottom: 0;
                font-size: 18px;
                line-height: 1.4;
                font-weight: 100;
            }
            :host .card-container__footer-content {
                    width: 100%;
                    height: 80px;
                    display: flex;
                    align-content: center;
                    justify-content: space-between;
                    align-items: center;
            }
            :host .card-container__footer-content h3 {
                color: var(--title-color-main-content);
                font-size: 30px;
                margin: 20px;
            }
            :host .card-container__footer-content button {
                margin-right: 20px;
                display: flex;
                color: var(--button-color-footer-content);
                background-color: var(--background-color-main-content);
                border-radius: 15px;
                width: 100px;
                height: 40px;
                align-items: center;
                justify-content: center;
                align-self: center;
                border: 1px solid transparent;
                font-weight: bold;
                font-size: 15px;
            }
            @media (min-width: 800px) {
                :host .card-container {
                    height: auto;
                    grid-template-columns: 1fr 1fr;
                    grid-template-rows: 1fr 1fr;
                }
                :host .card-container__img-content {
                    height: auto;
                    grid-column: 1 / 2;
                    grid-row: 1 / 3;
                }
                :host .card-container__img-content img {
                    width: 700px;
                    height: 400px;
                    transform: translate(-145px, -50px) rotate(330deg);
                }
                :host .card-container__main-content {
                    grid-column: 2 / 3;
                    grid-row: 1 / 2;
                }
                :host .card-container__main-content h3 {
                    margin: 30px 20px 10px 10px;
                }
                :host .card-container__footer-content {
                    height: 200px;
                    grid-column: 2 / 3;
                    grid-row: 2 / 3;
                }
                :host .card-container__footer-content h3 {
                    font-size: 40px;
                }
                :host .card-container__footer-content button {
                    width: 130px;
                    height: 50px;
                    font-size: 20px;
                    margin-right: 40px;
                }
               
            } 
            </style>
        `;
    }

    static get observedAttributes() {
        return ['mark', 'img', 'product', 'category', 'title', 'text', 'button', 'price'];
    }


}

customElements.define('card-component', CardComponent);