Components

Wee 4 comes with many changes and improvements, but none are as impactful as this one. Wee is now, at it's core, a component based framework. There is a lot to unpack here. This guide will attempt to explain the basics of components and how they are implemented in Wee.

Though the implementations are diverse, the concept of building user interfaces with components is one that is dominating web development, and rightly so. A component is a simple reusable piece of markup (HTML), functionality (JS), and styling (CSS). Building with components allows you to break down all the complexity of a design into it's most elemental pieces, building them one at a time, and plugging them together to form the end product. It is a game changer. Let's look at an example to get a better understanding. Below is a screenshot of the product index page for the wee-demo site:

Let's look at how we might start to break this down into components:

This is a good start. We have identified a header (blue), filters (red), products (green), breadcrumbs (purple), and pagination (orange). We are just at the start of identifying components on the page, but we have already identified two very distinct types of components in terms of styling. We call these layouts and modules.

Layouts and Modules #

The naming convention for the two types of components comes from the system known as SMACSS. Though we do not follow it verbatim, we have taken important concepts and adapted it into our own system that uses BEM as the primary mode of CSS organization. The definitions for the two component types are as follows.

Modules #

These are components that are completely unbiased in the context that they are used, and therefore are able to be dropped anywhere on the page.

Layouts #

Layouts hold one or more modules in a particular arrangement. They are only concerned with the orchestration of modules on the page. Classes identifying layouts have l- prefixed at the beginning of the class.

Looking back at the components we have identified so far in our example, we can see that some components are modules and others are layouts. Products (green) and header (blue) are layouts. Filters (red), breadcrumbs (purple), and pagination (orange) are modules. From looking at the design with this new lense, you might see that we are in fact not accounting for an existing layout, the one that holds breadcrumbs, products, and pagination on one side and filters on the other. Let's highlight this one in yellow and call it product-index as it defines the grid of the product index page (there is probably a better name, but this will do for now).

Now, let's continue dissecting the page by looking at the filters module. Here is how we might break it down into it's most elemental inner modules.

We now have a filter component (blue) which is comprised of a heading and another component of some kind, a toggle (green), checkbox-list (orange), checkbox (purple), and color selector (yellow). We have now identified all the modules that will need to be built as part of building the filters on the product index page. We would continue this process, identifying the modules as granularly as necessary before diving into building. Once that is complete, it is time to start building. As an example, let's create a checkbox component.

To create a new component in Wee, we can utilize the Wee CLI. In the top level of the wee project in the terminal, we call wee component -n checkbox. Within source/components, a new directory will be created that is named checkbox. This directory will have two files: index.js and index.pcss. Any styling specific to the component will go in the pcss file and any JavaScript logic specific to the component will go into the js file. This component is one of two distinct types: standard and Vue.

Standard vs Vue Components #

Standard #

Standard components consist of styling and/or scripting. They are intended to be used for pages that are primarily static/non-functional. This captures a good portion of web pages on a typical marketing site.

Often times for an agency, HTML is sourced from a CMS of some kind. For Lewis Communications, this CMS is typically built on PHP and utilizes a templating language like Twig. The relevancy of this to our component structure is that when we are building mainly static marketing pages for a site, we will derive the whole HTML document from the CMS rather than packaging the component-specific HTML directly with our front-end components that will live in our source/components directory in Wee. This is the reason that a standard component will include only a js and pcss file. In the event that there are minimal templating needs directly in the component, we utilize ES2015 template literals. We place extra emphasis on minimal templating. Once conditional logic or looping is required for the component, strongly consider sourcing the HTML from the server or creating a Vue component.

Vue #

Vue components are the tool of choice for complex, dynamic, and functional pages. If you have not checked into Vue.js, we highly recommend it. Wee 4 makes Vue a first-class citizen. As a result, you can generate a vue component directly from the CLI by passing the -v flag: wee component -n aVueComponent -v. This command will create a component that contains a pcss file for styling and a vue file. The vue file will contain both the template/markup for the component and the script that defines the component. If you would like to know more about .vue files, read more on Vue Loader. This file can be imported and registered as a child component of any other Vue instance of Vue component.

Vue requires a root component or Vue instance be registered. This defines the point where Vue anchors to the page. For a typical single page application, you will have one single root, as the entire page is one single tree of Vue components. However, when creating a hybrid website that contains Vue components only in select portions of the site, you may have more than one root instance on the document. As a result, we have made it easy to generate a root Vue component by passing the -r flag: wee component -n aVueComponent -vr.

Back to our checkbox, we now have our component base. As we are creating a standard checkbox component, we are assuming that the markup for the checkbox will be part of the HTML document served up by our CMS on the server. Let's say it looks something like this:

<div class="checkbox js-checkbox">
  <input type="checkbox" name="{{ item.value }}" class="checkbox__input" id="filter-{{ item.value }}" {% if item.isSelected %}checked{% endif %}>
  <label for="filter-{{ item.value }}" class="checkbox__label">
    <span class="checkbox__title">{{ item.title }}</span>
  </label>
</div>

In our example, we are creating the component markup with Twig. We are assuming that this is an include that is being placed in the HTML document to be served to the browser where needed. We are also assuming that we have a variable called item that has value, title, and isSelected properties to provide the initial state of each checkbox added to the page.

Note: If you are curious how we build component templates in the CMS to correspond with our front-end components, see DRY Templating with Twig and Craft CMS for more information on our approach. The TLDR version is that we use macros to create and organize component HTML. Ideally, the name of the macro will be identical to the name of the component defined in your source/components directory to make it easy to correlate the two together.

The next thing we will address is styling our checkbox component. I won't go into too much detail here, other than to mention we are creating a custom checkbox with a hidden input and styled pseudo element on the sibling label element, hence the &:checked + .checkbox__label selector. Normally, we stray from selectors that rely too heavily on the structure of markup, but here it is necessary. Also, this shows a glimpse of the syntax available with our custom PostCSS system. To read more on this subject, see the guide for PostCSS.

.checkbox {
    $checkboxHeight: 1.6rem;
    $borderWidth: 1px;
    $padding: 0.6rem;

    block();
    cursor: pointer;
    font-size: 1.4rem;
    &__label {
        block();
        padding(vertical, $padding);
        cursor: pointer;
        height: calc($padding * 2 + $checkboxHeight);
        position: relative;
        &::before {
            content();
            rounded(2px);
            square($checkboxHeight);
            border: solid $borderWidth $colors.gray;
            display: inline-block;
            vertical-align: middle;
        }
    }
    &__title {
        display: inline-block;
        line-height: 1em;
        margin-left: 1rem;
        user-select: none;
        vertical-align: bottom;
    }
    &__input {
        hide();
        &:checked + .checkbox__label {
            &::after {
                absolute();
                icon($icon.checkmark, 1.1);
                color: $colors.blue;
                left: 0;
                text-align: center;
                top: calc($padding + 1px + $checkboxHeight / 2);
                width: $checkboxHeight;
            }
        }
    }
}

The last thing we need to do in order to make this component functional is we will need to communicate when the value of the input has changed to a parent component, filters in this case, that will update the results on the page. checkbox/index.js will look something like this:

import $mediator from 'wee-mediator';
import $events from 'wee-events';

export default class Checkbox {
  constructor(options) {
    this.selector = options.selector || '.js-checkbox';
    this.group = options.group || null;
    this.namespace = options.namespace;

    // We will assume that guid is already a defined method 
    // that returns a unique string.
    this.id = guid();

    this.bind();
  }

  /**
  * Initialize checkbox to emit checked value when toggled
  */
  bind() {
    $events.on(this.selector, `change.${this.id}`, (e, el) => {
      $mediator.emit(`${this.namespace}.change`, {
        group: this.group,
        name: el.name,
        value: el.checked
      });
    });
  }

  /**
  * Clean up when checkbox needs to be destroyed
  */
  destroy() {
    $events.off(false, `.${this.id}`);
  }
}

This module exports a Checkbox class. Each instance of a checkbox will emit a message when toggled through wee-mediator, passing the current checked state of that particular checkbox to whoever is listening. In a parent filters component, we will listen for this message and update the product results. The parent filters component could look something like this:

import $ from 'wee-dom';
import $mediator from 'wee-mediator';
import $router, { RouteHandler } from 'wee-routes';
import { $serialize } from 'core/types';
import { uri } from 'wee-location';
import Checkbox from './checkbox';

let checkboxes = null;
let state = {
  category: [],
  size: [],
  gender: [],
  featured: '',
  color: []
};
let selections = Object.assign({}, state);

/**
* Update form data and trigger product result refresh
*/
function update(data) {
  let url = uri().path;
  let hasParams = false;
  let params = {};

  // Generate params object
  Object.keys(selections).forEach((key) => {
    if (selections[key].length) {
      hasParams = true;
      params[key] = selections[key];
    }
  });

  // If params exist, generate query string
  if (hasParams) {
    url = `${url}?${$serialize(params)}`;
  }

  // Use PJAX to navigate with History API and retrieve product results
  $router.push(url);
}

/**
* Initialize all checkboxes inside of filters
*/
function initCheckboxes() {
  $('.js-checkbox', '.js-filters').toArray()
    .forEach((el) => {
      checkboxes.push(new Checkbox({
        group: $(el).closest('.js-filter').data('name'),
        namespace: 'filters'
      }));
    });
}

/**
* Update selection which consists of checkboxes
*/
function toggleSelection(data) {
  let key = data.group;
  let newValue = data.name;
  let index = selections[key].indexOf(newValue);

  index === -1 ?
    selections[key].push(newValue) : 
    selections[key].splice(index, 1);

  update();
}

export default new RouteHandler({
  /**
  * This method will be executed when arriving at the products index page
  */
  init() {
    initCheckboxes();
    $mediator.on('filters-checkbox.change', toggleSelection);
  },

  /**
  * This method will be executed when leaving the products page
  */
  unload() {
    // Destroy checkboxes
    checkboxes.forEach(checkbox => checkbox.destroy());
    $mediator.remove('filters-checkbox.change');

    // Reset selections
    selections = Object.assign({}, state);
  }
});

Let's dissect what is happening here. We are exporting an instance of RouteHandler. A RouteHandler is assigned to one or more route records, and the methods registered (init and unload in this case) are fired at designated times during the route evaluation lifecycle. We are assuming that this RouteHandler will be assigned to the route record for /products which means init will fire when we navigate to the products index page from another location. Likewise, unload will fire when we are navigating away from /products. unload is only necessary when PJAX is enabled on the router. To read more on PJAX and Wee Router, see the guide for Routing.

When init triggers, we initialize the checkbox components and begin listening for changes to occur. When a checkbox is toggled, we execute toggleSelection which updates our selections and calls our generic update method. At this point, we navigate to the updated URL with $router.push which will replace the product results on the page to reflect the most recent selections.

Conclusion #

We just created a reusable checkbox component that is working within a larger filters component. It was an intentional decision to build standard components in this article as they are a little more open ended in how their application, and therefore need a bit more demonstration. We could (and arguably should depending on the needs of the rest of the site) build everything we just did as Vue components rather than standard. You can see a detailed example of Vue components by viewing the Wee Demo project. Wee attempts to allow for a range of UI complexity while providing a similar architectural experience. This will continue to take shape and mature as Wee 4 moves forward.