Skip to content

Latest commit

 

History

History
319 lines (219 loc) · 11.2 KB

File metadata and controls

319 lines (219 loc) · 11.2 KB

Contributing

Thanks for contributing to the GreenwoodJS website! This document aims to help guide contributions into this project.

Project Structure

The layout of the project is as follows:

  • /assets/ - Public assets like images and fonts used throughout the site
  • /components/ - Custom Element web components to be used throughout the project
  • /layouts/ - Page level layouts for various section of the website
  • /pages/ - File based routing as provided by Greenwood
  • /stories/ - General documentation about the project for developers, hosted in Storybook
  • /styles/ - Global theme and styles

Note

Please review the documentation contained in this project's Storybook by running npm run story:dev and going through the content in the Overview section.

Pull Requests

Generally, it's best to open an issue first before submitting a Pull Request. This is a good opportunity to validate the work first, and ask any questions.

This project uses conventional commits when submitting pull requests in combination with Husky pre-commit hooks that runs linting and formatting scripts. When ready to commit code to submit a PR, run npm run commit and follow the prompts.

Development

Styling

All global theming and general styles should go in src/styles/theme.css, like font family and CSS custom properties to be used throughout the site.

For anything that may not be easily "componentized" or is very general, like for markdown based content, should go in src/styles/main.css.

Note

Open Props are used in this project to provide a set of consistent and re-usable design system tokens. Please review these first before creating any new custom values or variables.

Components

This project leverage Web Components (custom elements) as the mechanism for all templating and / or interactivity, with pre-rendering of the content occurring during the build (SSG).

export default class Greeting extends HTMLElement {
  connectedCallback() {
    // ...
  }
}

// we use app- as the tag name prefix
customElements.define("app-greeting", Greeting);

Static Components (Light DOM)

Since most of the content for this project is static content, Light DOM based HTML is preferred, rendering directly into innerHTML. For styling these components, a Greenwood based implementation of CSS Modules is used, that will link the class names at build time yet still emit static CSS in the <head> of the page.

/* greeting.module.css */
.wrapper {
  color: var(--color-primary);
  box-shadow: var(--shadow-3);
}
import styles from "./greeting.module.css";

export default class Greeting extends HTMLElement {
  connectedCallback() {
    this.innerHTML = `
      <p class="${styles.wrapper}">Hello from the greeting component!</p>
    `;
  }
}

customElements.define("app-greeting", Greeting);

This would emit the following generated HTML

<app-greeting>
  <p class="greeting-1234321-wrapper">Hello from the greeting component!</p>
</app-greeting>

Important

When adding these components to a page, we would want to optimize them as static; data-gwd-opt="static"

<script src="../components/greeting/greeting.js" type="module" data-gwd-opt="static">

Interactive Components (Declarative Shadow DOM)

For interactive components that would require client side interactivity, like event handlers, these components should be authored rendering into a Shadow Root using Declarative Shadow DOM and with Greenwood's raw plugin.

/* card.css */
.card {
  color: var(--color-primary);
  box-shadow: var(--shadow-3);
}
import sheet from "./card.css" with { type: "css" };

export default class Card extends HTMLElement {
  selectItem() {
    // do the thing
  }

  connectedCallback() {
    if (!this.shadowRoot) {
      const thumbnail = this.getAttribute("thumbnail");
      const title = this.getAttribute("title");
      const template = document.createElement("template");

      template.innerHTML = `
        <div class="card">
          <h3>${title}</h3>
          <img src="${thumbnail}" alt="${title}" loading="lazy" width="100%">
          <button>View Item Details</button>
        </div>
      `;

      this.attachShadow({ mode: "open" });
      this.shadowRoot.appendChild(template.content.cloneNode(true));
    }

    this.shadowRoot.adoptedStyleSheets = [sheet];
    this.shadowRoot?.querySelector("button").addEventListener("click", this.selectItem.bind(this));
  }
}

customElements.define("app-card", Card);

This would emit the following generated HTML

<app-card title="My Title" thumbnail="/image.png">
  <template shadowrootmode="open">
    <div>
      <h3>My Title</h3>
      <img src="/image.png" alt="My Title" loading="lazy" width="100%" />
      <button>View Item Details</button>
    </div>
  </template>
</app-card>

Tip

If the component does not need client side JavaScript, use a Light DOM component. If it will need client side JavaScript, use a Shadow DOM component.

Testing

For each component, a testing file ("spec") should be included to test basic functionality, living alongside the component in its directory.

components/
  my-component/
    my-component.js
    my-component.spec.js

Spec File Setup

Each component will require a DOM-like environment in order to be queried for the custom element and, thus assert on the component details.

The following is an example of a spec file, with imports and a set of describe/it blocks. In this case, we import the add method from the math.js file, and assert that it provides an expected result.

import { expect } from "@esm-bundle/chai";
import { add } from "./math.js";

describe("Add Function", () => {
  it("should do something expected", () => {
    expect(add(1, 2)).equal(3);
  });
});

Testing the Component

With the spec file foundation, the custom element can now be created and applied to the mock DOM environment. To do this, we use the Web Test Runner (WTR) framework which provides the scaffolding to test Web Componets in a browser environment.

The WTR and the full setup is covered in greater detail within the GreenwoodJS test runner docs. In an attempt to not duplicate those docs, an outline has been provided here, but please refer to those docs for a full user guide.

  1. Setup

    Install dependencies and configure WTR. This project has that covered already! ✅ (Let's chat if there becomes a need to reconfigure the current setup.)

  2. Usage

    Within a traditional before block, create the custom element, and append to the DOM. See the example below. 👇

  3. Static Assets

    If experiencing a 404 for a missing asset, a simple middleware can help mock that request. Otherwise, you can skip this section.

  4. Install Resource Plugins

    If using a Greenwood resource plugin, you'll need to provide that info to stub out the signiture.

  5. Mock Data Requests

    If using one of Greenwood's Content as Data Client APIs, mocking fetch with mock data is necessary.

Examples

With the test setup complete, we can now create the block necessary to put the custom element under test.

Populating the previous example, we include the before block and create, customtize, and append the element using the Browser API.

import { expect } from "@esm-bundle/chai";
import "./my-custom-element.js";

// ...mocks truncated....

describe("Components/My Custom Element", () => {
  let myElement;

  before(async () => {
    // create.
    myElement = document.createElement("app-my-element"); // the tagname as it appears in the browser

    // customize.
    myElement.addAttribute("route", "/some/path/to/a/page");

    // append.
    document.body.appendChild(myElement);

    // all ready.
    await myElement.updateComplete;
  });

  after(() => {
    // cleanup.
    myElement.remove();
    myElement = null;
  });

  it("should do something expected", () => {
    expect("something").equal("some" + "thing");
  });

  // ...it()...
});

Storybook

For each component, a Storybook file should be included to demonstrate basic functionality, living alongside the component in its directory. Generally a default story should be sufficient.

components/
  my-component/
    my-component.js
    my-component.stories.js

Below is an example of a basic Storybook file:

import "./header.js";

const meta = {
  title: "Components/Header",
};

export default meta;

export const Primary = () => "<app-header></app-header>";

Content as Data

When a component implements one of Greenwood's Content as Data Client APIs, the story will need to mock this request before being able to render within the Storybook.

See the Greenwood Storybook docs for more information and the blog-posts-list.stories.js story for an example in this project.

Hosting and Deployment

This project is hosted on Netlify and automatically deploys on each merge into main. Release branches will be curated over the course of a Greenwood release cycle and then merged at the time the new Greenwood release is published to NPM.

For links to /discord/, a redirect is configured in netlify.toml to redirect these requests to the project's Discord server.

Pull Requests

To best help facilitate contributions to the project, we have Conventional Commits configured for the project to walk you through preparing commits in the format of <type>: #<issue> <summary-of-change>, e.g. bug: #123 fixed bug with the thing.

Make sure you have run npm run lint, npm run format and npm run test to prepare your commit. Then, after staging your files with git add, you can initiate the commit "wizard" by running:

$ npm run commit

The following will be required:

  • type
  • issue reference (can technically be empty)

Continuous Integration

To test the CI build scripts locally, run the yarn commands mentioned in the Workflows section of the README. (basically just make sure linting, formatting, and test tasks are all passing).

Link Checker

There is a npm script that you can run that will check all relative links and hashes (except for blog pages) to check that links aren't broken. Running the command will build the site for production automatically and generate a report.

$ npm run lint:links
#...

✅ all links checked successfully and no broken links found