Nathan Bennett - Big Nerd Ranch Thu, 10 Mar 2022 19:36:01 +0000 en-US hourly 1 https://wordpress.org/?v=6.5.5 Building a Design System and Consuming it in Multiple Frameworks using Stencil.js https://bignerdranch.com/blog/building-a-design-system-and-consuming-it-in-multiple-frameworks-using-stencil-js/ https://bignerdranch.com/blog/building-a-design-system-and-consuming-it-in-multiple-frameworks-using-stencil-js/#respond Thu, 10 Mar 2022 18:57:34 +0000 https://bignerdranch.com/?p=9327 Large organizations with multiple software development departments may find themselves supporting multiple web frameworks across the organization. This can make it challenging to keep the brand consistent across applications as each application is likely implementing its own solution to the brand. What if we could create a design system that could be used across multiple […]

The post Building a Design System and Consuming it in Multiple Frameworks using Stencil.js appeared first on Big Nerd Ranch.

]]>
Large organizations with multiple software development departments may find themselves supporting multiple web frameworks across the organization. This can make it challenging to keep the brand consistent across applications as each application is likely implementing its own solution to the brand. What if we could create a design system that could be used across multiple frameworks? Other organizations may find themselves implementing similar components in various brands for various clients. What if the design system could also be white-labeled?

This blog post will be exploring these questions by building a design system with Stencil.js. The design system will support white-labeling via CSS Variables. Then the design system will be used without a framework, and with the ReactVueSvelte, and Stencil frameworks.

Stencil is a compiler for web components that can build custom elements for use across multiple frameworks. The compatibility of custom elements with various frameworks is tracked by Custom Elements Everywhere.

The code for this blog can be found on GitHub at https://github.com/BNR-Developer-Sandbox/BNR-blog-stencil-design-system.

Building the Components

Each component encapsulates its own CSS and functionality. Developers wanting to publish a web component library can follow the Getting started guide for Stencil. The Stencil API provides decorators and lifecycle hooks to reduce boilerplate code and define implementation patterns.

Stencil Decorators

The Stencil API provides a number of decorators that are removed at compile time.

Each Stencil Component uses the @Component() decorator to declare a new web component defining the tag, styleUrl, and if the Shadow DOM should be used or not.

@Component({
  tag: "ds-form",
  styleUrl: "ds-form.css",
  shadow: true,
})

Lifecycle Hooks

The Stencil API also provides various lifecycle hooks. The only lifecycle hook used in this codebase is the render() method which uses JSX to return a tree of components to render at runtime.

Application Shell

The ds-shell component provides a header, main, and footer section available via slots. The header and footer slots are named slots, while the main content area uses the default unnamed slot. This component provides the general layout for an application.

import { Component, Host, h } from "@stencil/core";

@Component({
  tag: "ds-shell",
  styleUrl: "ds-shell.css",
  shadow: true,
})
export class DsShell {
  render() {
    return (
      <Host>
        <header>
          <slot name="header"></slot>
        </header>
        <main>
          <slot></slot>
        </main>
        <footer>
          <slot name="footer"></slot>
        </footer>
      </Host>
    );
  }
}

Hero

The ds-hero component provides a default slot container and encapsulates the CSS for the component.

import { Component, Host, h } from "@stencil/core";

@Component({
  tag: "ds-hero",
  styleUrl: "ds-hero.css",
  shadow: true,
})
export class DsHero {
  render() {
    return (
      <Host>
        <slot></slot>
      </Host>
    );
  }
}

Form

The ds-form component provides generic form handling by listening for inputchange, and click events that bubble up to the form element. The component uses the @Element() decorator to declare a reference to the host element in order to dispatch the formdata event from the host element when the form is submitted. The component also uses the @State() decorator to declare the internal state of the form data.

import { Component, Element, Host, State, h } from "@stencil/core";

@Component({
  tag: "ds-form",
  styleUrl: "ds-form.css",
  shadow: true,
})
export class DsForm {
  @Element() el: HTMLElement;
  @State() data: any = {};
  onInput(event) {
    const { name, value } = event.target;
    this.data[name] = value;
  }
  onChange(event) {
    const { name, value } = event.target;
    this.data[name] = value;
  }
  onClick(event) {
    if (event?.target?.type === "submit") {
      const formData = new FormData();
      Object.entries(this.data).forEach(([key, value]) => {
        formData.append(key, String(value));
      });
      const formDataEvent = new FormDataEvent("formdata", { formData });
      this.el.dispatchEvent(formDataEvent);
    }
  }
  render() {
    return (
      <Host>
        <form
          // form event
          onInput={(event) => this.onInput(event)}
          onChange={(event) => this.onChange(event)}
          onClick={(event) => this.onClick(event)}
        >
          <slot></slot>
        </form>
      </Host>
    );
  }
}

White-labeling the components

In order to implement white-labeling, CSS Variables are used in the components which can be set by the design system consumer. The example repository only sets three variables to illustrate the idea; a complete design system would likely include more variables including sizes, fonts, etc.

Each application defines CSS Variables to implement a different brand.

:root {
  /* CSS Variables for theming white-labeled components */
  --primary-color: red;
  --secondary-color: gold;
  --tertiary-color: green;
}

Each application also defines a simple CSS Reset.

body {
  margin: 0;
  background-color: var(--tertiary-color);
}

Consuming the components

While consuming the custom elements the same HTML structure was used in each framework. Each example application implemented in each framework composes the ds-shell component for application layout, with a ds-hero to provide a call-to-action content area, and a unique form and form handler to process the form.

<ds-shell>
  <h1 slot="header">App Name</h1>
  <ds-hero>
    <ds-form>
      <label>
        Your Name:
        <br />
        <input type="text" name="name" />
      </label>
      <br />
      <label>
        Your Expertise:
        <br />
        <input type="text" name="expertise" />
      </label>
      <br />
      <input type="submit" value="Say Hello" />
    </ds-form>
  </ds-hero>
  <span slot="footer">
    <span>1</span>
    <span>2</span>
    <span>3</span>
  </span>
</ds-shell>

Event binding

The main difference between frameworks was binding the formdata event. Each framework has a slightly different syntax for declaring event handlers. The following examples demonstrate the event binding syntax for each framework.

Without a Framework

The Design System includes an implementation of the application without a framework in index.html. CSS Variables are defined in a <style> tag in the <head> tag. The event listener is added in a <script> tag at the end of the <body> tag.

<!DOCTYPE html>
<html dir="ltr" lang="en">
  <head>
    <meta charset="utf-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=5.0"
    />
    <title>Design System</title>

    <script type="module" src="/build/design-system.esm.js"></script>
    <script nomodule src="/build/design-system.js"></script>
    <style type="text/css">
      :root {
        /* CSS Variables for theming white-labeled components */
        --primary-color: red;
        --secondary-color: gold;
        --tertiary-color: green;
      }

      body {
        margin: 0;
        background-color: var(--tertiary-color);
      }
    </style>
  </head>
  <body>
    <ds-shell>
      <h1 slot="header">App Name</h1>
      <ds-hero>
        <ds-form>
          <label>
            Your Name:
            <br />
            <input type="text" name="name" />
          </label>
          <br />
          <label>
            Your Expertise:
            <br />
            <input type="text" name="expertise" />
          </label>
          <br />
          <input type="submit" value="Say Hello" />
        </ds-form>
      </ds-hero>
      <span slot="footer">
        <span>1</span>
        <span>2</span>
        <span>3</span>
      </span>
    </ds-shell>

    <script>
      function handleFormData(event) {
        event.stopPropagation();
        const { formData } = event;
        const data = Object.fromEntries(formData);
        const { name, expertise } = data;
        alert(`Hello ${name}, I hear you are good at ${expertise}.`);
      }

      const form = document.getElementsByTagName("ds-form")[0];
      form.addEventListener("formdata", (event) => handleFormData(event));
    </script>
  </body>
</html>

In order to add the event listener without a framework the native addEventListener method is used.

form.addEventListener("formdata", (event) => handleFormData(event));

React

The React application is implemented in App.js with CSS Variables in index.css.

import "design-system/ds-shell";
import "design-system/ds-hero";
import "design-system/ds-form";

function handleFormData(event) {
  event.stopPropagation();
  const { formData } = event;
  const data = Object.fromEntries(formData);
  const { reaction } = data;
  alert(`I am surprised that you reacted ${reaction}.`);
}

function App() {
  return (
    <ds-shell>
      <h1 slot="header">React App</h1>
      <ds-hero>
        <ds-form onformdata={(event) => handleFormData(event)}>
          <label>
            How did you react?:
            <br />
            <input type="text" name="reaction" />
          </label>
          <br />
          <input type="submit" value="What is your reaction?" />
        </ds-form>
      </ds-hero>
      <span slot="footer">
        <span>1</span>
        <span>2</span>
        <span>3</span>
      </span>
    </ds-shell>
  );
}

export default App;

React uses a lowercase onformdata attribute in JSX to bind the formdata event to a handler.

<ds-form onformdata={(event) => handleFormData(event)}>
  <!-- ... -->
</ds-form>

Vue

The Vue application is implemented in App.vue with CSS Variables in base.css.

<script setup>
  import "design-system/ds-shell";
  import "design-system/ds-hero";
  import "design-system/ds-form";

  function handleFormData(event) {
    event.stopPropagation();
    const { formData } = event;
    const data = Object.fromEntries(formData);
    const { view } = data;
    alert(`${view}!?! Wow! What a view!`);
  }
</script>

<template>
  <ds-shell>
    <h1 slot="header">Vue App</h1>
    <ds-hero>
      <ds-form @formdata="handleFormData">
        <label>
          How's the view?:
          <br />
          <input type="text" name="view" />
        </label>
        <br />
        <input type="submit" value="Looking good?" />
      </ds-form>
    </ds-hero>
    <span slot="footer">
      <span>1</span>
      <span>2</span>
      <span>3</span>
    </span>
  </ds-shell>
</template>

<style>
  @import "@/assets/base.css";
</style>

Vue uses a lowercase @formdata attribute set to the function name to bind the formdata event to a handler.

<ds-form onformdata={(event) => handleFormData(event)}>
  <!-- ... -->
</ds-form>

Svelte

The Svelte application defines application variables and the form handler in main.js.

import App from "./App.svelte";

const app = new App({
  target: document.body,
  props: {
    appName: "Svelte App",
    fieldLabel: "Your Location",
    fieldName: "location",
    submitLabel: "Where you at?",
    handleFormData: (event) => {
      event.stopPropagation();
      const { formData } = event;
      const data = Object.fromEntries(formData);
      const { location } = data;
      alert(`Where's ${location}?`);
    },
  },
});

export default app;

App.svelte uses the variables from main.js to render HTML and bind events.

<script>
  export let appName, fieldLabel, fieldName, submitLabel, handleFormData;
  import "design-system/ds-shell";
  import "design-system/ds-hero";
  import "design-system/ds-form";
</script>

<ds-shell>
  <h1 slot="header">{appName}</h1>
  <ds-hero>
    <ds-form on:formdata="{handleFormData}">
      <label>
        {fieldLabel}:
        <br />
        <input type="text" name="{fieldName}" />
      </label>
      <br />
      <input type="submit" value="{submitLabel}" />
    </ds-form>
  </ds-hero>
  <span slot="footer">
    <span>1</span>
    <span>2</span>
    <span>3</span>
  </span>
</ds-shell>

The CSS Variables are defined in global.css.

Svelte uses a lowercase on:formdata attribute to bind the formdata event to a handler.

<ds-form on:formdata="{handleFormData}">
  <!-- ... -->
</ds-form>

Stencil

The Stencil application is implemented in app-root.tsx with CSS Variables in app.css.

import { Component, h } from "@stencil/core";
import "design-system/ds-shell";
import "design-system/ds-hero";
import "design-system/ds-form";

@Component({
  tag: "app-root",
  styleUrl: "app-root.css",
  shadow: true,
})
export class AppRoot {
  handleFormData(event) {
    event.stopPropagation();
    const { formData } = event;
    const data = Object.fromEntries(formData);
    const { expertise } = data;
    alert(`So you are good with ${expertise}...`);
  }
  render() {
    return (
      <ds-shell>
        <h1 slot="header">Stencil App</h1>
        <ds-hero>
          <ds-form onFormData={(event) => this.handleFormData(event)}>
            <label>
              Your Expertise:
              <br />
              <input type="text" name="expertise" />
            </label>
            <br />
            <input type="submit" value="Say Something" />
          </ds-form>
        </ds-hero>
        <span slot="footer">
          <span>1</span>
          <span>2</span>
          <span>3</span>
        </span>
      </ds-shell>
    );
  }
}

Stencil uses a camel case onFormData attribute to bind the formdata event to a handler.

<ds-form onFormData={(event) => this.handleFormData(event)}>
  <!-- -->
</ds-form>

Conclusions

Building web components allows developers to reuse UI elements in multiple frameworks. Starting with a design system, developers are able to develop a basic app shell in React, Vue, Svelte, and Stencil. The compatibility within frameworks is tracked by Custom Elements Everywhere. The application for each framework had different ways to handle events but each project handled imports, html, and CSS similarly. A properly configured Stencil project can be used to publish a component library that is consumed by developers across multiple frameworks.

Event Bindings

The main difference is how events are bound to handlers in each framework.

Without a framework use addEventListener(). In React use onformdata to listen for formdata events. In Vue use the @formdata shorthand or v-on:formdata to listen for events. In Svelte use on:formdata to declare a handler for formdata events. In Stencil use onFormData in order to handle events published by components.

The post Building a Design System and Consuming it in Multiple Frameworks using Stencil.js appeared first on Big Nerd Ranch.

]]>
https://bignerdranch.com/blog/building-a-design-system-and-consuming-it-in-multiple-frameworks-using-stencil-js/feed/ 0
GraphQL versus REST https://bignerdranch.com/blog/graphql-versus-rest/ Tue, 01 Dec 2020 11:00:05 +0000 https://www.bignerdranch.com/?p=4622 GraphQL and REST paradigms can both be used to create scalable and useful APIs, but GraphQL has some alternative approaches that make a big difference for developers.

The post GraphQL versus REST appeared first on Big Nerd Ranch.

]]>
GraphQL is a new way for developers to think about APIs. GraphQL and REST paradigms can both be used to create scalable and useful APIs, but GraphQL has some alternative approaches that make a big difference for developers. The core components of each API strategy are approached differently. This blog post focuses on three key differences between REST and GraphQL that give GraphQL an edge, especially for developer productivity.

Be sure to check out our post, What is GraphQL? if you’d like a refresher.

No more over/under fetching

Resources are one of the key ideas in REST. For developers using REST, the approach is to identify each resource by a URL. By contrast, developers are generally expected to use a single endpoint for GraphQL APIs. An API using REST to allow access to a book resource may return something like the following when clients send a GET request to the /books/1 path:

{
  "title": "Timeless Laws of Software Development",
  "author": {
    "first_name": "Jerry",
    "last_name": "Fitzpatrick"
  },
  "illustrator": {
    "first_name": "Jennifer",
    "last_name": "Kohnke"
  },
  "id": 1,
  "url": "/books/1",
  "categories": ["software", "testing"]
}

Everything about the above response to the GET request was determined by the API. Any time developers want to fetch details about a book they can generally expect the same type of response using the /books/{id} endpoint.

GraphQL is very different when it comes to how it interprets the idea of resources. In GraphQL, the concept of a resource is generally expressed as a type using the GraphQL schema language. For the book API in the example above, GraphQL would express the book resource as a type:

type Book {
  id: ID
  title: String
  illustrator: Illustrator
  author: Author
  categories: [String]
}

The Book type describes the kind of data available but notably lacks a URL that specifies how to retrieve a particular book by id. While confusing if you are new to GraphQL, the reason for lacking a URL is because GraphQL separates resources from how they are retrieved. In practice, this means backend developers must specify a Query type that exposes the ability to fetch books instead of determining a URL pattern to expose as an endpoint. In addition to the Book type, adding the following replaces the concept of a /books/{id} endpoint in REST:

type Query {
  book(id: ID!): Book
}

To get details about any book resource, any client can request the Book resource and only specific fields they need for a book. Instead of querying via an endpoint, developers would send a GET request to the API that looks something like this:

/graphql?query={ book(id: "1") { title } }

Note that instead of specifying the resource in the URL, as with REST, the GraphQL request for a book resource is in a different format. Developers sending this query to a GraphQL endpoint (e.g. /graphql) would retrieve something like the following:

{
  "title": "Timeless Laws of Software Development"
}

The shape and size of the GraphQL query is determined by the nature of the request. Developers working on clients consuming a GraphQL API get the benefit of being able to decide what data they want from the API. If a developer wanted more data, such as the name of the illustrator of a book, then they would simply request those additional fields from the API.

Typed queries

GraphQL’s schema language allows for the API to specify types for every field associated with an object. In addition to some special types, it also supports specifying custom object types as well. Developers are prone to mistakes when developing clients for APIs, especially when documentation on the shape of an endpoint’s response is lacking. REST has some solutions for mapping endpoints to fluent interfaces such as TypedRest, but GraphQL’s schema language comes with a type system baked in. Tools can parse the types specified for return values from queries to make tooling that catches errors early.

For example, the query that was looked at earlier can be easily understood by developers and tooling:

type Query {
  book(id: ID!): Book
}

Developers new to this API simply need to investigate the Book object type to know what will be returned by the above query. More information can be provided for introspection in a description, and we’ll cover that in a moment.

The baked-in type system has extensive documentation and is also used by developers to create tools that prevent errors during development. Server-side tools such as Apollo Server use the type system for introspection and even allows developers to test and preview queries in a playground. VSCode has a plugin called Graphql for VSCode that provides linting, syntax highlighting, and even schema validation. To take full advantage of the power of GraphQL’s type system and typed queries out of the box all you need to do is reach for a plugin in your IDE.

Self-documenting nature

GraphQL’s schema language can be checked for validity. This allows developers to reference a GraphQL API’s schema as a source of truth. While poor naming of object types and fields is possible, good naming within a GraphQL schema results in APIs that are self-documenting. Naming is probably one of the most important things to get right in a GraphQL project because tools and developers rely on the type system to explain the shape and nature of the data they are requesting.

For REST APIs, Swagger is a good option for producing publishable documentation. However, GraphQL treats documentation as a first-class feature of its specification, meaning developers don’t need to reach for external documentation tools or generators. Up-to-date and accurate descriptions of fields and types can be pulled directly from the GraphQL schema! For example, a schema could be documented using CommonMark in code as follows:

"""
The representation for books available in the catalog
"""
type Book {
  "Unique identifer"
  id: ID
  "The name of the book as listed in the catalog"
  title: String
  "The illustrator listed in the catalog for the book"
  illustrator: Illustrator
  "The author listed in the catalog for the book"
  author: Author
  "The categories this book belongs to on the website"
  categories: [String]
}

"""
Represents an illustrator of a book
"""
type Illustrator {
  first_name: String
  last_name: String
}

"""
Represents an author of a book
"""
type Author {
  first_name: String
  last_name: String
}

type Query {
  """
  Retrieves a book by ID
  """
  book("The unique `ID` of a book" id: ID!): Book
}

The description feature of the GraphQL specification, well-named object types, and descriptive fields ensures the documentation for a GraphQL API is always publishable to tools and future developers.

For queries and mutations, developers can easily see what fields are available. Entire IDEs are being developed just for GraphQL as well that leverages the language’s support for introspection. GraphiQL is one such IDE developed by the GraphQL Foundation.

Now that we’ve looked at REST vs GraphQL, you might be wondering if GraphQL is something that you want to tackle with your next project. Check out Is GraphQL Right for My Project? to get a good idea of the questions you should ask prior to jumping in.

The post GraphQL versus REST appeared first on Big Nerd Ranch.

]]>