Creando un componente de barra lateral (sidebar) reutilizable en Angular

Aprende como crear una barra lateral (sidebar) en Angular de manera que puedas usarla en múltiples secciones de tu aplicación.

Antes de comenzar quiero comentar que este tutorial no será un tutorial muy a detalle ya que más que nada es buscar aportar algo que trabajé en un proyecto y que me gustaría compartirles, ya que no lo considero suficiente para hacer una dependencia o un tutorial a detalle, aunque sí habrá un repositorio para esto.

Si estamos listos, entonces, manos a la obra.

Primer paso

Lo primero es generar el componente del sidebar a nivel HTML y a nivel CSS. En este caso usaremos SCSS.

Ya que tenemos los 3 archivos bases generados por Angular-CLI hay que ver la estructura del HTML:

<div class="sidebar" [style.width.px]="width">
<div class="sidebar__header">
<h3>{{ title }}</h3>
<button class="close" (click)="this.close()">
<fa-icon [icon]="['fas', 'times']"></fa-icon>
</button>
</div>
<div class="sidebar__body">
<ng-content></ng-content>
</div>
<div class="sidebar__grabber">
<fa-icon [icon]="['fas', 'grip-lines-vertical']"></fa-icon>
</div>
</div>
<div class="sidebar__overlay"></div>

Como podemos ver usaremos la clase “sidebar”, y usaremos un atributo especial para el ancho ya que vamos a implementar que el sidebar tenga un “draggable” que permita hacerlo más o menos ancho y así sus contenidos se adapten a la anchura que indica el usuario, usando mínimos y máximos.

El botón de close para cerrar con su respectiva función y en este caso, estamos incluyendo un ícono de FontAwesome para Angular.

Después incluimos el cuerpo del contenido a ser contenido variable dependiendo de lo indicado en la vista donde se llame el sidebar.

Casi al finalizar agregamos la barra que le permitirá modificar la anchura a nivel usuario y por último un overaly que permitirá hacer más enfoque a la barra lateral.

Ahora, vamos con el CSS, este será como se mencionó en SCSS:

.sidebar {
  background: #fff;
  position: fixed;
  height: 100vh;
  min-width: 320px;
  max-width: 75%;
  top: 0;
  display: flex;
  -webkit-box-orient: vertical;
  -webkit-box-direction: normal;
  flex-direction: column;
  box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.05);
  right: -100%;
  transition: right 0.25s ease;
  z-index: 40000;
  &__header {
    position: relative;
    display: -ms-flexbox;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    min-height: 72px;
    box-shadow: 0 2px 2px 0 rgba(117, 117, 117, 0.1);
    padding: 10px 20px;
    h3 {
      font-size: 18px;
    }
    .close {
      background: none;
      outline: none;
      border: none;
      margin-left: auto;
      font-size: 18px;
      font-weight: normal;
      line-height: 0.8;
      opacity: 0.5;
      &:hover {
        opacity: 1;
        fa-icon {
          color: #000;
        }
      }
    }
  }
  &__body {
    padding: 30px 20px;
    background: inherit;
    overflow-y: auto;
    height: 100%;
  }
  &__spacer {
    padding: 0;
    margin: 0;
    -webkit-box-flex: 1 !important;
    -ms-flex-positive: 1 !important;
    flex-grow: 1 !important;
    background: transparent;
  }
  &__footer {
    border-top: 1px solid rgba(0, 0, 0, 0.06);
  }
  &__grabber {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    margin-right: -15px;
    cursor: col-resize;
    height: 100%;
    width: 15px;
    overflow: hidden;
    background-color: rgba(241, 241, 241, 0);
    opacity: 1;
    &:hover,
    &:active {
      background-color: rgba(241, 241, 241, 1);
    }
    fa-icon {
      position: absolute;
      top: 50%;
      margin-top: -22px;
      left: 5px;
    }
  }
}
.sidebar__overlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(27, 51, 90, 0.5);
  z-index: 1000;
  display: none;
}
@media screen and (max-width: 39.9375em) {
  .sidebar {
    max-width: 100%;
  }
}
@media screen and (min-width: 40em) and (max-width: 63.9375em) {
  .sidebar {
    max-width: 100%;
  }
}

A grandes rasgos son estilos bastante básicos, solo habría que poner atención a la clase .sidebar con el max-width, ¿por qué?, porque ahí es donde va a recidir que no permitamos que la barra se expanda al 100% a menos que estemos en responsivo. Además de que tampoco permitiremos que el usuario alcance menos de 320px en el ancho de la barra.

Todo este estilo puede ir dentro del archivo que se genera a la hora de hacer el componente, porque vamos a incluir otro en el archivo styles.scss:

.sidebar__open {
  .sidebar {
    right: 0 !important;
  }
  .sidebar__overlay {
    display: block !important;
  }
}

Con esta clase podremos indicar la apertura del sidebar.

Segundo paso el componente

Ahora ya que tenemos la estrucutura y su estilo, vamos a generar la lógica del componente en sí.

El componente funcionará de forma que, lo vamos a aislar, haciendo que cada vez que agreguemos un nuevo sidebar la lógica funcione sólo para sí mismo.

Es decir, si tenemos 2 sidebars distintos en la misma vista, cada uno funcione por separado.

import {
  Component,
  ElementRef,
  OnDestroy,
  OnInit,
  HostListener,
  Input,
} from "@angular/core";
import { SidebarService } from "./sidebar.service";
@Component({
  selector: "app-sidebar",
  templateUrl: "./sidebar.component.html",
  styleUrls: ["./sidebar.component.scss"],
})
export class SidebarComponent implements OnInit, OnDestroy {
  public width = 320;
  public x = 100;
  public oldX = 0;
  public grabber = false;
  @Input() id: string;
  @Input() title = "";
  private element: any;

  @HostListener("document:mousemove", ["$event"])
  onMouseMove(event: MouseEvent) {
    if (!this.grabber) {
      return;
    }
    this.resizer(event.clientX - this.oldX);
    this.oldX = event.clientX;
  }

  @HostListener("document:mouseup", ["$event"])
  onMouseUp(event: MouseEvent) {
    this.grabber = false;
  }

  @HostListener("document:mousedown", ["$event"])
  onMouseDown(event: any) {
    if (event.target.className === "sidebar__grabber") {
      this.grabber = true;
      this.oldX = event.clientX;
    }
  }

  constructor(private sidebarService: SidebarService, private el: ElementRef) {
    this.element = el.nativeElement;
  }

  ngOnInit() {
    const sidebar = this;
    if (!this.id) {
      console.error("Sidebar must have an ID");
      return;
    }

    if (localStorage.getItem("sidebar_width") === null) {
      localStorage.setItem("sidebar_width", this.width.toString());
    } else {
      this.width = parseInt(localStorage.getItem("sidebar_width"), 10);
      const browserWidth = Math.max(
        document.body.scrollWidth,
        document.documentElement.scrollWidth,
        document.body.offsetWidth,
        document.documentElement.offsetWidth,
        document.documentElement.clientWidth
      );
      if (this.width > browserWidth) {
        this.width = browserWidth;
        this.setSidebarWidth(this.width);
      }
    }

    document.body.appendChild(this.element);

    this.element.addEventListener("click", function (e: any) {
      if (e.target.className === "sidebar__overlay") {
        sidebar.close();
      }
    });
    const sidebarComponentData = {
      id: this.id,
      open: function () {
        sidebar.open();
      },
      close: function () {
        sidebar.close();
      },
    };
    this.sidebarService.add(sidebarComponentData);
  }
  ngOnDestroy() {
    this.sidebarService.remove(this.id);
    this.element.remove();
  }
  private resizer(offsetX: number) {
    this.width -= offsetX;
    this.setSidebarWidth(this.width);
  }
  private setSidebarWidth(width: number) {
    localStorage.setItem("sidebar_width", width.toString());
  }
  public open(): void {
    this.element.classList.add("sidebar__open");
  }
  public close(): void {
    this.element.classList.remove("sidebar__open");
  }
}

Vamos a explicar rápido qué hace cada parte, de forma simple:

  • Importamos un servicio aún no existente, este, lo veremos más adelante.
  • El selector será “app-sidebar”, ustedes pueden usar el que gusten.
  • Anexamos el ancho base para el sidebar.
  • Agregamos dos variables para las ubicaciones del ancho
  • Agregamos una variable para saber si están arrastrando o no el sidebar para modificar su ancho
  • Agregamos dos inputs para las directivas del título y un ID que nos hará saber cuál es el sidebar que estaremos manejando.
  • Después agregamos un evento para cuando se mueva el sidebar y entonces le haga el resize.
  • Otro para retirar que se está moviendo el sidebar y a su vez para indicarlo.
  • Posterior en el constructor importamos el servicio que previamente mencioné (aún no existe) y la clase para referir al elemento.
  • En nuestro ngOnInit incluimos la lógica para que el usuario pueda guardar el ancho indicado la última vez en su localStorage, un par de comprobaciones, un evento para cuando hagan click en el overlay se puedan cerrar los sidebars y la asignación de cuántos sidebars habrá basados en el servicio.
  • En nuestro ngOnDestroy asignaremos que cuando se destruya el componente (cambio de ruta) se retiren los sidebars.
  • Así como un par de funciones de apoyo.

Ya que tenemos entonces el componente, vamos al servicio.

Último paso

Vamos a crear un servicio que nos permitirá conectar el componente con su ejecución y funcionamiento.

Para ello tendremos el siguiente código:

import { Injectable } from "@angular/core";
@Injectable({ providedIn: "root" })
export class SidebarService {
  private sidebars = [];
  constructor() {}
  public add(sidebar: any): void {
    this.sidebars.push(sidebar);
  }
  public remove(id: string): void {
    this.sidebars = this.sidebars.filter((x) => x.id !== id);
  }
  public open(id: string): void {
    const sidebar: any = this.sidebars.filter((x) => x.id === id)[0];
    sidebar.open();
  }
  public close(id: string): void {
    const sidebar: any = this.sidebars.filter((x) => x.id === id)[0];
    sidebar.close();
  }
}

En este servicio lo único que hará será almacenar los sidebars y de esta forma abrir o cerrarlos basados en la información interna del componente.

Y más o menos se vería así:

Conclusión

Esto nos permite tener un sidebar personalizado para información como un menú, un formulario, texto informativo u otros.

Les dejo el repositorio de esta publicación:

https://github.com/asfo/ng-sidebar

¿Cuál es tu reacción?
+1
0
+1
0
+1
0
+1
0
+1
0
Total
0
Shares
Publicación anterior

Exportando un JSON a CSV con JavaScript.

Siguiente Publicación

Autenticando un API con NodeJS y JWT (JSON Web Tokens)

Publicaciones Relacionadas