ardi

0.3.20 • Public • Published

Ardi

Welcome to the Weightless Web

Ardi makes it almost too easy to create reactive custom elements that work with any site or framework.

Check out the demos!

Features

  1. Object-oriented API
  2. Single-file components
  3. Reactive props and state
  4. Easy-to-use Context API
  5. Templates in µhtml, JSX, or Handlebars
  6. Helpful lifecycle callbacks
  7. No building, compiling, or tooling

Installation

You can use Ardi from NPM or a CDN.

NPM

npm i ardi
import ardi, { html } from 'ardi'

ardi({ tag: 'my-component' })

CDN

<script type="module">
  import ardi, { html } from '//unpkg.com/ardi'

  ardi({ tag: 'my-component' })
</script>

API

Ardi uses a straightforward object-oriented API. To demonstrate the API, we'll be looking at simplified code from the podcast demo.

Podcast Component with 5 episodes of The Daily by The New York Times loaded.

Tag

Define the component's tag. The tag must follow the custom element naming convention. We'll call this component 'podcast-embed'.

ardi({
  tag: 'podcast-embed',
})

Extends

If you are building a component that extends a default element, you can define the prototype and tag here. Note that Safari still does not support extending built-in elements 😭.

ardi({
  extends: [HTMLAnchorElement, 'a'],
})

Shadow

Ardi renders to the Shadow DOM by default. You can disable this behavior if you need to.

ardi({
  shadow: false,
})

Props

Props allow you to configure a component using the element's attributes. To create a property, add a key under props whose value is an array containing a handler function and (optionally) a default value. The handler takes the string value from the prop's attribute and transforms it, i.e. from a string '4' to a number 4. The handler can be a built-in function (like String, Number, or JSON.parse) or an arrow function. Every prop is reactive, which means that whether a prop's value is set internally or via its attribute, the change will trigger a render. Prop values are accessible directly from this, i.e. this.pagesize.

Here are the props configured in the podcast demo.

ardi({
  props: {
    feed: [String, 'https://feeds.simplecast.com/54nAGcIl'],
    pagesize: [Number, 10],
    pagelabel: [String, 'Page'],
    prevpagelabel: [String, 'Prevous Page'],
    nextpagelabel: [String, 'Next Page'],
    pauselabel: [String, 'pause'],
    playlabel: [String, 'play'],
  },
})

State

State is a reactive container for data, which means any change will trigger a render. Values declared in state are accessible from this, i.e. this.episodes.

Here is how state is defined in the podcast demo.

ardi({
  state: () => ({
    feedJSON: {},
    nowPlaying: null,
    page: 0,
    paused: true,
  }),
})

Template

μhtml is the default template library, and it's just like JSX except you create your templates using tagged template literals. μhtml is extremely efficient. When the component's state changes, instead of re-rendering an entire element, μhtml makes tiny, surgical DOM updates as-needed.

Event Handlers

Event handlers can be applied to an element using React's on syntax (onClick) or Vue's @ syntax (@click). Here is a snippet showing the play/pause button for an episode in the podcast demo.

ardi({
  template() {
    return html`
      ...
      <button
        part="play-button"
        @click=${() => this.playPause(track)}
        aria-label=${this.nowPlaying === track && !this.paused
          ? this.pauselabel
          : this.playlabel}
      >
        ${this.icon(
          this.nowPlaying === track && !this.paused ? 'pause' : 'play'
        )}
      </button>
      ...
    `
  },
})

Lists

Lists are handled using the Array.map() method. In the podcast demo, we will use a map to list the episodes that are returned by the xml feed. Lists generally do not require a key, but in cases where the order of elements changes you can add a key using html.for(key).

ardi({
  template() {
    return html`
      ...
      <div part="episodes">
        ${this.episodes.map((episode) => {
          return html`<div part="episode">...</div>`
        })}
      </div>
      ...
    `
  },
})

Conditional Rendering

Ternary operators are the recommended way to handle conditional rendering. The snippet below shows how elements can be conditionally rendered based on the available data.

ardi({
  template() {
    return html`
      ...
      <audio ref="player" src=${this.nowPlaying} />
      <div part="header">
        ${image ? html`<img part="image" src=${image} />` : ''}
        <div part="header-wrapper">
          ${title ? html`<p part="title">${title}</p>` : ''}
          ${author ? html`<p part="author">${author}</p>` : ''}
          ${link ? html`<a part="link" href=${link}>${link}</a>` : ''}
        </div>
      </div>
      ${description ? html`<p part="description">${description}</p>` : ''}
      ...
    `
  },
})

If you prefer a more HTML-like syntax, Ardi provides a <if-else> element that you can use instead. To use it, just assign the if prop with a condition and nest your element inside. If you want to provide a fallback element, you can assign it to the else slot and it will be displayed if the condition is falsey. You can see this in action in the Employee Card component.

ardi({
  template() {
    return html`
      <if-else if=${image}>
        <img part="image" src=${image} />
        <svg slot="else" viewBox="0 0 24 24">
          <path d="..." />
        </svg>
      </if-else>
    `
  },
})

Slots

Ardi components use the Shadow DOM by default, which means you can use <slot> tags to project nested elements into your templates. You can use a single default slot or multiple named slots.

The podcast demo has two named slots allowing the pagination button icons to be customized.

ardi({
  template() {
    return html`
      ...
      <button
        part="pagination-prev"
        @click=${() => this.page--}
        disabled=${this.page > 0 ? null : true}
        aria-label=${this.prevpagelabel}
      >
        <slot name="prev-icon"> ${this.icon('leftArrow')} </slot>
      </button>
      ...
    `
  },
})

Refs

Ardi allows you to add ref attributes to elements in your template, which are accessible from this.refs.

In the podcast component, the player ref is used by the togglePlayback method to control playback.

ardi({
  template() {
    return html`<audio ref="player" src=${this.nowPlaying} />...`
  },
  togglePlayback() {
    // ...
    this.refs.player.play()
    // ...
  },
})

Methods

You can add any number of methods in your component and access them via this. Custom methods can be used in your template, in lifecycle callbacks, or inside of other methods. For examples, you can view the complete code for the podcast demo. There are many more examples in components listed on the demos page.

Context

Ardi has a powerful and easy to use context api, allowing one component to share and synchronize its props or state with multiple child components. You can see this API in action in the i18n demo, this CodePen example, and in the CSS section below.

To share context from a parent component, add the context attribute with a descriptive name, i.e. context="theme" You can then use this.context("theme") to reference the element and access its props or state. When a child component uses the context to make changes to the parent element's props or state, the parent element will notify every other child component that accesses the same values, keeping the context synchronized throughout the application.

<ardi-component context="theme"></ardi-component>

Styles

Ardi components use the Shadow DOM by default. Elements in the Shadow DOM can access CSS variables declared on the page. Elements can also be styled using part attributes.

Inline CSS

You can use Javascript in an inline style attribute.

ardi({
  template() {
    const { bg, color } = this.context('theme')
    return html`<nav style=${`background: ${bg}; color: ${color};`}>...</nav>`
  },
})

Styles Key

If you have a lot of CSS, it's cleaner to create a styles key. Ardi provides a css helper function to facilitate working with VSCode and other IDEs that support tagged template literals.

import ardi, { css, html } from '//unpkg.com/ardi'

ardi({
  template() {
    const { bg, color } = this.context('theme')
    return html`<nav style=${`--bg: ${bg}; --color: ${color};`}>...</nav>`
  },
  styles: css`
    nav {
      background: var(--bg);
      color: var(--color);
    }
  `,
})

Styles Function

If you prefer, you can also use Javascript variables and functions directly in your CSS by creating the styles key as a function.

ardi({
  template() {
    return html`<nav>...</nav>`
  },
  styles() {
    const { bg, color } = this.context('theme')
    return `
      nav {
        background: ${bg};
        color: ${color};
      }
    `
  },
})

CSS Pre-Processors

Ardi is a runtime framework, designed to work with any app, site, or platform. Since Sass and Less are build-time languages, no official support is provided. If you want to write styles in sass and use them in your components, you can always compile to native CSS and @import the file using Ardi's styles key.

Many Sass features are redundant when using the Shadow DOM. Complex BEM selectors requiring & nesting are unnecessary because styles are scoped to the component, and CSS has native support for variables. Nesting is even coming soon. That being said, if cannot live without using SASS (or another pre-processor) for prototyping, here is a demo showing how you can.

Lifecycle

Ardi has several lifecycle callbacks, providing a convenient way to fetch data or apply effects.

created()

This callback runs as soon as the component is initialized. This is a good place to load data, setup observers, etc.

A great example of this is in the forecast demo, where a resize observer is created to apply styles based on the component's rendered width (regardless of the viewport width).

ardi({
  tag: 'ardi-forecast',
  created() {
    new ResizeObserver(() =>
      requestAnimationFrame(
        () => (this.small = this.clientWidth <= this.breakpoint)
      )
    ).observe(this)
  },
})

ready()

This callback runs as soon as the component's template is rendered, allowing you to call methods that access refs defined in the template.

rendered()

This method runs each time the component renders an update. This was added to support event listeners when writing templates with Handlebars or untagged template literals, but you can use this method for any purpose.

changed()

Although props are reactive, meaning the template is automatically updated when a prop's value changes, you may encounter scenarios where you need to handle a property's value manually, i.e. to fetch data or apply an effect. You can use this callback to observe and respond to prop updates.

Here is an example from the forecast demo.

ardi({
  tag: 'ardi-forecast',
  changed(prop) {
    if (
      prop.old &&
      prop.new &&
      ['lat', 'lon', 'locale', 'unit'].includes(prop.name)
    ) {
      this.fetchForecast()
    }
  },
})

intersected()

This method is called when the component is scrolled into view. You can use the ratio parameter to determine how much of the component should be visible before you apply an effect. Ardi will only create the intersection observer if you include this method, so omit it if you do not intend to use it.

In the forecast demo, the intersect method is used to lazy-load data once the component is scrolled into view. This trick can save a lot of money if you use paid APIs!

ardi({
  tag: 'ardi-forecast',
  intersected(r) {
    if (!this.gotWeather && r > 0.2) {
      this.fetchForecast()
    }
  },
})

Template Options

μhtml is tiny, fast and efficient, and we strongly recommend it. However, JSX is king right now, and Handlebars is still holding on strong. That's why Ardi allows you to use whatever template library you prefer. Sample code for each supported option is provided below, for comparison. There is also an interactive CodePen demo showing all three examples.

μhtml

import ardi, { html } from '//unpkg.com/ardi'

ardi({
  tag: 'uhtml-counter',
  state: () => ({ count: 0 }),
  template() {
    return html`
    <button @click=${() => this.count++}>
      Count: ${this.count}
    </button>`
  },
})

JSX-Dom

import ardi, { html } from '//unpkg.com/ardi'
import React from '//cdn.skypack.dev/jsx-dom'

ardi({
  tag: 'jsx-counter',
  state: () => ({ count: 0 }),
  template() {
    return (
      <button onClick={() => this.count++}>
        Count: {this.count}
      </button>
    )
  },
})

Handlebars

With Handlebars (or any template that returns a simple string: i.e. an untagged template literal), event listeners can be added to the rendered method. If present, the rendered method will run after each render.

import ardi, { html } from '//unpkg.com/ardi'
import handlebars from 'https://cdn.skypack.dev/handlebars@4.7.7'

ardi({
  tag: 'hbs-counter',
  state: () => ({ count: 0 }),
  template() {
    return handlebars.compile(
      `<button ref='counter'>Count: {{count}}</button>`
    )(this)
  },
  rendered() {
    this.refs.counter.addEventListener('click', () => this.count++)
  },
})

Package Sidebar

Install

npm i ardi

Weekly Downloads

212

Version

0.3.20

License

ISC

Unpacked Size

6.06 MB

Total Files

130

Last publish

Collaborators

  • jameslovallo