Arquitectura de interfaces web: Parte 2

Vistas, componentes y renderizado


Vistas / Componentes

Una casa con buenas “vistas”, pero estará construida sobre cimientos sólidos? Photo by Cindy Tang on Unsplash
Primer storyboard de nuestra aplicación de ejemplo

Qué es una vista (view)?

Si nos fijamos bien en el storyboard, podríamos decir que los dos primeros rectángulos representan dos estados de una misma vista o página. Ésta va a ser nuestra primera abstracción a nivel de arquitectura: las vistas. En líneas generales, podemos decir que las vistas tienen tres características principales.

  1. Se pueden combinar/anidar en una estructura de árbol En el caso de las aplicaciones web, solemos imaginar cada página como una vista independiente. Pero las vistas no solo representan estas páginas o pantallas, sino que normalmente nos permiten combinarlas para componer vistas que contienen vistas que a su vez pueden contener a otras. Algo parecido a lo que ocurre en HTML cuando anidamos una etiqueta dentro de otra y así sucesivamente, construyendo una estructura de árbol.
  2. Ofrecen posibilidad de interacción con el usuario Además de mostrar data al usuario, las vistas son el punto de interacción con el usuario, a través de botones, links, inputs, … Las vistas deben poder capturar esa interacción y comunicar la intención del usuario al resto de la aplicación.

Implementando un sistema de vistas

Para nuestro ejemplo vamos a inspirarnos en los componentes stateless de React. De hecho, a partir de ahora me voy a permitir el lujo de hablar de vistas y componentes indistintamente.

HTMLElement MyComponent(props)
import SignIn from '../../src/pages/SignIn';describe('SignIn', () => {
  it('should return div with nested button', () => {
    const el = SignIn();
    expect(el.tagName).toBe('DIV');
    expect(el.className).toBe('sign-in');
    expect(el.children.length).toBe(1);
    expect(el.children[0].tagName).toBe('BUTTON');
    expect(el.children[0].innerText).toBe('Sign in');
  });
});
npx jest test/pages/SignIn.spec.js
export default () => {
  const page = document.createElement('div');
  page.className = 'sign-in';  const signInButton = document.createElement('button');
  signInButton.innerText = 'Sign in';  page.appendChild(signInButton);
  return page;
};
npx jest test/pages/SignIn.spec.js
export default () => {
  const el = document.createElement('button');
  el.innerText = 'Sign in';
  return el;
};
import SignInButton from '../components/SignInButton';export default () => {
  const el = document.createElement('div');
  el.className = 'sign-in';
  el.appendChild(SignInButton());
  return el;
};
{
  "env": {
    "jest": true
  }
}
export default (props) => {
  const el = document.createElement('button');
  el.innerText = props.text;
  return el;
};
import Button from './Button';export default () => Button({ text: 'Sign in' });

HTMLElement createElement(tagName, options)
export default (tagName, opts = {}) => {
  const { children, ...rest } = opts;
  const element = Object.assign(
    document.createElement(tagName),
    rest,
  );  if (children && typeof children.forEach === 'function') {
    children
      .filter(item => item)
      .forEach(element.appendChild.bind(element));
  }  return element;
};
import createElement from '../lib/createElement';export default props => createElement('button', {
  innerText: props.text,
});
import createElement from '../lib/createElement';
import SignInButton from '../components/SignInButton';export default () => createElement('div', {
  className: 'sign-in',
  children: [SignInButton()],
});

Render

Es hora de llevar nuestras vistas a la pantalla! Hasta ahora hemos usado nuestros componentes desde tests. Ahora nos toca pintar estas vistas como prometimos. Para eso vamos a introducir el concepto de renderizado. El verbo render (en inglés) significa algo como entregar o materializar. En el contexto de interfaces, hablamos de render para referirnos al acto de materializar una vista (normalmente aplicándole una data) y mostrarla en la pantalla.

void render(Component, target)
describe('render', () => {
  it('should render component in given target', () => {
    const Component = () => document.createElement('div');
    const target = document.createElement('div');
    expect(render(Component, target)).toBeUndefined();
    expect(target.children.length).toBe(1);
    expect(target.children[0] instanceof HTMLDivElement)
      .toBe(true);
  });  it('should empty target before rendering', () => {
    const Component = () => document.createElement('div');
    const target = document.createElement('div');
    expect(render(Component, target)).toBeUndefined();
    expect(target.children.length).toBe(1);
    expect(target.children[0] instanceof HTMLDivElement)
      .toBe(true);    expect(render(Component, target)).toBeUndefined();
    expect(target.children.length).toBe(1);
    expect(target.children[0] instanceof HTMLDivElement)
      .toBe(true);
  });  it('should not append when component returns falsy', () => {
    const target = document.createElement('div');
    expect(render(() => null, target)).toBeUndefined();
    expect(target.children.length).toBe(0);
  });
});
export default (Component, target) => {
  const child = Component({});
  if (child) {
    Object.assign(target, { innerHTML: '' });
    target.appendChild(child);
  }
};
import render from './lib/render';
import SignIn from './pages/SignIn';render(SignIn, document.getElementById('root'));
yarn start


Laboratoria Devs

Un blog curado por el chapter de developers de Laboratoria 🚀 ❤️

Lupo Montero

Written by

Laboratorian, JavaScripter and UNIX man

Laboratoria Devs

Un blog curado por el chapter de developers de Laboratoria 🚀 ❤️