Creando un Custom Element para transformar Imágenes en JavaScript
Tags: javascript, webdev, programming, tutorial
Published: 2023-12-19 21:49:58

Hola, soy [@ratapan](https://dev.to/ratapan) y hoy quiero compartir con ustedes el primer custom element que he creado. Este elemento Input, que permite tomar un archivo de imagen, cortarlo y darle un nuevo tamaño, pueden personalizarlo tanto como quieran, es primera vez que hago uno, así que si tienen sugerencias, les agradecería.

## ¿Qué es un Custom Element?

Los Custom Elements son una parte fundamental de la suite de tecnologías de Web Components. Nos permiten crear nuestros propios elementos HTML, con su funcionalidad y estilos encapsulados, siendo todo esto **nativo** de la plataforma web.

## Presentando `rtp-crop`

`rtp-crop` es un custom element que desarrollé para manipular imágenes directamente en el navegador. Puede tomar una imagen, cortarla a un tamaño específico y cambiar su resolución.

### Código del Custom Element

El código de `rtp-crop` se divide en varias partes. Primero, tenemos los estilos CSS, que se tienen que insertar, ya que utiliza el shadow dom para crear el custom element, además podemos usar variables de CSS para personalizar todos a la vez:

```javascript
//_rtp_image_crop.js
const styles = (height) => `
.rtp-crop{
  min-height: 30px;
  max-height:100%;
  height: ${height};
  min-width: 50px;
  max-width:100%;
  display: flex;
  justify-content: center;
  align-items: center;
  position:relative;
}
.rtp-crop__label{
  cursor: pointer;
  padding: var(--input-padding);
  border-radius: var(--input-radius);
  color: var(--c-font);
  background-color: var(--c-i-c);
  margin-right:auto;
}
.rtp-crop__input{
  display: none;
}
.rtp-crop__preview{
  max-width:100%;
  max-height:100%;
}`;
```

Luego, la función principal que cambia la resolución de la imagen y su extensión. 

```javascript
/**
 * Reduces the resolution of an image file to specified dimensions.
 *
 * This function takes an image file as input along with the desired new width
 * and height. It returns a Promise that resolves with an object containing the
 * original image size, the new image size, and the reduced resolution image file.
 * If the input image is not square, it will be cropped to fit the specified
 * dimensions. The new image is returned in the WebP format.
 *
 * @param {File} imageFile - The image file to reduce resolution of.
 * @param {string} type - The type of the final image.
 * @param {number} newWidth - The desired width for the new image.
 * @param {number} newHeight - The desired height for the new image.
 * @returns {Promise<{img?: File, name?: string}>} A Promise that resolves with an object containing the new image file (`img`) and the name. The Promise is rejected if there is an error in processing the image.
 */
export async function reduceImageResolution(
  imageFile,
  type,
  newWidth,
  newHeight
) {
  return new Promise((resolve, reject) => {
    const canvas = document.createElement("canvas");
    const ctx = canvas.getContext("2d");
    const image = new Image();

    image.src = URL.createObjectURL(imageFile);
    image.onload = () => {
      const originalWidth = image.naturalWidth;
      const originalHeight = image.naturalHeight;

      const size = Math.min(originalWidth, originalHeight);

      canvas.width = newWidth;
      canvas.height = newHeight;

      const sourceX = (originalWidth - size) / 2;
      const sourceY = (originalHeight - size) / 2;
      const sourceWidth = size;
      const sourceHeight = size;

      ctx.drawImage(
        image,
        sourceX,
        sourceY,
        sourceWidth,
        sourceHeight,
        0,
        0,
        newWidth,
        newHeight
      );

      canvas.toBlob(
        (blob) => {
          if (blob) {
            const reducedImage = new File([blob], imageFile.name, {
              type:`image/${type}`,
            });
            resolve({
              img: reducedImage,
            });
          } else {
            reject(new Error("Error processing image"));
          }
        },
        imageFile.type,
        1
      );
    };

    image.onerror = () => {
      reject(new Error("Error loading image"));
    };
  });
}
```
La lógica, en el caso de que el valor de salida "size" la relación es de 1:1, lo que hará será tomar "File"  y en caso de ser más ancho que alto, eliminará los costados hasta quedar centrado y en caso de ser más alto que ancho, se eliminaría arriba y abajo para dejar el centro como en estos casos, además de cambiar por defecto el formato a Webp o al de elección:

|input |output|
|------|------|
|![Imagen de ejemplo, más alta que ancha](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mul95w23x4rsivox6ruv.png) | ![Output de la imagen más alta que ancha](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7v397fd4vfnl0jywan1n.png) |
| ![Imagen de ejemplo, más ancha que alta](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wgnfxa0k6y9pn6eq11tt.png) |![Output de la imagen más ancha que alta](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gps7anqi4kvlwbjxwmln.png) |

Y finalmente, la definición del custom element, este tiene diferentes valores default que podemos cambiar desde la declaración HTML de nuestro elemento, también a través del **set value(val)** generamos un evento tipo change para utilizar el valor de nuestro elemento:

```javascript
///_rtp_image_crop.js
class RtpCropInput extends HTMLElement {
  constructor() {
    super();
    this.shadow = this.attachShadow({ mode: "open" });
    this._value = null;

    // Styles
    this.myStyles = document.createElement("style");
    this.myStyles.innerHTML = styles();

    // Container
    this.inputContainer = document.createElement("div");

    // Image
    this.inputPreview = document.createElement("img");
    this.inputPreview.alt = "Preview input image";
    this.inputPreview.loading = "lazy";
    this.inputPreview.style.display = "none";

    // Label
    this.inputLabel = document.createElement("label");
    this.inputLabel.textContent = "Select an image";

    // Input
    this.inputThis = document.createElement("input");
    this.inputThis.type = "file";
    this.inputThis.accept = "image/*";
    this.inputThis.id = "file-input"; // Setting an ID for the input
    this.inputLabel.setAttribute("for", "file-input"); // Referencing the input ID in the label

    //clases
    this.inputContainer.className = "rtp-crop";
    this.inputLabel.className = "rtp-crop__label";
    this.inputThis.className = "rtp-crop__input";
    this.inputPreview.className = "rtp-crop__preview";

    // Append children
    this.shadow.appendChild(this.myStyles);
    this.shadow.appendChild(this.inputContainer);
    this.inputContainer.appendChild(this.inputThis);
    this.inputContainer.appendChild(this.inputLabel);
    this.inputContainer.appendChild(this.inputPreview);
  }
  get value() {
    return this._value;
  }
  set value(val) {
    this._value = val;
    this.dispatchEvent(new Event("change"));
  }
  connectedCallback() {
    let size = this.getAttribute("output-size") || 800;
    let type = this.getAttribute("output-type") || "webp";
    this.myStyles.innerHTML = styles(
      this.getAttribute("style-height") || "40px"
    );

    const previewImage = (file) => {
      const reader = new FileReader();
      reader.onload = (e) => {
        this.value = file;
        this.inputPreview.src = e.target.result;
        this.inputPreview.style.display = "inline-block";
      };
      reader.readAsDataURL(file);
    };

    // Event listener for input
    this.inputThis.addEventListener("change", async (ev) => {
      const target = ev.target;
      if (target.files && target.files[0]) {
        const data = await reduceImageResolution(
          target.files[0],
          type,
          size,
          size
        );
        if (data.img) {
          this.inputLabel.innerHTML = `Change image </br> ${
            data.img.name.length > 25
              ? data.img.name.substring(0, 19) + "..."
              : data.img.name
          }`;
          this.processedImage = data.img;
          previewImage(data.img);
        }
      }
    });
  }
}

customElements.define("rtp-crop", RtpCropInput);
```

![Custom elemnt base](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nlsyv28c4pe73fo3oqb0.png)

### Uso en HTML

Para usar `rtp-crop`, simplemente inclúyelo en tu HTML de la siguiente manera:

```html
<script type="module" src="./webComponents/_rtp_image_crop.js"></script>
<article class="card">   
  <rtp-crop
    id="input-crop"
    output-size="500"
    output-type="webp"
    style-height="100px"/>
</article>   
```
O de la siguiente manera, dejando los datos por defecto:
```html
<script type="module" src="./webComponents/_rtp_image_crop.js">
<article class="card">   
  <rtp-crop id="input-crop"/>
</article>   
```

![Custom element cargado](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/um58bl8d6lxg953qjc42.png)


## Estilos Adicionales y Variables CSS

He definido varios estilos y variables CSS para personalizar la apariencia del elemento:
### variables 
```css
//_variable.css
:root {
  --header-h: 40px;
  --mx-w: 1000px;

  --input-padding: 2px 4px;
  --input-radius: 4px;

  --space-1: 4px;
  --space-2: 8px;
  --space-3: 16px;
  --space-4: 24px;
  --space-5: 32px;
}
:root.dark {
  --c-bg: #222627;
  --c-bg_2: #0a0000;
  --c-font: #ffffff;
  --c-font_2: #fff2d6;
  --c-link: #fff8e8;
  --c-select: #4c463a;

  --c-i-a: #e3c4ad;
  --c-i-b: #dca780;
  --c-i-c: #a07250;
}
```
### style.css
```css
@import "./style/_typography.css";
@import "./style/_variable.css";
@import "./style/_reset.css";

.section {
  height: 100dvh;
  padding: var(--space-2) var(--space-3);
  display: grid;
  place-items: center;
}
.card {
  width: 400px;
  max-width: 500px;
  border: 1px solid var(--c-font_2);
  box-shadow: 4px 4px 8px var(--c-bg_2);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--space-2);
  -webkit-border-radius: var(--space-2);
  -moz-border-radius: var(--space-2);
  -ms-border-radius: var(--space-2);
  -o-border-radius: var(--space-2);
}

```

## Integración en JavaScript

Finalmente, el elemento se integra en el archivo JavaScript principal:

```javascript
//main.js
import './webComponents/_rtp_image_crop.js';
const $ = (sele)=> document.querySelector(sele)

const cropInput = $('#input-crop')

cropInput.addEventListener("change",(event)=>{
  console.log(event.target.value)
})
```

## Conclusión

Crear este custom element ha sido una experiencia de aprendizaje increíble. Espero que `rtp-crop` sea útil en tus proyectos y te inspire a crear tus propios elementos personalizados.

---

🐙 [GitHub](https://github.com/Ratapan)
📸 [Instagram](https://www.instagram.com/ratapics/)
🐈‍⬛ [Custom element en mi GitHub](https://github.com/Ratapan/My-Docu/blob/main/javascript/rtp_custome_lements/webComponents/_rtp_image_crop.js)