Skip to content
This repository has been archived by the owner on Aug 5, 2021. It is now read-only.

Styleguide

Owen Buckley edited this page Sep 3, 2018 · 5 revisions

Just as important as the way the application is written, is how to structure and organize the application. This section covers general conventions for designing and architecting a web application that builds around Component Driven Development and the App Shell Model.

Components are not owned by any framework. With ES2015+ import, th file system becomes more flexible. Make sure to organize and adapt your code organization as best suits the needs of your app and team. The following is just guide that favors flat directories where possible and birds of a feather co-location for component assets.

Project Structure

Setting up our workspace is a good first step for starting any application. Let's take a look at what makes up a typical project structure:

.
├── src
│   ├── app
│   │   ├── app.css
│   │   └── app.js
│   ├── components
│   │   ├── footer
│   │   │   ├── footer.css
│   │   │   ├── footer.js
│   │   │   └── footer.spec.js
│   │   └── header
│   │       ├── images
│   │       │   └── banner.jpg
│   │       ├── header.css
│   │       ├── header.js
│   │       └── header.spec.js
│   ├── pages
│   │   └── home
│   │       ├── home.css
│   │       └── home.js
│   └── services
│       ├── validation.js
│       └── validation.spec.js
│   ├── index.html
│   ├── index.js
│   └── favicon.png
├── .babelrc
├── .editorconfig
├── .eslintrc
├── .gitattributes
├── .gitignore
├── README.md
├── package.json
├── postcss.config.js
├── webpack.config.common.js
├── webpack.config.develop.js
├── webpack.config.prod.js
└── yarn.lock
  • / - at the root of the project are our configuration files i.e. .eslintrc, .babelrc, webpack configs, etc
  • src/app/ - main application module, referenced in the entry point
  • src/components/ - resusable UI component modules (custom elements)
  • src/services/ - non UI functions / classes for handling + wrapping backend REST or browser APIs calls, typically not tied to the UI
  • src/pages/ - routable "views" / states (also custom elements)
  • src/index.html - main layout of the application, the "app shell"
  • src/index.js - main entry point into the application, responsible for using import to load the app module
  • test/ - integration, E2E tests

Component Driven Development (Web Components)

The core building blocks of a modern web application, "Web Components" help encapsulate business and rendering logic using a set of complimentary Web APIs (Custom Elements, Shadow DOM, HTML Templates).

By subclassing the HTMLElement base class and combining that with lit-html, we can create our own reusable HTML tags and glue that to a class or function.

Sample Component

So let's take a look at one now! The component belows renders a list of "Todo" items and shows how an event can be setup to add a new "todo" to the list when a button in the template is clicked.

JavaScript

import { html, render } from 'lit-html';
import css from './todo-list.css';

class TodoList extends HTMLElement {
  
  constructor() {
    super();

    // our todos "list" property
    this.todos = [];

    // setup Shadow DOM
    this.root = this.attachShadow({ mode: 'closed' });
    
    // render the result of this.template() to this.root
    render(this.template(), this.root);
  }

  addTodo() {
    // notice we can access elements within the Shadom DOM using standard browser APIs
    const inputElement = this.root.getElementById('todo-input');

    if (inputElement.value && inputElement.value !== '') {
      this.todos.push({
        completed: false,
        task: newTodoValue,
        id: new Date().getTime(),
        createdTime: new Date().getTime()
      });

      // reset the value of our <input> element and then re-render the element
      inputElement.value = '';
      render(this.template(), this.root);
    } else {
      console.warn('invalid input, please try again');
    }
  }

  renderTodoListItems() {
    return this.todos.map((todo) => {
      return html`
        <li>
          ${todo.task}<span class="delete-todo">X</span>
        </li>
      `;
    });
  }

  template() {
    return html`
      <style>
        ${css}
      </style>
      
      <div>
        <h3><u>My Todo List 📝</u></h3>

        <input id="todo-input" type="text" placeholder="Food Shopping" required/>
        <button id="add-todo" onclick=${ this.addTodo.bind(this) }>+ Add</button>

        <ol>
          ${ this.renderTodoListItems() }
        </ol>
    
      </div>
    `;
  }
}

customElements.define('todo-list', TodoList);

With tools like Babel, we can safely use all these modern features and let the Build Pipeline take care of the rest.

CSS

Although a lot of attention gets paid to JavaScript, CSS has come a long way too. Although spec work is still ongoing to mature a lot of the developer experience features valued in tools like Less and Sass, luckily we have PostCSS to help us out. 👍

Let's take a look at some modern CSS, as in this example for a <Card> component.

/* :host psuedo-selector to scope CSS specifically to an element */
:host {

  /* & lets us nest selectors */
  & .card {
    width: 70%;
    margin: 0 auto 35px;
    max-width: 1024px;
    min-width: 320px;
    border: 2px solid #020202;
    border-radius: 5px;
    padding: 10px;
  }

  /* using CSS Grid within our .wrapper classes */
  & .wrapper {
    display: grid;
    grid-template-columns: repeat(12, [col-start] 1fr);
    grid-gap: 20px;
  }

  & .wrapper > * {
    grid-column: col-start / span 12;
  }

  & .card-header, .card-content {
    text-align: left;
  }

  /* custom media queries */
  & @media (min-width: 500px) {
    .card-header-icon {
      display: none;
    }

    .card {
      border: none;
    }
  }

  & @media (min-width: 700px) {
    .card {
      border: 2px solid #020202;
    }
    
    .card-header-icon {
      display: inline;
      grid-column: col-start / span 2;
      grid-row: 1 / 6;
    }

    .card-header {
      grid-column: col-start 3 / span 10;
      grid-row: 1 / 6;
    }
  }
}

Event Handling (Pass Down, Bubble Up)

Event handling plays an important part in inter-component communication. For smaller / more compartmentalized applications or when a central, shared state management solution (like Redux) isn't needed, the browsers CustomEvent API can be used to facilitate this communication.

Below is an example of the TodoList component (from above) registering two custom events that are then used by a TodoListItem to propogate state updates to the parent component, using addEventListener

// TodoList component
constructor() {
  super();

  this.todos = [];
  this.root = this.attachShadow({ mode: 'closed' });

  document.addEventListener('deleteTodo', (event) => this.deleteTodo(event.detail));
  document.addEventListener('completeTodo', (event) => this.completeTodo(event.detail));

  render(this.template(), this.root);
}

// TodoListItem component
class TodoListItem extends HTMLElement {
  constructor() {
    super();
    
    this._todo = {};
    this.root = this.attachShadow({ mode: 'closed' });

    render(this.template(), this.root);
  }

  /* code */
  
  dispatchCompleteTodoEvent() {
    const event = new CustomEvent('completeTodo', { detail: this._todo.id });

    document.dispatchEvent(event);
  }

  dispatchDeleteTodoEvent() {
    const event = new CustomEvent('deleteTodo', { detail: this._todo.id });

    document.dispatchEvent(event);
  }

  template() {
    const isCompleted = this._todo.completed;
    const completionStatus = isCompleted ? '✅' : '⛔';

    return html` 
      <style>
        ${css}
      </style>

      <span>
        ${this._todo.task}

        <input class="complete-todo" type="checkbox" checked=${isCompleted} onchange=${ this.dispatchCompleteTodoEvent.bind(this) }/>
        <span>${ completionStatus }</span>
            
        <span class="delete-todo" onclick=${ this.dispatchDeleteTodoEvent.bind(this) }></span>
      </span>
    `;
  }
}

Recommended Documentation