Crear un Skeleton en Angular 7.0

Frontend, Nova, Programación

Un skeleton no es más que una caja con un poco de CSS que se muestra antes de el contenido final para dar una sensación de fluidez en el usuario final (en vez de mostrar el típico «Cargando datos, espere por favor»).

Ejemplo

Vamos a implementar un Skeleton para carga los artículos del blog de Nova Internet y como base tomamos uno de los artículos del blog, concretamente el artículo “Deprecated while each() php7.2.x”:

Como podemos ver, el contenedor tiene una imagen principal, un título, un adelanto del contenido del artículo y la fecha de publicación .

Ahora en el Skeleton usaremos estos elementos como base y para dar la sensación de carga pondremos cajas de color gris en cada línea de texto y en la imagen.

El diseño quedaría así:

La estructura es similar aunque en este caso no son ni imágenes ni texto, solo una caja gris que llena el espacio correspondiente a los datos del artículo (mediante CSS).

Teniendo diseñada la estructura del Skeleton ya podemos empezar a «picar código».

 

Estructura de un proyecto Angular

 

«Damos por hecho que si estás leyendo este artículo por lo menos conoces la estructura básica de un proyecto en Angular 7»

En el directorio src/ tenemos el fichero app.component.ts el cual es el componente principal de la aplicación y app.module.ts  que es el módulo principal en el cuál incluiremos los demás componentes y módulos que iremos creando.

 

app.module.ts


import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';

import { HomeComponent } from './pages/home/home.component';
import { BlogItemModule } from './partials/blog-item/blog-item.module';

@NgModule({
   declarations: [
      AppComponent,
      HomeComponent
   ],
   imports: [
      BrowserModule,
      BlogItemModule
   ],
   providers: [],
   bootstrap: [AppComponent]
})
export class AppModule { }

HTML

Casi todo el código es generado por Angular, lo único que hemos agregado son los componentes HomeComponent y BlogItemModule.

El directorio pages/ contiene como su nombre dice las páginas de la web y en el cual tenemos el directorio home/ y dentro de éste los archivos home.component.ts (es dónde realizaremos las pruebas), el archivo home.component.html (que es la estructura HTML del componente), el archivo home.component.scss  (que son los estilos CSS de ese componente) y el archivo home.component.spect.ts (que es donde haremos los test unitarios).

 

home.component.html

<div class="container">
 <app-blog-item></app-blog-item>
</div>

En el archivo home.component.html tenemos un div con la clase container y dentro está el componente app-blog-item que corresponde al modulo BlogItemModule.

En el directorio partials/ está el directorio blog-item/ en el cual está el componente blog-item (el que contiene el Skeleton) y su correspondiente módulo.

En el archivo blog-element.component.html crearemos un div que sera el contenedor principal de nuestro post y que en nuestro caso lo llamaremos blog-element

<div class="blog-element"></div>

CSS

Ahora en el CSS definimos el estilo del div, tomando como referencia las medidas del post le pondremos al div un ancho de 380px y alto de 490px

.blog-element{
    display: inline-block;
    height: 490px;
    width: 380px;
    background: white !important;
}

Teniendo ya el contenedor del post pasaremos a crear el Skeleton (por fin!).

Como dijimos antes, utilizaremos la pseudoclase :empty la cual aplica CSS al elemento cuando no tiene contenido y la combinaremos con la pseudoclase ::after la cual agrega contenido al principio del div.

Combinando las dos pseudoclases logramos que se muestre el contenido del after sólo cuando el div esté vacío.

Ahora  le damos un alto y ancho de 100% para que se muestre con el mismo tamaño del contenedor y le agregamos un content que sea vacío (ya que no le agregaremos texto).

La pseudo-clase :empty de CSS representa cualquier elemento que no tenga hijos. Los hijos pueden ser nodos de elemento o texto (incluido el espacio en blanco). Los comentarios o las instrucciones de procesamiento no afectan si un elemento se considera vacío. MDN

Y ahora lo importante, el fondo, en el cual utilizamos la función de css linear-gradient.

Esta función crea un degradado con background-size y background-position al cual podemos darle un tamaño y una posición específica.

Como no queremos que tenga degradado, al linear-gradient le pasamos el mismo color como parámetro 2 veces.

La propiedad background es un atajo para definir los valores individuales del fondo en una única regla CSS. Se puede usar background para definir los valores de una o de todas las propiedades siguientes: background-attachment, color, image, position, repeat. MDN

En el atributo background-size pondremos el tamaño que queremos para cada elemento, el primero es el Skeleton de la imagen que le pondremos de ancho 100% y de alto 195px ya que es la altura aproximada de la imagen el post.

Para el título pondremos 95% de ancho y 25 px de alto y en el caso del texto creamos 6 elementos ( 1 para el skeleton de cada línea ) con un tamaño de 90% de ancho y 20px de alto.

Los altos de los Skeleton del texto y el título están basados en el alto que tiene el título final y el propio texto final.

Por último,  el Skeleton de la fecha tendrá un ancho de 40% y un alto de 15px ya que es mas pequeño que el texto.

La propiedad CSS background-size especifica el tamaño de las imágenes de fondo.

Nota: Si el valor de esta propiedad no se encuentra en una propiedad abreviada background , ésta es aplicada para los elementos después de la propiedad CSS background-size,  y el valor de esta propiedad se restablece a su valor inicial de la propiedad abreviada. MDN

Y ahora el background-position en el Skeleton de la imagen  que es el primer elemento y que empieza en la parte superior izquierda del contenedor. A éste le ponemos de posición 0 0.

En el caso del título,  horizontalmente empieza en 0 y verticalmente tenemos que sumar los 195px de la imagen más el margen que tiene el título de la imagen (15px), con lo que la posición vertical del título seria 210px.

En el caso de los 6 Skeleton de texto tenemos que calcular el margen que tiene el primer elemento y el título (15px), lo que hace que la posición vertical del primer elemento de texto sea 250px, la altura de 20px de cada Skeleton de texto y el margen entre el texto que es de 7px por lo cual a partir de los 250px del primer elemento vamos sumando 27px  y por último la posición del Skeleton de la fecha sería la posición del último Skeleton de texto más 15px de margen que tiene la fecha con el texto  lo que nos daría unos 420px.

.blog-element:empty::after{
   content: '';
   display: block;
   width: 100%;
   height: 100%;
   background-image: linear-gradient(#e7e7e7, #e7e7e7), 
   linear-gradient(#e7e7e7, #e7e7e7), 
   linear-gradient(#e7e7e7, #e7e7e7),
   linear-gradient(#e7e7e7, #e7e7e7),
   linear-gradient(#e7e7e7, #e7e7e7),
   linear-gradient(#e7e7e7, #e7e7e7), 
   linear-gradient(#e7e7e7, #e7e7e7),
   linear-gradient(#e7e7e7, #e7e7e7),
    linear-gradient(#e7e7e7, #e7e7e7);
    background-size: 100% 195px,
    95% 25px,
    90% 20px,
    90% 20px,
    90% 20px,
    90% 20px,
    90% 20px,
    90% 20px,
    40% 15px;
    background-position: 0 0,
    0 210px,
    0 250px,
    0 277px,
    0 304px,
    0 331px,
    0 358px,
    0 385px,
    0 420px;
    background-repeat: no-repeat;
}

Últimos detalles

Para probar el funcionamiento primero agregamos el CSS del post :

a {
   color: #337ab7;
   text-decoration: none;
}

.single-blog-container{
   text-align: left;
}

.single-blog-container .blog-image {
   margin-bottom: 15px;
}

.single-blog-container .blog-image img {
   width: 100%;
   height: auto;
}

.blog-title {
   margin-bottom: 10px;
   font-size: 55px;
   color: #222;
   font-weight: 400;
   line-height: 25px;
}

.single-blog-container .blog-title h2 {
   margin-bottom: 10px;
   font-size: 19px;
   color: #222;
   font-weight: 400;
   line-height: 25px;
   font-family: "Libre Baskerville",Georgia,serif;
}

.single-blog-container .blog-text {
   margin-bottom: 15px;
   font-size: 14px;
   line-height: 22px;
   color: #222;
   font-family: Muli,sans-serif;
}

.single-blog-container .blog-date {
   padding-bottom: 20px;
   border-bottom: 1px solid #e1e0e1;
   line-height: 22px;
   text-transform: uppercase;
   color: #7e7e7e;
   font-family: Muli,sans-serif;
   font-size: 12px;
}

 

Luego agregaremos el código HTML :

<div class="blog-element"><div class="single-blog-container" *ngIf="showPost">
   <a href="/blog/deprecated-while-each-php7-2-x" title="Deprecated while each() PHP7.2.x" alt="Deprecated while each() PHP7.2.x">
      <div class="blog-image">
         <img src="https://blog.nova.es/wp-content/uploads/2019/01/programar.jpg">
      </div>
      <div class="blog-title">
         <h2>Deprecated while each() PHP7.2.x</h2>
      </div>
      <div class="blog-text">
         Para solucionar el error Deprecated list() = each() dentro de un while , en la versión PHP7.2.x, podemos usar varias soluciones : 1. Sin $value : Código deprecated : while (list($key, ) = each($array)) {{'{'}} Código modificado: foreach(array_keys($array) as $key) {{'{'}} &nbsp; 2. Con $value y $key: Código deprecated : while (list($key, $value) = each($array)) …
      </div>
      <div class="blog-date">
         <span>18 Diciembre. 2018</span>
      </div>
   </a>
</div></div>

Es muy importante que no se agregen ni tabulaciones ni espacion entre el div blog-element y el div-single-post-container ya que como comantamos anteriormente, la pseudoclase empty interpretaria esto como contenido y no funcionaria correctamente.

Para terminar en el componente debemos de poner un setTimeout para simular una carga de contenido por AJAX:

import { Component, OnInit } from '@angular/core';

@Component({
   selector: 'app-blog-item',
   templateUrl: './blog-item.component.html',
   styleUrls: ['./blog-item.component.scss']
})
export class BlogItemComponent implements OnInit 
{
   showPost = false;
   constructor()
   {
      setTimeout(() => {
         this.showPost = true;
      }, 1500);
   }
   ngOnInit() {
   }
}

Animación

Con esto ya sería totalmente funcional nuestro Skeleton, pero podemos ir más lejos y agregar un efecto de entrada y salida al contenido del post.

Ésto lo podemos hacer fácilmente gracias al componente de animaciones que tiene integrado el core de Angular.

Para poder hacer esto tenemos que importar el componente BrowserAnimationsModule en el app.module.ts de nuestro proyecto:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { HomeComponent } from './pages/home/home.component';
import { BlogItemModule } from './partials/blog-item/blog-item.module';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

@NgModule({
   declarations: [
      AppComponent,
      HomeComponent
   ],
   imports: [
      BrowserModule,
      BlogItemModule,
      BrowserAnimationsModule
   ],
   providers: [],
   bootstrap: [AppComponent]
})
export class AppModule {}

 

Luego en el blog-item.component.ts tenemos que importar los componentes trigget, style, animate y transition:

@Component({
selector: 'app-blog-item',
templateUrl: './blog-item.component.html',
styleUrls: ['./blog-item.component.scss'],
animations: [
trigger(
'gamesSkeletonAnimation', [
transition(':enter', [
style({opacity: 0}),
animate('100ms', style({opacity: 1}))
]),
transition(':leave', [
style({opacity: 1}),
animate('100ms', style({opacity: 0}))
])
]
),
],
})
export class BlogItemComponent implements OnInit 
{
showPost = false;
constructor()
{
setTimeout(() => {
this.showPost = true;
}, 1500);
}
ngOnInit() {}
}

 

Viendo el código vemos que en el decorador @Component hay un nuevo parámetro llamado animations el cual tiene una función definida llamada trigger que tiene como primer parámetro el nombre de la animación y como segundo parámetro el comportamiento de las diferentes transiciones que puede tener el elemento que tenga esta animación asignada.

Para más detalle puedes ir a la web oficial de angular para ver mas información sobre estas animaciones y sus diferentes estados.

 

Y por último asignamos la animacion al div single-blog-container que es el que tiene el *ngIf que muestra el contenido

<div class="blog-element"><div class="single-blog-container" *ngIf="showPost" [@gamesSkeletonAnimation]>
   <a href="/blog/deprecated-while-each-php7-2-x" title="Deprecated while each() PHP7.2.x" alt="Deprecated while each() PHP7.2.x">
      <div class="blog-image">
         <img src="https://blog.nova.es/wp-content/uploads/2019/01/programar.jpg">
      </div>
      <div class="blog-title">
         <h2>Deprecated while each() PHP7.2.x</h2>
      </div>
      <div class="blog-text">
         Para solucionar el error Deprecated list() = each() dentro de un while , en la versión PHP7.2.x, podemos usar varias soluciones : 1. Sin $value : Código deprecated : while (list($key, ) = each($array)) {{'{'}} Código modificado: foreach(array_keys($array) as $key) {{'{'}} &nbsp; 2. Con $value y $key: Código deprecated : while (list($key, $value) = each($array)) …
      </div>
      <div class="blog-date">
         <span>18 Diciembre. 2018</span>
      </div>
   </a>
</div></div>

 

Descárgate el proyecto desde nuestro github.