aprende-a-crear-un-grid-system

A lo largo de esta serie, hemos tratado de explicar la mayoría de características de Sass. Con lo que tenemos, es posible comenzar a elaborar librerías o componentes de alta calidad, siguiendo buenas prácticas y haciendo uso de todo lo aprendido.

Hoy, vamos a realizar un artículo mucho más práctico, crearemos un Grid system. Es fundamental estar al día con la serie Aprende Sass, los invito a revisarla para refrescar conceptos.

Antes de arrancar, me gustaría comentar que hay muchísimas formas de realizar un buen grid system, voy a explicarles la que más me ha gustado y vengo usando en los últimos tiempos.

Grid system:

Es de los componentes más importantes en todo desarrollo.

example-grid-system

Estructura base de un grid system

En esta oportunidad crearemos uno interno, esto quiere decir que no necesitaremos las clases del tipo .col3, .col4 para crear nuestra estructura. Con esto, vamos a reducir la cantidad de HTML y hacerla más leíble, pongamos un ejemplo:

<!-- HTML con grid system externo (clases de ayuda)  -->
<section class="name-section">
  <div class="col8>
    <div class="first-sub-section">
      <!--  stuff -->
    </div>	
  </div>
  <div class="col4>
    <div class="second-sub-section">
      <!--  stuff -->
    </div>
  </div>
</section>
<!-- HTML con grid system interno: -->
<section class="section">
  <div class="first-sub-section">
    <!--  stuff -->
  </div>
  <div class="second-sub-section">
    <!--  stuff -->
  </div>
</section>

Se nota la reducción y también, al no contar con clases extras, podemos leer más fácilmente el componente.

Vamos a crear un directorio para organizar nuestro proyecto, voy a llamarlo ‘grid-system‘. Puedes darle el nombre que prefieres, no es obligatorio usar el mismo, solo ten en cuenta que si utilizas un nombre diferente tendrás que editar las rutas que vayamos colocando a lo largo de este artículo.

Con nuestro editor de texto favorito abrimos el proyecto y creamos, en el directorio principal, los dos primeros archivos ‘base.scss‘ y ‘_config.scss‘.

  • base.scss: archivo principal donde se importan todos los parciales del proyecto.
  • _config.scss: archivo de configuración.

Recuerden, los archivos que tengan un piso ‘_’ son parciales y los que no, son los que usará Sass para compilar a CSS.

base.scss o _base.scss los creamos en todos los directorios. Los parciales ‘_base.scss‘ irán en los subdirectorios y el ‘base.scss‘ en el directorio principal. Los utilizaremos para agrupar archivos de un directorio y luego facilitar el proceso de importar a otro lugar. Es más sencillo importar un solo archivo base que 20 archivos independientes.

Ahora, abrimos el archivo ‘_config.scss‘ y agregamos las configuraciones iniciales requeridas por nuestro grid system.

//
// Variables:
// Use @function map-fetch($map, $keys)

$grid: (
  col-qty: 12,
  normal: (
    gutter: 20px,
    width: 60px
  ),
  large: (
    gutter: 30px,
    width: 70px
  ) 
);

Básicamente, se trata de una variable de tipo map multidimensional que contiene los valores de configuración requeridos:

  • col-qty: cantidad total de columnas.
  • gutter: margen lateral de cada columna.
  • width: ancho de cada columna.

Para comenzar a darle vida al archivo ‘base.scss‘, vamos a importar el archivo de configuración que acabamos de poblar.

@import 'config'

Seguidamente, creamos tres subdirectorios que serán necesarios (functions, mixins y utilities) y dentro de ellos, seguimos la convención de crear un archivo base ‘_base.scss‘.

Necesitamos conectar los subdirectorios con el archivo base del directorio principal:

@import "config";

// nuevas lineas
//
@import "utilites/base";
@import "functions/base";
@import "mixins/base"

Rápidamente podemos apreciar la ventaja de usar un archivo base en cada uno de los subdirectorios del proyecto. Simplicidad y fácil lectura.

Ya contamos con una estructura principal que nos permitirá desarrollar toda la lógica de nuestro grid system.

Antes de continuar, vamos a ver un breve repaso de lo que hemos completado:

  • Un archivo ‘_config.scss‘ para guardar los datos de configuración.
  • Subdirectorio para las Funciones.
  • Subdirectorio para las Utilidades.
  • Subdirectorio para los Mixins.
  • Por último, un archivo ‘base.scss‘ en el directorio principal y en cada subdirectorio un ‘_base.scss‘.

Ahora sí, ¡continuemos!

Utilidades (utilities):

En el directorio utilities/ (utilidades o herramientas externas), debe ir todo lo que el proyecto necesite para funcionar pero que no sea, directamente, parte de él. Las dependencias.

Lo primero que necesitamos agregar, es una función que nos ayude a manipular fácilmente los maps multidimensionales.

En Sass, contamos con una función ‘map-get‘ que nos permite obtener el valor de una llave, siempre y cuando, el map sea de un solo nivel. Cuando son multidimensionales, el proceso se vuelve un poco más complejo.

Map-fetch:

Para hacernos la vida más fácil, vamos a crear un archivo ‘_map-fetch.scss‘ dentro del directorio ‘utilities‘ y le agregamos la siguiente función:

//
// An easy way to fetch a deep value in a multi-level map.
//
// @param $map
// @param $keys
//
// map-fetch($map, $keys)
//

@function map-fetch($map, $keys) {
  @each $key in $keys {
    $map: map-get($map, $key);
  }
  @return $map;
}

Vamos a comprobar que nuestra función ‘map-fetch‘ está trabajando correctamente. Su uso es similar a ‘map-get‘, solo que esta puede recibir tantas llaves (keys) como sean necesarias.

// Sass
body {
  content: map-fetch($grid, 'normal' 'width');
}

// CSS
body {
  content: 60px; 
  // o el valor que determinemos en el map 
  // para $grid > normal > width.
}
map-fetch

Así va quedando la estructura del grid system.

Otra de las herramientas externas que necesitaremos será, una librería para manipular media queries. Nos ayudará a determinar puntos de quiebre para poder crear estilos específicos para cada tamaño de pantalla.

Para no desviarnos mucho del tema principal, vamos a reutilizar una pequeña librería que desarrolle para mi estructura base de proyectos con Sass.

breakpoint (puntos de quiebre con media queries):

Básicamente, está compuesta de tres archivos:

  • _vars.scss‘: archivo de configuración. Mediante un map agrupamos las medias queries.
//
// Variables:

$breakpoints: (
  xxx-large: "(min-width: 1360px)",
  large: "(min-width: 1024px)",
  small-to-medium: "(min-width: 240px) and (max-width: 1023px)",
  medium-to-large: "(min-width: 600px)",
  medium: "(min-width: 600px) and (max-width: 1023px)",
  small: "(min-width: 240px) and (max-width: 599px)" 
);
  • _mixin.scss‘: se encarga de comprobar si el parámetro «device» se encuentra en el map $breakpoints. Si la comprobación es verdadera, genera el media query y si es falsa, imprime un mensaje de error.
// Generate Media Queries
// @param $device (xxx-large | large | medium-to-large | small-to-medium | medium | small);
// 
// @include breakpoint(large);
// 

@mixin breakpoint($device) {
  @if not map-has-key($breakpoints, $device) {
    @error 'Device value (#{$device}) is unknown';
  } 

  @media #{map-get($breakpoints, $device)} {
    @content;
  }
}
  • _base.scss‘: agrupa los dos archivos explicados anteriormente.
// import all parts of mixin
//
// @import "name-file";

@import "vars";
@import "mixin";

Usarlo es muy fácil, un ejemplo a continuación:

body {
  @include breakpoint(large) {
    // styles for large
  }

  @include breakpoint(medium) {
    // styles for medium
  }
}

Por último, nos queda importar la librería breakpoints y la función map-fetch a nuestro archivo base del directorio /utilities:

// import all utilities
//

@import "map-fetch";
@import "media-queries/base";

Importante:

Las herramientas externas que estamos utilizando son opcionales, pueden utilizar las que prefieran. En caso de seleccionar otras, deben estar atentos en modificar cada una de las partes donde hagamos uso de ellas.

Ya tenemos todo lo necesario para comenzar a trabajar de lleno en el grid system, hagamos un pequeño listado de los siguientes puntos a tratar:

  • Contenedores.
  • Columnas.

Contenedores (Mixins + Functions):

Para agrupar y centrar las diferentes secciones (columnas) de un diseño, necesitamos un contenedor. Vamos a crearlo paso a paso:

Comencemos con los Mixins, vamos a crear un archivo llamado ‘_generate-container.scss‘.

// Generate container
// use calc-container function.
//
// @param $type ("normal" or "inner")
//
// @include container();
// 

@mixin container($type: "normal") {
  @if $type == "inner" {
    width: 100%;
  } @else if $type == "normal" {
    @include width-container();	  
  } @else {
    @error 'Type value (#{$type}) is unknown';		
  }

  margin-left: auto;
  margin-right: auto;

  &:after {
    clear: both;  
    content: '';
    display: block;
  }
}

El Mixin recibe un solo parámetro llamado Type, es utilizado para especificar si el contenedor que vamos a generar se encuentra o no dentro de otro contenedor. El parámetro es opcional y por defecto su valor es «normal».

Seguidamente, comprobamos el valor del parámetro:

  • Si es inner, el ancho siempre será 100%. Es decir, sí el contenedor que vamos a generar se encuentra dentro de otro contenedor, su ancho siempre será el 100% de su padre.
  • Si es normal, utilizaremos los valores de configuración del grid system para calcular el ancho correspondiente. Es importante resaltar que utilizamos otro Mixin ‘width-container‘ para realizar esta tarea.
  • Si el valor de Type no es ninguno de los anteriores, se imprimirá un  mensaje de error indicándonos que no es reconocido y automáticamente se detendrá la ejecución de Sass.

En el mismo archivo ‘_generate-container.scss‘ creamos el Mixin width-container:

// Generate width for container 
//
@mixin width-container() {
  @include breakpoint(large) {
    width: calc-container(map-fetch($grid, normal width), map-get($grid, col-qty), map-fetch($grid, normal gutter));		
  }

  @include breakpoint(xxx-large) {
    width: calc-container(map-fetch($grid, large width), map-get($grid, col-qty), map-fetch($grid, large gutter));
  }

  @include breakpoint(small-to-medium) {
    width: 100%;
  } 		
}

Calculamos, por separado, el ancho del contenedor en diferentes situaciones haciendo uso del Mixin ‘breakpoint‘. Una manera sencilla de manipular los diferentes puntos de quiebre que tendrán los contenedores.

En este punto ya se habrán dado cuenta que en el Mixin ‘width-container‘ no calculamos directamente el ancho sino que llamamos a una función ‘calc-container‘ que se debe encargar de realizar la operación matemática.

Las funciones, serán utilizadas para realizar operaciones y devolver un valor. Con esto, conseguiremos distribuir de una mejor manera las tareas y/o responsabilidades que tendremos que realizar.

Vamos a crear nuestra primera función, dentro del directorio correspondiente, creamos un archivo ‘_calc-container.scss‘ con lo siguiente:

// Calculate width of container
//
// @param $col-width
// @param $col-qty
// @param $gutter
//
// calc-container($col-width, $col-qty, $gutter);
//

@function calc-container($col-width, $col-qty, $gutter) {
  @return ($col-width * $col-qty) + $gutter * ($col-qty - 1) + $gutter;
}

La función recibe tres parámetros, realiza la operación matemática y luego imprime el resultado.

Rápidamente, vamos a comprobar que el generador de contenedores trabaja correctamente:

// Sass
.wrap {
  @include container();
}

.under-wrap {
  @include container("inner");
}
.wrap {
  margin-left: auto;
  margin-right: auto; 
}

@media (min-width: 1024px) {
  .wrap {
    width: 960px; 
  } 
}

@media (min-width: 1360px) {
  .wrap {
    width: 1200px; 
  } 
}

@media (min-width: 240px) and (max-width: 1023px) {
  .wrap {
    width: 100%; 
  } 
}
  
.wrap:after {
  clear: both;
  content: '';
  display: block; 
}

.under-wrap {
  width: 100%;
  margin-left: auto;
  margin-right: auto; 
}

.under-wrap:after {
  clear: both;
  content: '';
  display: block; 
}

Tu CSS debe ser igual o similar (si cambiaste los parámetros de configuración) al código que mostramos arriba. Sí tienes algún problema revisa nuevamente, con calma, cada uno de los pasos anteriores antes de continuar.

Generadores de Columnas (Mixins + Functions):

Primeramente, vamos a crear el Mixin para conocer cuales serán las funciones que necesitamos. Nos dirigimos al directorio correspondiente y creamos un archivo ‘_generate-col.scss‘ con el siguiente código:

// generate columns
// use functions calc-col-fixed and calc-col-percentage.
//
// @param $col-qty
// @param $option-medium (full | half | two-thirds | one-third)
// @param $option-small (full | half | two-thirds | one-third)
// 
// @include col(12, "half", "full");
// 

@mixin col($col-qty: 12, $option-medium: "full", $option-small: $option-medium) {
  @include breakpoint(large) {
    margin-left: map-fetch($grid, normal gutter) / 2;
    margin-right: map-fetch($grid, normal gutter) / 2;
    width: calc-col-fixed($col-qty, "normal");
  }

  @include breakpoint(xxx-large) {
    margin-left: map-fetch($grid, large gutter) / 2;
    margin-right: map-fetch($grid, large gutter) / 2;		
    width: calc-col-fixed($col-qty, "big");
  }

  @include breakpoint(small-to-medium) {
    box-sizing: border-box;
    padding-left: 10px;
    padding-right: 10px;		
  }

  @include breakpoint(medium) {
    width: calc-col-percentage($option-medium);
  }	

  @include breakpoint(small) {
    width: calc-col-percentage($option-small);
  }

  float: left;	
}

Recibe tres parámetros, todos con valores por defecto.

  • $col-qty: número de columnas, por defecto 12.
  • $option-medium: para pantallas medianas (tabletas y algunos teléfonos en landscape), por defecto ‘full’.
  • $option-small: para pantallas pequeñas (teléfonos), por defecto el valor de $option-medium.

Rápidamente nos damos cuenta que usamos el Mixin breakpoint para lograr generar diferentes estilos en cada uno de los puntos de quiebre.

Usamos 2 funciones; la primera para calcular el ancho representado en pixeles ‘cal-col-fixed‘ y la segunda representado en porcentajes ‘cal-col-percentage‘. Vamos a crear esas dos funciones en el directorio correspondiente.

‘_cal-col-fixed’ representado en pixeles, con el siguiente código:

// Calculate column fixed width
//
// @param $col-qty
// @param $type (normal | big)
//
// $result (width | gutter)
//
// calc-col-fixed($col-qty, 'normal');
//

@function calc-col-fixed($col-qty, $type: "normal") {
  $result: 0;
  @if $type == "normal" {
    $result: map-fetch($grid, normal width), map-fetch($grid, normal gutter);
  } @else if $type == "big" {
    $result: map-fetch($grid, large width), map-fetch($grid, large gutter);
  }
  @return (nth($result, 1) * $col-qty) + (nth($result, 2) * $col-qty) - nth($result, 2);
}

Recibe dos parámetros:

  • $col-qty: número de columnas.
  • $type: el tipo puede ser normal o big.

Se comprueba mediante una condición si el valor de $type es normal o big y dependiendo del resultado, se buscan en la variable $grid del archivo de configuración principal ‘_config.scss‘ el ancho de columna ‘col-width’ y el margen lateral ‘gutter’. Por último, esos valores se guardan en una variable $result.

Ya terminada la comprobación del párrafo anterior, se realiza una operación matemática para calcular el ancho total de la columna.

(nth($result, 1) * $col-qty) + (nth($result, 2) * $col-qty) - nth($result, 2)

‘_cal-col-percentages’ representado en porcentaje, con el siguiente código:

// Calculate column width percentage
//
// @param $type (normal | big)
//
// calc-col-fixed($col-qty);
//

@function calc-col-percentage($type) {
  $options: (
    full       : 100%,
    half       : 50%,
    two-thirds : 66.66%,
    one-third  : 33.33%
  );

  @if not map-has-key($options, $type) {
    @error 'Type value (#{$type}) is unknown';
  }
  @return map-get($options, $type);
}

Recibe un solo parámetro:

  • $type tiene cuatro opciones posibles: full, half, two-thirds y one-third.

Tenemos una variable de tipo map multidimensional, con las palabras claves permitidas en el parámetro $type. También se le da un valor a cada una de esas palabras claves.

Luego, nos encontramos un condicional que se encarga de verificar si el parámetro pasado a la función, no se encuentra en $options (map multidimensional). Si el resultado es verdadero se imprime un mensaje de error y si el condicional es falso, buscamos el valor de ese parámetro en el map $options y lo imprimimos.

Nos falta importar las dos Funciones y el Mixin que acabamos de crear. Abrimos el archivo ‘_base.scss‘ del subdirectorio de funciones.

// -----
//
@import "calc-col-fixed";
@import "calc-col-percentage";

Ahora importamos el Mixin.

// -----
//
@import "generate-col";

Vamos a realizar un pequeño ejemplo para comprobar que las columnas se están generando correctamente:

Es muy sencillo generar columnas, solo deben llamar al Mixin col, pasarle tres parámetros y el se encargará de hacer la ‘magia’.

.blog {
  @include col(8, "two-thirds", "full");
}

.sidebar {
  @include col(4, "one-third", "full");
}

Recuerden lo siguiente:

  • Primer parámetro: número de columnas a generar para desktop (computadoras de escritorio).
  • Segundo parámetro: opción para pantallas medianas (tabletas o teléfonos en landscape).
  • Tercer parámetro: opción para pantallas pequeñas (teléfonos).

El CSS quedará así:

.blog {
  float: left; }
  @media (min-width: 1024px) {
    .blog {
      margin-left: 10px;
      margin-right: 10px;
      width: 620px; } }
  @media (min-width: 1360px) {
    .blog {
      margin-left: 15px;
      margin-right: 15px;
      width: 770px; } }
  @media (min-width: 240px) and (max-width: 1023px) {
    .blog {
      box-sizing: border-box;
      padding-left: 10px;
      padding-right: 10px; } }
  @media (min-width: 600px) and (max-width: 1023px) {
    .blog {
      width: 66.66%; } }
  @media (min-width: 240px) and (max-width: 599px) {
    .blog {
      width: 100%; } }

.sidebar {
  float: left; }
  @media (min-width: 1024px) {
    .sidebar {
      margin-left: 10px;
      margin-right: 10px;
      width: 300px; } }
  @media (min-width: 1360px) {
    .sidebar {
      margin-left: 15px;
      margin-right: 15px;
      width: 370px; } }
  @media (min-width: 240px) and (max-width: 1023px) {
    .sidebar {
      box-sizing: border-box;
      padding-left: 10px;
      padding-right: 10px; } }
  @media (min-width: 600px) and (max-width: 1023px) {
    .sidebar {
      width: 33.33%; } }
  @media (min-width: 240px) and (max-width: 599px) {
    .sidebar {
      width: 100%; } }

Muy sencillo, ¿No les parece? con una sola línea de código generamos una completa estructura para que nuestro diseño se ajuste y visualice correctamente en cualquier dispositivo.

Conclusión:

Hoy aprendimos a crear un grid system paso a paso y a medida que fuimos avanzando también vimos cómo organizar, de una manera adecuada y modular, nuestros proyectos.

Si quieren tener acceso al código del grid system, pueden visitar el repositorio. Es importante resaltar que todo el código lo saqué de mi estructura base para proyectos front-end. Si quieres darle un vistazo a un paquete más completo puedes revisar mini.scss

Regístrate hoy en Styde y obtén acceso a todo nuestro contenido.

Lección anterior Cómo usar las directivas @warn y @error en Sass