custom elements – CSS-Tricks https://css-tricks.com Tips, Tricks, and Techniques on using Cascading Style Sheets. Mon, 13 Mar 2023 13:24:00 +0000 en-US hourly 1 https://wordpress.org/?v=6.2.2 https://i0.wp.com/css-tricks.com/wp-content/uploads/2021/07/star.png?fit=32%2C32&ssl=1 custom elements – CSS-Tricks https://css-tricks.com 32 32 45537868 Making Calendars With Accessibility and Internationalization in Mind https://css-tricks.com/making-calendars-with-accessibility-and-internationalization-in-mind/ https://css-tricks.com/making-calendars-with-accessibility-and-internationalization-in-mind/#comments Mon, 13 Mar 2023 13:23:52 +0000 https://css-tricks.com/?p=376950 Doing a quick search here on CSS-Tricks shows just how many different ways there are to approach calendars. Some show how CSS Grid can create the layout efficiently. Some attempt to bring actual data into the mix. Some …


Making Calendars With Accessibility and Internationalization in Mind originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Doing a quick search here on CSS-Tricks shows just how many different ways there are to approach calendars. Some show how CSS Grid can create the layout efficiently. Some attempt to bring actual data into the mix. Some rely on a framework to help with state management.

There are many considerations when building a calendar component — far more than what is covered in the articles I linked up. If you think about it, calendars are fraught with nuance, from handling timezones and date formats to localization and even making sure dates flow from one month to the next… and that’s before we even get into accessibility and additional layout considerations depending on where the calendar is displayed and whatnot.

Many developers fear the Date() object and stick with older libraries like moment.js. But while there are many “gotchas” when it comes to dates and formatting, JavaScript has a lot of cool APIs and stuff to help out!

January 2023 calendar grid.

I don’t want to re-create the wheel here, but I will show you how we can get a dang good calendar with vanilla JavaScript. We’ll look into accessibility, using semantic markup and screenreader-friendly <time> -tags — as well as internationalization and formatting, using the Intl.Locale, Intl.DateTimeFormat and Intl.NumberFormat-APIs.

In other words, we’re making a calendar… only without the extra dependencies you might typically see used in a tutorial like this, and with some of the nuances you might not typically see. And, in the process, I hope you’ll gain a new appreciation for newer things that JavaScript can do while getting an idea of the sorts of things that cross my mind when I’m putting something like this together.

First off, naming

What should we call our calendar component? In my native language, it would be called “kalender element”, so let’s use that and shorten that to “Kal-El” — also known as Superman’s name on the planet Krypton.

Let’s create a function to get things going:

function kalEl(settings = {}) { ... }

This method will render a single month. Later we’ll call this method from [...Array(12).keys()] to render an entire year.

Initial data and internationalization

One of the common things a typical online calendar does is highlight the current date. So let’s create a reference for that:

const today = new Date();

Next, we’ll create a “configuration object” that we’ll merge with the optional settings object of the primary method:

const config = Object.assign(
  {
    locale: (document.documentElement.getAttribute('lang') || 'en-US'), 
    today: { 
      day: today.getDate(),
      month: today.getMonth(),
      year: today.getFullYear() 
    } 
  }, settings
);

We check, if the root element (<html>) contains a lang-attribute with locale info; otherwise, we’ll fallback to using en-US. This is the first step toward internationalizing the calendar.

We also need to determine which month to initially display when the calendar is rendered. That’s why we extended the config object with the primary date. This way, if no date is provided in the settings object, we’ll use the today reference instead:

const date = config.date ? new Date(config.date) : today;

We need a little more info to properly format the calendar based on locale. For example, we might not know whether the first day of the week is Sunday or Monday, depending on the locale. If we have the info, great! But if not, we’ll update it using the Intl.Locale API. The API has a weekInfo object that returns a firstDay property that gives us exactly what we’re looking for without any hassle. We can also get which days of the week are assigned to the weekend:

if (!config.info) config.info = new Intl.Locale(config.locale).weekInfo || { 
  firstDay: 7,
  weekend: [6, 7] 
};

Again, we create fallbacks. The “first day” of the week for en-US is Sunday, so it defaults to a value of 7. This is a little confusing, as the getDay method in JavaScript returns the days as [0-6], where 0 is Sunday… don’t ask me why. The weekends are Saturday and Sunday, hence [6, 7].

Before we had the Intl.Locale API and its weekInfo method, it was pretty hard to create an international calendar without many **objects and arrays with information about each locale or region. Nowadays, it’s easy-peasy. If we pass in en-GB, the method returns:

// en-GB
{
  firstDay: 1,
  weekend: [6, 7],
  minimalDays: 4
}

In a country like Brunei (ms-BN), the weekend is Friday and Sunday:

// ms-BN
{
  firstDay: 7,
  weekend: [5, 7],
  minimalDays: 1
}

You might wonder what that minimalDays property is. That’s the fewest days required in the first week of a month to be counted as a full week. In some regions, it might be just one day. For others, it might be a full seven days.

Next, we’ll create a render method within our kalEl-method:

const render = (date, locale) => { ... }

We still need some more data to work with before we render anything:

const month = date.getMonth();
const year = date.getFullYear();
const numOfDays = new Date(year, month + 1, 0).getDate();
const renderToday = (year === config.today.year) && (month === config.today.month);

The last one is a Boolean that checks whether today exists in the month we’re about to render.

Semantic markup

We’re going to get deeper in rendering in just a moment. But first, I want to make sure that the details we set up have semantic HTML tags associated with them. Setting that up right out of the box gives us accessibility benefits from the start.

Calendar wrapper

First, we have the non-semantic wrapper: <kal-el>. That’s fine because there isn’t a semantic <calendar> tag or anything like that. If we weren’t making a custom element, <article> might be the most appropriate element since the calendar could stand on its own page.

Month names

The <time> element is going to be a big one for us because it helps translate dates into a format that screenreaders and search engines can parse more accurately and consistently. For example, here’s how we can convey “January 2023” in our markup:

<time datetime="2023-01">January <i>2023</i></time>

Day names

The row above the calendar’s dates containing the names of the days of the week can be tricky. It’s ideal if we can write out the full names for each day — e.g. Sunday, Monday, Tuesday, etc. — but that can take up a lot of space. So, let’s abbreviate the names for now inside of an <ol> where each day is a <li>:

<ol>
  <li><abbr title="Sunday">Sun</abbr></li>
  <li><abbr title="Monday">Mon</abbr></li>
  <!-- etc. -->
</ol>

We could get tricky with CSS to get the best of both worlds. For example, if we modified the markup a bit like this:

<ol>
  <li>
    <abbr title="S">Sunday</abbr>
  </li>
</ol>

…we get the full names by default. We can then “hide” the full name when space runs out and display the title attribute instead:

@media all and (max-width: 800px) {
  li abbr::after {
    content: attr(title);
  }
}

But, we’re not going that way because the Intl.DateTimeFormat API can help here as well. We’ll get to that in the next section when we cover rendering.

Day numbers

Each date in the calendar grid gets a number. Each number is a list item (<li>) in an ordered list (<ol>), and the inline <time> tag wraps the actual number.

<li>
  <time datetime="2023-01-01">1</time>
</li>

And while I’m not planning to do any styling just yet, I know I will want some way to style the date numbers. That’s possible as-is, but I also want to be able to style weekday numbers differently than weekend numbers if I need to. So, I’m going to include data-* attributes specifically for that: data-weekend and data-today.

Week numbers

There are 52 weeks in a year, sometimes 53. While it’s not super common, it can be nice to display the number for a given week in the calendar for additional context. I like having it now, even if I don’t wind up not using it. But we’ll totally use it in this tutorial.

We’ll use a data-weeknumber attribute as a styling hook and include it in the markup for each date that is the week’s first date.

<li data-day="7" data-weeknumber="1" data-weekend="">
  <time datetime="2023-01-08">8</time>
</li>

Rendering

Let’s get the calendar on a page! We already know that <kal-el> is the name of our custom element. First thing we need to configure it is to set the firstDay property on it, so the calendar knows whether Sunday or some other day is the first day of the week.

<kal-el data-firstday="${ config.info.firstDay }">

We’ll be using template literals to render the markup. To format the dates for an international audience, we’ll use the Intl.DateTimeFormat API, again using the locale we specified earlier.

The month and year

When we call the month, we can set whether we want to use the long name (e.g. February) or the short name (e.g. Feb.). Let’s use the long name since it’s the title above the calendar:

<time datetime="${year}-${(pad(month))}">
  ${new Intl.DateTimeFormat(
    locale,
    { month:'long'}).format(date)} <i>${year}</i>
</time>

Weekday names

For weekdays displayed above the grid of dates, we need both the long (e.g. “Sunday”) and short (abbreviated, ie. “Sun”) names. This way, we can use the “short” name when the calendar is short on space:

Intl.DateTimeFormat([locale], { weekday: 'long' })
Intl.DateTimeFormat([locale], { weekday: 'short' })

Let’s make a small helper method that makes it a little easier to call each one:

const weekdays = (firstDay, locale) => {
  const date = new Date(0);
  const arr = [...Array(7).keys()].map(i => {
    date.setDate(5 + i)
    return {
      long: new Intl.DateTimeFormat([locale], { weekday: 'long'}).format(date),
      short: new Intl.DateTimeFormat([locale], { weekday: 'short'}).format(date)
    }
  })
  for (let i = 0; i < 8 - firstDay; i++) arr.splice(0, 0, arr.pop());
  return arr;
}

Here’s how we invoke that in the template:

<ol>
  ${weekdays(config.info.firstDay,locale).map(name => `
    <li>
      <abbr title="${name.long}">${name.short}</abbr>
    </li>`).join('')
  }
</ol>

Day numbers

And finally, the days, wrapped in an <ol> element:

${[...Array(numOfDays).keys()].map(i => {
  const cur = new Date(year, month, i + 1);
  let day = cur.getDay(); if (day === 0) day = 7;
  const today = renderToday && (config.today.day === i + 1) ? ' data-today':'';
  return `
    <li data-day="${day}"${today}${i === 0 || day === config.info.firstDay ? ` data-weeknumber="${new Intl.NumberFormat(locale).format(getWeek(cur))}"`:''}${config.info.weekend.includes(day) ? ` data-weekend`:''}>
      <time datetime="${year}-${(pad(month))}-${pad(i)}" tabindex="0">
        ${new Intl.NumberFormat(locale).format(i + 1)}
      </time>
    </li>`
}).join('')}

Let’s break that down:

  1. We create a “dummy” array, based on the “number of days” variable, which we’ll use to iterate.
  2. We create a day variable for the current day in the iteration.
  3. We fix the discrepancy between the Intl.Locale API and getDay().
  4. If the day is equal to today, we add a data-* attribute.
  5. Finally, we return the <li> element as a string with merged data.
  6. tabindex="0" makes the element focusable, when using keyboard navigation, after any positive tabindex values (Note: you should never add positive tabindex-values)

To “pad” the numbers in the datetime attribute, we use a little helper method:

const pad = (val) => (val + 1).toString().padStart(2, '0');

Week number

Again, the “week number” is where a week falls in a 52-week calendar. We use a little helper method for that as well:

function getWeek(cur) {
  const date = new Date(cur.getTime());
  date.setHours(0, 0, 0, 0);
  date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7);
  const week = new Date(date.getFullYear(), 0, 4);
  return 1 + Math.round(((date.getTime() - week.getTime()) / 86400000 - 3 + (week.getDay() + 6) % 7) / 7);
}

I didn’t write this getWeek-method. It’s a cleaned up version of this script.

And that’s it! Thanks to the Intl.Locale, Intl.DateTimeFormat and Intl.NumberFormat APIs, we can now simply change the lang-attribute of the <html> element to change the context of the calendar based on the current region:

January 2023 calendar grid.
de-DE
January 2023 calendar grid.
fa-IR
January 2023 calendar grid.
zh-Hans-CN-u-nu-hanidec

Styling the calendar

You might recall how all the days are just one <ol> with list items. To style these into a readable calendar, we dive into the wonderful world of CSS Grid. In fact, we can repurpose the same grid from a starter calendar template right here on CSS-Tricks, but updated a smidge with the :is() relational pseudo to optimize the code.

Notice that I’m defining configurable CSS variables along the way (and prefixing them with ---kalel- to avoid conflicts).

kal-el :is(ol, ul) {
  display: grid;
  font-size: var(--kalel-fz, small);
  grid-row-gap: var(--kalel-row-gap, .33em);
  grid-template-columns: var(--kalel-gtc, repeat(7, 1fr));
  list-style: none;
  margin: unset;
  padding: unset;
  position: relative;
}
Seven-column calendar grid with grid lines shown.

Let’s draw borders around the date numbers to help separate them visually:

kal-el :is(ol, ul) li {
  border-color: var(--kalel-li-bdc, hsl(0, 0%, 80%));
  border-style: var(--kalel-li-bds, solid);
  border-width: var(--kalel-li-bdw, 0 0 1px 0);
  grid-column: var(--kalel-li-gc, initial);
  text-align: var(--kalel-li-tal, end); 
}

The seven-column grid works fine when the first day of the month is also the first day of the week for the selected locale). But that’s the exception rather than the rule. Most times, we’ll need to shift the first day of the month to a different weekday.

Showing the first day of the month falling on a Thursday.

Remember all the extra data-* attributes we defined when writing our markup? We can hook into those to update which grid column (--kalel-li-gc) the first date number of the month is placed on:

[data-firstday="1"] [data-day="3"]:first-child {
  --kalel-li-gc: 1 / 4;
}

In this case, we’re spanning from the first grid column to the fourth grid column — which will automatically “push” the next item (Day 2) to the fifth grid column, and so forth.

Let’s add a little style to the “current” date, so it stands out. These are just my styles. You can totally do what you’d like here.

[data-today] {
  --kalel-day-bdrs: 50%;
  --kalel-day-bg: hsl(0, 86%, 40%);
  --kalel-day-hover-bgc: hsl(0, 86%, 70%);
  --kalel-day-c: #fff;
}

I like the idea of styling the date numbers for weekends differently than weekdays. I’m going to use a reddish color to style those. Note that we can reach for the :not() pseudo-class to select them while leaving the current date alone:

[data-weekend]:not([data-today]) { 
  --kalel-day-c: var(--kalel-weekend-c, hsl(0, 86%, 46%));
}

Oh, and let’s not forget the week numbers that go before the first date number of each week. We used a data-weeknumber attribute in the markup for that, but the numbers won’t actually display unless we reveal them with CSS, which we can do on the ::before pseudo-element:

[data-weeknumber]::before {
  display: var(--kalel-weeknumber-d, inline-block);
  content: attr(data-weeknumber);
  position: absolute;
  inset-inline-start: 0;
  /* additional styles */
}

We’re technically done at this point! We can render a calendar grid that shows the dates for the current month, complete with considerations for localizing the data by locale, and ensuring that the calendar uses proper semantics. And all we used was vanilla JavaScript and CSS!

But let’s take this one more step

Rendering an entire year

Maybe you need to display a full year of dates! So, rather than render the current month, you might want to display all of the month grids for the current year.

Well, the nice thing about the approach we’re using is that we can call the render method as many times as we want and merely change the integer that identifies the month on each instance. Let’s call it 12 times based on the current year.

as simple as calling the render-method 12 times, and just change the integer for monthi:

[...Array(12).keys()].map(i =>
  render(
    new Date(date.getFullYear(),
    i,
    date.getDate()),
    config.locale,
    date.getMonth()
  )
).join('')

It’s probably a good idea to create a new parent wrapper for the rendered year. Each calendar grid is a <kal-el> element. Let’s call the new parent wrapper <jor-el>, where Jor-El is the name of Kal-El’s father.

<jor-el id="app" data-year="true">
  <kal-el data-firstday="7">
    <!-- etc. -->
  </kal-el>

  <!-- other months -->
</jor-el>

We can use <jor-el> to create a grid for our grids. So meta!

jor-el {
  background: var(--jorel-bg, none);
  display: var(--jorel-d, grid);
  gap: var(--jorel-gap, 2.5rem);
  grid-template-columns: var(--jorel-gtc, repeat(auto-fill, minmax(320px, 1fr)));
  padding: var(--jorel-p, 0);
}

Final demo

Bonus: Confetti Calendar

I read an excellent book called Making and Breaking the Grid the other day and stumbled on this beautiful “New Year’s poster”:

Source: Making and Breaking the Grid (2nd Edition) by Timothy Samara

I figured we could do something similar without changing anything in the HTML or JavaScript. I’ve taken the liberty to include full names for months, and numbers instead of day names, to make it more readable. Enjoy!


Making Calendars With Accessibility and Internationalization in Mind originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/making-calendars-with-accessibility-and-internationalization-in-mind/feed/ 1 376950
An Approach to Lazy Loading Custom Elements https://css-tricks.com/an-approach-to-lazy-loading-custom-elements/ https://css-tricks.com/an-approach-to-lazy-loading-custom-elements/#comments Mon, 13 Feb 2023 15:10:41 +0000 https://css-tricks.com/?p=376991 We’re fans of Custom Elements around here. Their design makes them particularly amenable to lazy loading, which can be a boon for performance.

Inspired by a colleague’s experiments, I recently set about writing a simple auto-loader: Whenever a custom …


An Approach to Lazy Loading Custom Elements originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
We’re fans of Custom Elements around here. Their design makes them particularly amenable to lazy loading, which can be a boon for performance.

Inspired by a colleague’s experiments, I recently set about writing a simple auto-loader: Whenever a custom element appears in the DOM, we wanna load the corresponding implementation if it’s not available yet. The browser then takes care of upgrading such elements from there on out.

Chances are you won’t actually need all this; there’s usually a simpler approach. Used deliberately, the techniques shown here might still be a useful addition to your toolset.

For consistency, we want our auto-loader to be a custom element as well — which also means we can easily configure it via HTML. But first, let’s identify those unresolved custom elements, step by step:

class AutoLoader extends HTMLElement {
  connectedCallback() {
    let scope = this.parentNode;
    this.discover(scope);
  }
}
customElements.define("ce-autoloader", AutoLoader);

Assuming we’ve loaded this module up-front (using async is ideal), we can drop a <ce-autoloader> element into the <body> of our document. That will immediately start the discovery process for all child elements of <body>, which now constitutes our root element. We could limit discovery to a subtree of our document by adding <ce-autoloader> to the respective container element instead — indeed, we might even have multiple instances for different subtrees.

Of course, we still have to implement that discover method (as part of the AutoLoader class above):

discover(scope) {
  let candidates = [scope, ...scope.querySelectorAll("*")];
  for(let el of candidates) {
    let tag = el.localName;
    if(tag.includes("-") && !customElements.get(tag)) {
      this.load(tag);
    }
  }
}

Here we check our root element along with every single descendant (*). If it’s a custom element — as indicated by hyphenated tags — but not yet upgraded, we’ll attempt to load the corresponding definition. Querying the DOM that way might be expensive, so we should be a little careful. We can alleviate load on the main thread by deferring this work:

connectedCallback() {
  let scope = this.parentNode;
  requestIdleCallback(() => {
    this.discover(scope);
  });
}

requestIdleCallback is not universally supported yet, but we can use requestAnimationFrame as a fallback:

let defer = window.requestIdleCallback || requestAnimationFrame;

class AutoLoader extends HTMLElement {
  connectedCallback() {
    let scope = this.parentNode;
    defer(() => {
      this.discover(scope);
    });
  }
  // ...
}

Now we can move on to implementing the missing load method to dynamically inject a <script> element:

load(tag) {
  let el = document.createElement("script");
  let res = new Promise((resolve, reject) => {
    el.addEventListener("load", ev => {
      resolve(null);
    });
    el.addEventListener("error", ev => {
      reject(new Error("failed to locate custom-element definition"));
    });
  });
  el.src = this.elementURL(tag);
  document.head.appendChild(el);
  return res;
}

elementURL(tag) {
  return `${this.rootDir}/${tag}.js`;
}

Note the hard-coded convention in elementURL. The src attribute’s URL assumes there’s a directory where all custom element definitions reside (e.g. <my-widget>/components/my-widget.js). We could come up with more elaborate strategies, but this is good enough for our purposes. Relegating this URL to a separate method allows for project-specific subclassing when needed:

class FancyLoader extends AutoLoader {
  elementURL(tag) {
    // fancy logic
  }
}

Either way, note that we’re relying on this.rootDir. This is where the aforementioned configurability comes in. Let’s add a corresponding getter:

get rootDir() {
  let uri = this.getAttribute("root-dir");
  if(!uri) {
    throw new Error("cannot auto-load custom elements: missing `root-dir`");
  }
  if(uri.endsWith("/")) { // remove trailing slash
    return uri.substring(0, uri.length - 1);
  }
  return uri;
}

You might be thinking of observedAttributes now, but that doesn’t really make things easier. Plus updating root-dir at runtime seems like something we’re never going to need.

Now we can — and must — configure our elements directory: <ce-autoloader root-dir="/components">.

With this, our auto-loader can do its job. Except it only works once, for elements that already exist when the auto-loader is initialized. We’ll probably want to account for dynamically added elements as well. That’s where MutationObserver comes into play:

connectedCallback() {
  let scope = this.parentNode;
  defer(() => {
    this.discover(scope);
  });
  let observer = this._observer = new MutationObserver(mutations => {
    for(let { addedNodes } of mutations) {
      for(let node of addedNodes) {
        defer(() => {
          this.discover(node);
        });
      }
    }
  });
  observer.observe(scope, { subtree: true, childList: true });
}

disconnectedCallback() {
  this._observer.disconnect();
}

This way, the browser notifies us whenever a new element appears in the DOM — or rather, our respective subtree — which we then use to restart the discovery process. (You might argue we’re re-inventing custom elements here, and you’d be kind of correct.)

Our auto-loader is now fully functional. Future enhancements might look into potential race conditions and investigate optimizations. But chances are this is good enough for most scenarios. Let me know in the comments if you have a different approach and we can compare notes!


An Approach to Lazy Loading Custom Elements originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/an-approach-to-lazy-loading-custom-elements/feed/ 4 376991
Using Custom Elements in Svelte https://css-tricks.com/using-custom-elements-in-svelte/ https://css-tricks.com/using-custom-elements-in-svelte/#respond Tue, 22 Jun 2021 14:37:50 +0000 https://css-tricks.com/?p=342634 Svelte fully supports custom elements (e.g. <my-component>) without any custom configuration or wrapper components and has a perfect score on Custom Elements Everywhere. However, there are still a few quirks you need to watch out for, especially around …


Using Custom Elements in Svelte originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Svelte fully supports custom elements (e.g. <my-component>) without any custom configuration or wrapper components and has a perfect score on Custom Elements Everywhere. However, there are still a few quirks you need to watch out for, especially around how Svelte sets data on custom elements. At Alaska Airlines, we experienced many of these issues first-hand as we integrated the custom elements from our design system into a Svelte application.

While Svelte supports compiling to custom elements, that is not within the scope of this post. Instead, I will focus on using custom elements built with the Lit custom element library in a Svelte application. These concepts should transfer to custom elements built with or without a supporting library.

Property or attribute?

To fully understand how to use custom elements in Svelte, you need to understand how Svelte passes data to a custom element.

Svelte uses a simple heuristic to determine whether to pass data to a custom element as a property or an attribute. If a corresponding property exists on the custom element at runtime, Svelte will pass the data as a property. Otherwise, it will pass it as an attribute. This seems simple, but has interesting implications.

For instance, let’s say you have a coffee-mug custom element that takes a size property. You can use it in a Svelte component like so:

<coffee-mug class="mug" size="large"></coffee-mug>

You can open this Svelte REPL to follow along. You should see the custom element render the text “This coffee mug’s size is: large ☕️.”

When writing the HTML inside the component, it seems like you’re setting both class and size as attributes. However, this is not the case. Right-click on the “This coffee mug’s size is” text in the REPL’s output and click “Inspect.” This will bring open the DevTools inspector. When you inspect the rendered HTML, you’ll notice that only class was set as an attribute — it’s as if size simply disappeared! However, size is getting set somehow, because “large” still appears in the element’s rendered text.

This is because size is a property on the element, but class is not. Because Svelte detects a size property, it chooses to set that property instead of an attribute. There is no class property, so Svelte sets it as an attribute instead. That’s not a problem or something that changes how we expect the component to behave, but can be very confusing if you’re unaware of it, because there’s a disconnect between the HTML you think you’re writing and what Svelte actually outputs.

Svelte isn’t unique in this behavior — Preact uses a similar method to determine whether to set an attribute or a property on custom elements. Because of that, the use cases I discuss will also occur in Preact, though the workarounds will be different. You will not run into these issues with Angular or Vue because they have a special syntax that lets you choose to set an attribute or a property.

Svelte’s heuristic makes it easy to pass complex data like arrays and objects which need to be set as properties. Consumers of your custom elements shouldn’t need to think about whether they need to set an attribute or a property — it just magically works. However, like any magic in web development, you eventually run into some cases that require you to dig a little deeper and understand what’s going on behind the scenes.

Let’s go through some use cases where custom elements behave strangely. You can find the final examples in this Svelte REPL.

Attributes used as styling hooks

Let’s say you have a custom-text element that displays some text. If the flag attribute is present, it prepends a flag emoji and the word “Flagged:” to the text. The element is coded as follows:

import { html, css, LitElement } from 'lit';
export class CustomText extends LitElement {
  static get styles() {
    return css`
      :host([flag]) p::before {
        content: '🚩';
      }
    `;
  }
  static get properties() {
    return {
      flag: {
        type: Boolean
      }
    };
  }
  constructor() {
    super();
    this.flag = false;
  }
  render() {
    return html`<p>
      ${this.flag ? html`<strong>Flagged:</strong>` : ''}
      <slot></slot>
    </p>`;
  }
}
customElements.define('custom-text', CustomText);

You can see the element in action in this CodePen.

However, if you try to use the custom element the same way in Svelte, it doesn’t entirely work. The “Flagged:” text is shown, but the emoji is not. What gives?

<script>
  import './custom-elements/custom-text';
</script>

<!-- This shows the "Flagged:" text, but not 🚩 -->
<custom-text flag>Just some custom text.</custom-text>

The key here is the :host([flag]) selector. :host selects the element’s shadow root (i.e. the <custom-text> element), so this selector only applies if the flag attribute is present on the element. Since Svelte chooses to set the property instead, this selector doesn’t apply. The “Flagged:” text is added based on the property, which is why that still showed.

So what are our options here? Well, the custom element shouldn’t have assumed that flag would always be set as an attribute. It is a custom element best practice to keep primitive data attributes and properties in sync since you don’t know how the consumer of the element will interact with it. The ideal solution is for the element author to make sure any primitive properties are reflected to attributes, especially if those attributes are used for styling. Lit makes it easy to reflect your properties:

static get properties() {
  return {
    flag: {
      type: Boolean,
      reflect: true
    }
  };
}

With that change, the flag property is reflected back to the attribute, and everything displays as expected.

However, there may be cases where you don’t have control over the custom element definition. In that case, you can force Svelte to set the attribute using a Svelte action.

Using a Svelte action to force setting attributes

Actions are a powerful Svelte feature that run a function when a certain node is added to the DOM. For example, we can write an action that will set the flag attribute on our custom-text element:

<script>
  import './custom-elements/custom-text';
  function setAttributes(node) {
    node.setAttribute('flag', '');
  }
</script>

<custom-text use:setAttributes>
  Just some custom text.
</custom-text>

Actions can also take parameters. For instance, we could make this action more generic and accept an object containing the attributes we want to set on a node.

<script>
  import './custom-elements/custom-text';
  function setAttributes(node, attributes) {
    Object.entries(attributes).forEach(([k, v]) => {
      if (v !== undefined) {
        node.setAttribute(k, v);
      } else {
        node.removeAttribute(k);
      }
    });
  }
</script>

<custom-text use:setAttributes={{ flag: true }}>
  Just some custom text.
</custom-text>

Finally, if we want the attributes to react to state changes, we can return an object with an update method from the action. Whenever the parameters we pass to the action change, the update function will be called.

<script>
  import './custom-elements/custom-text';
  function setAttributes(node, attributes) {
    const applyAttributes = () => {
      Object.entries(attributes).forEach(([k, v]) => {
        if (v !== undefined) {
          node.setAttribute(k, v);
        } else {
          node.removeAttribute(k);
        }
      });
    };
    applyAttributes();
    return {
      update(updatedAttributes) {
        attributes = updatedAttributes;
        applyAttributes();
      }
    };
  }
  let flagged = true;
</script>
<label><input type="checkbox" bind:checked={flagged} /> Flagged</label>
<custom-text use:setAttributes={{ flag: flagged ? '' : undefined }}>
  Just some custom text.
</custom-text>

Using this approach, we don’t have to update the custom element to reflect the property — we can control setting the attribute from inside our Svelte app.

Lazy-loading custom elements

Custom elements are not always defined when the component first renders. For example, you may wait to import your custom elements until after the web component polyfills have loaded. Also, in a server-side rendering context such as Sapper or SvelteKit, the initial server render will take place without loading the custom element definition.

In either case, if the custom element is not defined, Svelte will set everything as attributes. This is because the property does not exist on the element yet. This is confusing if you’ve grown accustomed to Svelte only setting properties on custom elements. This can cause issues with complex data such as objects and arrays.

As an example, let’s look at the following custom element that displays a greeting followed by a list of names.

import { html, css, LitElement } from 'lit';
export class FancyGreeting extends LitElement {
  static get styles() {
    return css`
      p {
        border: 5px dashed mediumaquamarine;
        padding: 4px;
      }
    `;
  }
  static get properties() {
    return {
      names: { type: Array },
      greeting: { type: String }
    };
  }
  constructor() {
    super();
    this.names = [];
  }
  render() {
    return html`<p>
      ${this.greeting},
      ${this.names && this.names.length > 0 ? this.names.join(', ') : 'no one'}!
    </p>`;
  }
}
customElements.define('fancy-greeting', FancyGreeting);

You can see the element in action in this CodePen.

If we statically import the element in a Svelte application, everything works as expected.

<script>
  import './custom-elements/fancy-greeting';
</script>
<!-- This displays "Howdy, Amy, Bill, Clara!" -->
<fancy-greeting greeting="Howdy" names={['Amy', 'Bill', 'Clara']} />

However, if we dynamically import the component, the custom element does not become defined until after the component has first rendered. In this example, I wait to import the element until the Svelte component has been mounted using the onMount lifecycle function. When we delay importing the custom element, the list of names is not set properly and the fallback content is displayed instead.

<script>
  import { onMount } from 'svelte';
  onMount(async () => {
    await import('./custom-elements/fancy-greeting');
  });
</script>
<!-- This displays "Howdy, no one!"-->
<fancy-greeting greeting="Howdy" names={['Amy', 'Bill', 'Clara']} />

Because the custom element definition is not loaded when Svelte adds fancy-greeting to the DOM, fancy-greeting does not have a names property and Svelte sets the names attribute — but as a string, not as a stringified array. If you inspect the element in your browser DevTools, you’ll see the following:

<fancy-greeting greeting="Howdy" names="Amy,Bill,Clara"></fancy-greeting> 

Our custom element tries to parse the names attribute as an array using JSON.parse, which throws an exception. This is handled automatically using Lit’s default array converter, but the same would apply to any element that expects an attribute to contain a valid JSON array.

Interestingly, once you update the data passed to the custom element Svelte will start setting the property again. In the below example, I moved the array of names to the state variable names so that I can update it. I also added an “Add name” button that will append the name “Rory” to the end of the names array when clicked.

Once the button is clicked, the names array is updated, which triggers a re-render of the component. Since the custom element is now defined, Svelte detects the names property on the custom element and sets that instead of the attribute. This causes the custom element to properly display the list of names instead of the fallback content.

<script>
  import { onMount } from 'svelte';
  onMount(async () => {
    await import('./custom-elements/fancy-greeting');
  });
  let names = ['Amy', 'Bill', 'Clara'];
  function addName() {
    names = [...names, 'Rory'];
  }
</script>

<!-- Once the button is clicked, the element displays "Howdy, Amy, Bill, Clara, Rory!" -->
<fancy-greeting greeting="Howdy" {names} />
<button on:click={addName}>Add name</button>

As in the previous example, we can force Svelte to set the data how we want using an action. This time, instead of setting everything as an attribute, we want to set everything as a property. We will pass an object as a parameter that contains the properties we want to set on the node. Here’s how our action will be applied to the custom element:

<fancy-greeting
  greeting="Howdy"
  use:setProperties={{ names: ['Amy', 'Bill', 'Clara'] }}
/>

Below is the the implementation of the action. We iterate over the properties object and use each entry to set the property on the custom element node. We also return an update function so that the properties are reapplied if the parameters passed to the action change. See the previous section if you want a refresher on how you can react to state changes with an action.

function setProperties(node, properties) {
  const applyProperties = () => {
    Object.entries(properties).forEach(([k, v]) => {
      node[k] = v;
    });
  };
  applyProperties();
  return {
    update(updatedProperties) {
      properties = updatedProperties;
      applyProperties();
    }
  };
}

By using the action, the names are displayed properly on first render. Svelte sets the property when first rendering the component, and the custom element picks that property up once the element has been defined.

Boolean attributes

The final issue we ran into is how Svelte handles boolean attributes on a custom element. This behavior has recently changed with Svelte 3.38.0, but we’ll explore pre- and post-3.38 behavior since not everyone will be on the latest Svelte version.

Suppose we have a <secret-box> custom element with a boolean property open that indicates whether the box is open or not. The implementation looks like this:

import { html, LitElement } from 'lit';
export class SecretBox extends LitElement {
  static get properties() {
    return {
      open: {
        type: Boolean
      }
    };
  }
  render() {
    return html`<div>The box is ${this.open ? 'open 🔓' : 'closed 🔒'}</div>`;
  }
}
customElements.define('secret-box', SecretBox);

You can see the element in action in this CodePen.

As seen in the CodePen, you can set the open property to true multiple ways. Per the HTML spec, the presence of a boolean attribute represents the true value, and its absence represents false.

<secret-box open></secret-box>
<secret-box open=""></secret-box>
<secret-box open="open"></secret-box>

Interestingly, only the last of the above options shows “The box is open” when used inside a Svelte component. The first two show “The box is closed” despite setting the open attribute. What’s going on here?

As with the other examples, it all goes back to Svelte choosing properties over attributes. If you inspect the elements in the browser DevTools, no attributes are set — Svelte has set everything as properties. We can console.log the open property inside our render method (or query the element in the console) to discover what Svelte set the open property to.

// <secret-box open> logs ''
// <secret-box open=""> logs ''
// <secret-box open="open"> logs 'open'
render() {
  console.log(this.open);
  return html`<div>The box is ${this.open ? 'open 🔓' : 'closed 🔒'}</div>`;
}

In the first two cases, open equals an empty string. Since an empty string is falsy in JavaScript, our ternary statement evaluates to the false case and shows that the box is closed. In the final case, the open property is set to the string “open” which is truthy. The ternary statement evaluates to the true case and shows that the box is open.

As a side note, you don’t run into this issue when you lazy load the element. Since the custom element definition is not loaded when Svelte renders the element, Svelte sets the attribute instead of the property. See the above section for a refresher.

There’s an easy way around this issue. If you remember that you’re setting the property, not the attribute, you can explicitly set the open property to true with the following syntax.

<secret-box open={true}></secret-box>

This way you know you’re setting the open property to true. Setting to a non-empty string also works, but this way is the most accurate since you’re setting true instead of something that happens to be truthy.

Until recently, this was the only way to properly set boolean properties on custom elements. However, with Svelte 3.38, I had a change released that updated Svelte’s heuristic to allow setting shorthand boolean properties. Now, if Svelte knows that the underlying property is a boolean, it will treat the open and open="" syntaxes the same as open={true}.

This is especially helpful since this is how you see examples in many custom element component libraries. This change makes it easy to copy-paste out of the docs without having to troubleshoot why a certain attribute isn’t working how you’d expect.

However, there is one requirement on the custom element author side — the boolean property needs a default value so that Svelte knows it’s of boolean type. This is a good practice anyway if you want that property to be a boolean.

In our secret-box element, we can add a constructor and set the default value:

constructor() {
  super();
  this.open = true;
}

With that change, the following will correctly display “The box is open” in a Svelte component.

<secret-box open></secret-box>
<secret-box open=""></secret-box>

Wrapping up

Once you understand how Svelte decides to set an attribute or a property, a lot of these seemingly strange issues start to make more sense. Any time you run into issues passing data to a custom element inside a Svelte application, figure out if it’s being set as an attribute or a property and go from there. I’ve given you a few escape hatches in this article to force one or the other when you need to, but they should generally be unnecessary. Most of the time, custom elements in Svelte just work. You just need to know where to look if something does go wrong.


Special thanks to Dale Sande, Gus Naughton, and Nanette Ranes for reviewing an early version of this article.


Using Custom Elements in Svelte originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/using-custom-elements-in-svelte/feed/ 0 342634
Creating Custom Form Controls with ElementInternals https://css-tricks.com/creating-custom-form-controls-with-elementinternals/ https://css-tricks.com/creating-custom-form-controls-with-elementinternals/#comments Wed, 24 Mar 2021 14:22:49 +0000 https://css-tricks.com/?p=336775 Ever since the dawn of time, humanity has dreamed of having more control over form elements. OK, I might be overselling it a tiny bit, but creating or customizing form components has been a holy grail of front-end web development …


Creating Custom Form Controls with ElementInternals originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Ever since the dawn of time, humanity has dreamed of having more control over form elements. OK, I might be overselling it a tiny bit, but creating or customizing form components has been a holy grail of front-end web development for years.

One of the lesser-heralded, but most powerful features of custom elements (e.g. <my-custom-element>) has quietly made its way into Google Chrome as of version 77 and is working its way into other browsers. The ElementInternals standard is a very exciting set of features with a very unassuming name. Among the features internals adds are the ability to participate in forms and an API around accessibility controls.

In this article, we’re going to look at how to create a custom form control, integrate constraint validation, introduce the basics of internal accessibility and see a way to combine these features to create a highly-portable macro form control.

Let’s start by creating a very simple custom element that matches our design system. Our element will hold all of its styles within the shadow DOM and ensure some basic accessibility. We’ll use the wonderful LitElement library from the Polymer team at Google for our code examples and, although you definitely don’t need it, it does provide a great abstraction for writing custom elements.

In this Pen, we’ve created a <rad-input> that has some basic design to it. We have also added a second input to our form that is a vanilla HTML input, and added a default value (so you can simply press submit and see it work).

When we click our submit button a few things happen. First, the submit event’s preventDefault method is called, in this case, to ensure our page doesn’t reload. After this, we create a FormData object which gives us access to information about our form which we use to construct a JSON string and append it to an <output> element. Notice, however, that the only value-added to our output is from the element with name="basic".

That’s because our element doesn’t know how to interact with the form just yet, so let’s set up our <rad-input> with an ElementInternals instance to help it live up to its name. To start, we’ll need to call our method’s attachInternals method in the element’s constructor, we’ll also be importing an ElementInternals polyfill into our page to work with browsers that don’t support the spec yet.

The attachInternals method returns a new element internals instance which contains some new APIs we can use in our method. In order to let our element take advantage of these APIs, we need to add a static formAssociated getter that returns true.

class RadInput extends LitElement {
  static get formAssociated() {
    return true;
  }

  constructor() {
    super();
    this.internals = this.attachInternals();
  }
}

Let’s take a look at some of the APIs in our element’s internals property:

  • setFormValue(value: string|FormData|File, state?: any): void — This method will set the element’s value on its parent form if one is present. If the value is null, the element will not participate in the form submission process.
  • form — A reference to our element’s parent form, if one exists.
  • setValidity(flags: Partial<ValidityState>, message?: string, anchor?: HTMLElement): void — The setValidity method will help control our element’s validity state within the form. If the form is invalid, a validation message must be present.
  • willValidate — Will be true if the element will be evaluated when the form is submitted.
  • validity — A validity object that matches the APIs and semantics attached to HTMLInputElement.prototype.validity.
  • validationMessage — If the control has been set as invalid with setValidity, this is the message that was passed in describing the error.
  • checkValidity — Will return true if the element is valid, otherwise this will return false and fire an invalid event on the element.
  • reportValidity — Does the same as checkValidity, and will report problems to the user if the event isn’t cancelled.
  • labels — A list of elements that label this element using the label[for] attribute.
  • A number of other controls used to set aria information on the element.

Setting a custom element’s value

Let’s modify our <rad-input> to take advantage of some of these APIs:

Here we’ve modified the element’s _onInput method to include a call to this.internals.setFormValue. This tells the form our element wants to register a value with the form under its given name (which is set as an attribute in our HTML). We’ve also added a firstUpdated method (loosely analogous with connectedCallback when not using LitElement) which sets the element’s value to an empty string whenever the element is done rendering. This is to make sure our element always has a value with the form (and though it is not necessary, you may want to exclude your element from the form by passing in a null value).

Now when we add a value to our input and submit the form, we will see that we have a radInput value in our <output> element. We can also see our element has been added to the HTMLFormElement’s radInput property. One thing you might have noticed, however, is that despite the fact that despite the fact that our element doesn’t have a value, it will still allow the form submission to take place. Let’s add some validation to our element next.

Adding constraint validation

In order to set our field’s validation, we need to modify our element a little bit to make use of the setValidity method on our element internals object. This method will take in three arguments (the second one is only required if the element is invalid, the third is always optional). The first argument is a partial ValidityState object. If any flag is set to true the control will be marked as invalid. If one of the built-in validity keys doesn’t meet your needs, there is a catch-all customError key that should work. Lastly, if the control is valid, we pass in an object literal ({}) to reset the control’s validity.

The second argument here is the control’s validity message. This argument is required if the control is invalid, and not allowed if the control is valid. The third argument is an optional validation target that will control the user’s focus if and when the form is submitted as invalid or reportValidity is called.

We’re going to introduce a new method to our <rad-input> that will take care of this logic for us:

_manageRequired() {
  const { value } = this;
  const input = this.shadowRoot.querySelector('input');
  if (value === '' && this.required) {
    this.internals.setValidity({
      valueMissing: true
    }, 'This field is required', input);
  } else {
    this.internals.setValidity({});
  }
}

This function gets the control’s value and input. If the value is equal to an empty string and the element is marked as required, we’ll call the internals.setValidity and toggle the control’s validity. Now we all we need to do is call this method in our firstUpdated and _onInput methods and we’ll have added some basic validation to our element.

Clicking the submit button before a value is entered into our <rad-input> will now display an error message in browsers that support the ElementInternals spec. Unfortunately, displaying validation errors is still not supported by the polyfill as there isn’t any reliable way to trigger the built-in validation popup in non-supporting browsers.

We’ve also added some basic accessibility information to our example by using our internals object. We’ve added an additional property to our element, _required, which will serve as a proxy for this.required and as a getter/setter for required.

get required() {
  return this._required;
}

set required(isRequired) {
  this._required = isRequired;
  this.internals.ariaRequired = isRequired;
}

By passing the required property to internals.ariaRequired, we are alerting screen readers that our element is currently expecting a value. In the polyfill, this is done by adding an aria-required attribute; however, in supporting browsers, the attribute won’t be added to the element because that property is inherent to the element.

Creating a micro-form

Now that we have a working input that meets our design system, we might want to begin composing our elements into patterns that we can reuse throughout several applications. One of the most compelling features for ElementInternals is that the setFormValue method can take not only string and file data, but also FormData objects. So let’s say we want to create a common address form that might be used in multiple organizations, we can do that easily with our newly-created elements.

In this example, we have a form created inside our element’s shadow root where we have composed four <rad-input> elements to make an address form. Instead of calling setFormValue with a string, this time we’ve chosen to pass along the entire value of our form. As a result, our element passes along the values of each individual element inside its child form to the outer form.


Adding constraint validation to this form would be a fairly straightforward process, as would providing additional styles, behaviors and slotting in content. Using these newer APIs finally allows developers to unlock a ton of potential inside custom elements and finally gives us free-range on controlling our user experiences.


Creating Custom Form Controls with ElementInternals originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/creating-custom-form-controls-with-elementinternals/feed/ 6 336775
Web Components Are Easier Than You Think https://css-tricks.com/web-components-are-easier-than-you-think/ https://css-tricks.com/web-components-are-easier-than-you-think/#comments Mon, 08 Mar 2021 16:06:03 +0000 https://css-tricks.com/?p=335065 When I’d go to a conference (when we were able to do such things) and see someone do a presentation on web components, I always thought it was pretty nifty (yes, apparently, I’m from 1950), but it always seemed complicated …


Web Components Are Easier Than You Think originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
When I’d go to a conference (when we were able to do such things) and see someone do a presentation on web components, I always thought it was pretty nifty (yes, apparently, I’m from 1950), but it always seemed complicated and excessive. A thousand lines of JavaScript to save four lines of HTML. The speaker would inevitably either gloss over the oodles of JavaScript to get it working or they’d go into excruciating detail and my eyes would glaze over as I thought about whether my per diem covered snacks.

But in a recent reference project to make learning HTML easier (by adding zombies and silly jokes, of course), the completist in me decided I had to cover every HTML element in the spec. Beyond those conference presentations, this was my first introduction to the <slot> and <template> elements. But as I tried to write something accurate and engaging, I was forced to delve a bit deeper.

And I’ve learned something in the process: web components are a lot easier than I remember.

Article series

Either web components have come a long way since the last time I caught myself daydreaming about snacks at a conference, or I let my initial fear of them get in the way of truly knowing them — probably both.

I’m here to tell you that you—yes, you—can create a web component. Let’s leave our distractions, fears, and even our snacks at the door for a moment and do this together.

Let’s start with the <template>

A <template> is an HTML element that allows us to create, well, a template—the HTML structure for the web component. A template doesn’t have to be a huge chunk of code. It can be as simple as:

<template>
  <p>The Zombies are coming!</p>
</template>

The <template> element is important because it holds things together. It’s like the foundation of building; it’s the base from which everything else is built. Let’s use this small bit of HTML as the template for an <apocalyptic-warning> web component—you know, as a warning when the zombie apocalypse is upon us.

Then there’s the <slot>

<slot> is merely another HTML element just like <template>. But in this case, <slot> customizes what the <template> renders on the page.

<template>
  <p>The <slot>Zombies</slot> are coming!</p>
</template>

Here, we’ve slotted (is that even a word?) the word “Zombies” in the templated markup. If we don’t do anything with the slot, it defaults to the content between the tags. That would be “Zombies” in this example.

Using <slot> is a lot like having a placeholder. We can use the placeholder as is, or define something else to go in there instead. We do that with the name attribute.

<template>
  <p>The <slot name="whats-coming">Zombies</slot> are coming!</p>
</template>

The name attribute tells the web component which content goes where in the template. Right now, we’ve got a slot called whats-coming. We’re assuming zombies are coming first in the apocalypse, but the <slot> gives us some flexibility to slot something else in, like if it ends up being a robot, werewolf, or even a web component apocalypse.

Using the component

We’re technically done “writing” the component and can drop it in anywhere we want to use it.

<apocalyptic-warning>
  <span slot="whats-coming">Halitosis Laden Undead Minions</span>
</apocalyptic-warning>

<template>
  <p>The <slot name="whats-coming">Zombies</slot> are coming!</p>
</template>

See what we did there? We put the <apocalyptic-warning> component on the page just like any other <div> or whatever. But we also dropped a <span> in there that references the name attribute of our <slot>. And what’s between that <span> is what we want to swap in for “Zombies” when the component renders.

Here’s a little gotcha worth calling out: custom element names must have a hyphen in them. It’s just one of those things you’ve gotta know going into things. The spec prescribes that to prevent conflicts in the event that HTML releases a new element with the same name.

Still with me so far? Not too scary, right? Well, minus the zombies. We still have a little work to do to make the <slot> swap possible, and that’s where we start to get into JavaScript.

Registering the component

As I said, you do need some JavaScript to make this all work, but it’s not the super complex, thousand-lined, in-depth code I always thought. Hopefully I can convince you as well.

You need a constructor function that registers the custom element. Otherwise, our component is like the undead: it’s there but not fully alive.

Here’s the constructor we’ll use:

// Defines the custom element with our appropriate name, <apocalyptic-warning>
customElements.define("apocalyptic-warning",

  // Ensures that we have all the default properties and methods of a built in HTML element
  class extends HTMLElement {

    // Called anytime a new custom element is created
    constructor() {

      // Calls the parent constructor, i.e. the constructor for `HTMLElement`, so that everything is set up exactly as we would for creating a built in HTML element
      super();

      // Grabs the <template> and stores it in `warning`
      let warning = document.getElementById("warningtemplate");

      // Stores the contents of the template in `mywarning`
      let mywarning = warning.content;

      const shadowRoot = this.attachShadow({mode: "open"}).appendChild(mywarning.cloneNode(true));
    }
  });

I left detailed comments in there that explain things line by line. Except the last line:

const shadowRoot = this.attachShadow({mode: "open"}).appendChild(mywarning.cloneNode(true));

We’re doing a lot in here. First, we’re taking our custom element (this) and creating a clandestine operative—I mean, shadow DOM. mode: open simply means that JavaScript from outside the :root can access and manipulate the elements within the shadow DOM, sort of like setting up back door access to the component.

From there, the shadow DOM has been created and we append a node to it. That node will be a deep copy of the template, including all elements and text of the template. With the template attached to the shadow DOM of the custom element, the <slot> and slot attribute take over for matching up content with where it should go.

Check this out. Now we can plop two instances of the same component, rendering different content simply by changing one element.

Styling the component

You may have noticed styling in that demo. As you might expect, we absolutely have the ability to style our component with CSS. In fact, we can include a <style> element right in the <template>.

<template id="warningtemplate">
  <style>
    p {
      background-color: pink;
      padding: 0.5em;
      border: 1px solid red;
    }
  </style>

    <p>The <slot name="whats-coming">Zombies</slot> are coming!</p>
</template>

This way, the styles are scoped directly to the component and nothing leaks out to other elements on the same page, thanks to the shadow DOM.

Now in my head, I assumed that a custom element was taking a copy of the template, inserting the content you’ve added, and then injecting that into the page using the shadow DOM. While that’s what it looks like on the front end, that’s not how it actually works in the DOM. The content in a custom element stays where it is and the shadow DOM is sort of laid on top like an overlay.

Screenshot of the HTML source of the zombie-warning component. The custom element is expanded in the shadow dam, including the style block, the custom element, and the template.

And since the content is technically outside the template, any descendant selectors or classes we use in the template’s <style> element will have no affect on the slotted content. This doesn’t allow full encapsulation the way I had hoped or expected. But since a custom element is an element, we can use it as an element selector in any ol’ CSS file, including the main stylesheet used on a page. And although the inserted material isn’t technically in the template, it is in the custom element and descendant selectors from the CSS will work.

apocalyptic-warning span {
  color: blue;
}

But beware! Styles in the main CSS file cannot access elements in the <template> or shadow DOM.

Let’s put all of this together

Let’s look at an example, say a profile for a zombie dating service, like one you might need after the apocalypse. In order to style both the default content and any inserted content, we need both a <style> element in the <template> and styling in a CSS file.

The JavaScript code is exactly the same except now we’re working with a different component name, <zombie-profile>.

customElements.define("zombie-profile",
  class extends HTMLElement {
    constructor() {
      super();
      let profile = document.getElementById("zprofiletemplate");
      let myprofile = profile.content;
      const shadowRoot = this.attachShadow({mode: "open"}).appendChild(myprofile.cloneNode(true));
    }
  }
);

Here’s the HTML template, including the encapsulated CSS:

<template id="zprofiletemplate">
  <style>
    img {
      width: 100%;
      max-width: 300px;
      height: auto;
      margin: 0 1em 0 0;
    }
    h2 {
      font-size: 3em;
      margin: 0 0 0.25em 0;
      line-height: 0.8;
    }
    h3 {
      margin: 0.5em 0 0 0;
      font-weight: normal;
    }
    .age, .infection-date {
      display: block;
    }
    span {
      line-height: 1.4;
    }
    .label {
      color: #555;
    }
    li, ul {
      display: inline;
      padding: 0;
    }
    li::after {
      content: ', ';
    }
    li:last-child::after {
      content: '';
    }
    li:last-child::before {
      content: ' and ';
    }
  </style>

  <div class="profilepic">
    <slot name="profile-image"><img src="https://assets.codepen.io/1804713/default.png" alt=""></slot>
  </div>

  <div class="info">
    <h2><slot name="zombie-name" part="zname">Zombie Bob</slot></h2>

    <span class="age"><span class="label">Age:</span> <slot name="z-age">37</slot></span>
    <span class="infection-date"><span class="label">Infection Date:</span> <slot name="idate">September 12, 2025</slot></span>

    <div class="interests">
      <span class="label">Interests: </span>
      <slot name="z-interests">
        <ul>
          <li>Long Walks on Beach</li>
          <li>brains</li>
          <li>defeating humanity</li>
        </ul>
      </slot>
    </div>

    <span class="z-statement"><span class="label">Apocalyptic Statement: </span> <slot name="statement">Moooooooan!</slot></span>

  </div>
</template>

Here’s the CSS for our <zombie-profile> element and its descendants from our main CSS file. Notice the duplication in there to ensure both the replaced elements and elements from the template are styled the same.

zombie-profile {
  width: calc(50% - 1em);
  border: 1px solid red;
  padding: 1em;
  margin-bottom: 2em;
  display: grid;
  grid-template-columns: 2fr 4fr;
  column-gap: 20px;
}
zombie-profile img {
  width: 100%;
  max-width: 300px;
  height: auto;
  margin: 0 1em 0 0;
}
zombie-profile li, zombie-profile ul {
  display: inline;
  padding: 0;
}
zombie-profile li::after {
  content: ', ';
}
zombie-profile li:last-child::after {
  content: '';
}
zombie-profile li:last-child::before {
  content: ' and ';
}

All together now!

While there are still a few gotchas and other nuances, I hope you feel more empowered to work with the web components now than you were a few minutes ago. Dip your toes in like we have here. Maybe sprinkle a custom component into your work here and there to get a feel for it and where it makes sense.

That’s really it. Now what are you more scared of, web components or the zombie apocalypse? I might have said web components in the not-so-distant past, but now I’m proud to say that zombies are the only thing that worry me (well, that and whether my per diem will cover snacks…)


Web Components Are Easier Than You Think originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/web-components-are-easier-than-you-think/feed/ 25 335065
3 Approaches to Integrate React with Custom Elements https://css-tricks.com/3-approaches-to-integrate-react-with-custom-elements/ https://css-tricks.com/3-approaches-to-integrate-react-with-custom-elements/#respond Fri, 15 Jan 2021 19:26:20 +0000 https://css-tricks.com/?p=332292 In my role as a web developer who sits at the intersection of design and code, I am drawn to Web Components because of their portability. It makes sense: custom elements are fully-functional HTML elements that work in all modern …


3 Approaches to Integrate React with Custom Elements originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
In my role as a web developer who sits at the intersection of design and code, I am drawn to Web Components because of their portability. It makes sense: custom elements are fully-functional HTML elements that work in all modern browsers, and the shadow DOM encapsulates the right styles with a decent surface area for customization. It’s a really nice fit, especially for larger organizations looking to create consistent user experiences across multiple frameworks, like Angular, Svelte and Vue.

In my experience, however, there is an outlier where many developers believe that custom elements don’t work, specifically those who work with React, which is, arguably, the most popular front-end library out there right now. And it’s true, React does have some definite opportunities for increased compatibility with the web components specifications; however, the idea that React cannot integrate deeply with Web Components is a myth.

In this article, I am going to walk through how to integrate a React application with Web Components to create a (nearly) seamless developer experience. We will look at React best practices its and limitations, then create generic wrappers and custom JSX pragmas in order to more tightly couple our custom elements and today’s most popular framework.

Coloring in the lines

If React is a coloring book — forgive the metaphor, I have two small children who love to color — there are definitely ways to stay within the lines to work with custom elements. To start, we’ll write a very simple custom element that attaches a text input to the shadow DOM and emits an event when the value changes. For the sake of simplicity, we’ll be using LitElement as a base, but you can certainly write your own custom element from scratch if you’d like.

Our super-cool-input element is basically a wrapper with some styles for a plain ol’ <input> element that emits a custom event. It has a reportValue method for letting users know the current value in the most obnoxious way possible. While this element might not be the most useful, the techniques we will illustrate while plugging it into React will be helpful for working with other custom elements.

Approach 1: Use ref

According to React’s documentation for Web Components, “[t]o access the imperative APIs of a Web Component, you will need to use a ref to interact with the DOM node directly.”

This is necessary because React currently doesn’t have a way to listen to native DOM events (preferring, instead, to use it’s own proprietary SyntheticEvent system), nor does it have a way to declaratively access the current DOM element without using a ref.

We will make use of React’s useRef hook to create a reference to the native DOM element we have defined. We will also use React’s useEffect and useState hooks to gain access to the input’s value and render it to our app. We will also use the ref to call our super-cool-input’s reportValue method if the value is ever a variant of the word “rad.”

One thing to take note of in the example above is our React component’s useEffect block.

useEffect(() => {
  coolInput.current.addEventListener('custom-input', eventListener);
  
  return () => {
    coolInput.current.removeEventListener('custom-input', eventListener);
  }
});

The useEffect block creates a side effect (adding an event listener not managed by React), so we have to be careful to remove the event listener when the component needs a change so that we don’t have any unintentional memory leaks.

While the above example simply binds an event listener, this is also a technique that can be employed to bind to DOM properties (defined as entries on the DOM object, rather than React props or DOM attributes).

This isn’t too bad. We have our custom element working in React, and we’re able to bind to our custom event, access the value from it, and call our custom element’s methods as well. While this does work, it is verbose and doesn’t really look like React.

Approach 2: Use a wrapper

Our next attempt at using our custom element in our React application is to create a wrapper for the element. Our wrapper is simply a React component that passes down props to our element and creates an API for interfacing with the parts of our element that aren’t typically available in React.

Here, we have moved the complexity into a wrapper component for our custom element. The new CoolInput React component manages creating a ref while adding and removing event listeners for us so that any consuming component can pass props in like any other React component.

function CoolInput(props) {
  const ref = useRef();
  const { children, onCustomInput, ...rest } = props;
  
  function invokeCallback(event) {
    if (onCustomInput) {
      onCustomInput(event, ref.current);
    }
  }
  
  useEffect(() => {
    const { current } = ref;
    current.addEventListener('custom-input', invokeCallback);
    return () => {
      current.removeEventListener('custom-input', invokeCallback);
    }
  });
  
  return <super-cool-input ref={ref} {...rest}>{children}</super-cool-input>;
}

On this component, we have created a prop, onCustomInput, that, when present, triggers an event callback from the parent component. Unlike a normal event callback, we chose to add a second argument that passes along the current value of the CoolInput’s internal ref.

Using these same techniques, it is possible to create a generic wrapper for a custom element, such as this reactifyLitElement component from Mathieu Puech. This particular component takes on defining the React component and managing the entire lifecycle.

Approach 3: Use a JSX pragma

One other option is to use a JSX pragma, which is sort of like hijacking React’s JSX parser and adding our own features to the language. In the example below, we import the package jsx-native-events from Skypack. This pragma adds an additional prop type to React elements, and any prop that is prefixed with onEvent adds an event listener to the host.

To invoke a pragma, we need to import it into the file we are using and call it using the /** @jsx <PRAGMA_NAME> */ comment at the top of the file. Your JSX compiler will generally know what to do with this comment (and Babel can be configured to make this global). You might have seen this in libraries like Emotion.

An <input> element with the onEventInput={callback} prop will run the callback function whenever an event with the name 'input' is dispatched. Let’s see how that looks for our super-cool-input.

The code for the pragma is available on GitHub. If you want to bind to native properties instead of React props, you can use react-bind-properties. Let’s take a quick look at that:

import React from 'react'

/**
 * Convert a string from camelCase to kebab-case
 * @param {string} string - The base string (ostensibly camelCase)
 * @return {string} - A kebab-case string
 */
const toKebabCase = string => string.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, '$1-$2').toLowerCase()

/** @type {Symbol} - Used to save reference to active listeners */
const listeners = Symbol('jsx-native-events/event-listeners')

const eventPattern = /^onEvent/

export default function jsx (type, props, ...children) {
  // Make a copy of the props object
  const newProps = { ...props }
  if (typeof type === 'string') {
    newProps.ref = (element) => {
      // Merge existing ref prop
      if (props && props.ref) {
        if (typeof props.ref === 'function') {
          props.ref(element)
        } else if (typeof props.ref === 'object') {
          props.ref.current = element
        }
      }

      if (element) {
        if (props) {
          const keys = Object.keys(props)
          /** Get all keys that have the `onEvent` prefix */
          keys
            .filter(key => key.match(eventPattern))
            .map(key => ({
              key,
              eventName: toKebabCase(
                key.replace('onEvent', '')
              ).replace('-', '')
            })
          )
          .map(({ eventName, key }) => {
            /** Add the listeners Map if not present */
            if (!element[listeners]) {
              element[listeners] = new Map()
            }

            /** If the listener hasn't be attached, attach it */
            if (!element[listeners].has(eventName)) {
              element.addEventListener(eventName, props[key])
              /** Save a reference to avoid listening to the same value twice */
              element[listeners].set(eventName, props[key])
            }
          })
        }
      }
    }
  }
  
  return React.createElement.apply(null, [type, newProps, ...children])
}

Essentially, this code converts any existing props with the onEvent prefix and transforms them to an event name, taking the value passed to that prop (ostensibly a function with the signature (e: Event) => void) and adding it as an event listener on the element instance.

Looking forward

As of the time of this writing, React recently released version 17. The React team had initially planned to release improvements for compatibility with custom elements; unfortunately, those plans seem to have been pushed back to version 18.

Until then it will take a little extra work to use all the features custom elements offer with React. Hopefully, the React team will continue to improve support to bridge the gap between React and the web platform.


3 Approaches to Integrate React with Custom Elements originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/3-approaches-to-integrate-react-with-custom-elements/feed/ 0 332292
Playing With (Fake) Container Queries With watched-box & resizeasaurus https://css-tricks.com/playing-with-fake-container-queries-with-watched-box-resizeasaurus/ https://css-tricks.com/playing-with-fake-container-queries-with-watched-box-resizeasaurus/#comments Tue, 05 May 2020 00:40:03 +0000 https://css-tricks.com/?p=308022 Heydon’s <watched-box> is a damn fantastic tool. It’s a custom element that essentially does container queries by way of class names that get added to the box based on size breakpoints that are calculated with ResizeObserver. It’s like a …


Playing With (Fake) Container Queries With watched-box & resizeasaurus originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Heydon’s <watched-box> is a damn fantastic tool. It’s a custom element that essentially does container queries by way of class names that get added to the box based on size breakpoints that are calculated with ResizeObserver. It’s like a cleaner version of what Philip was talking about a few years ago.

I’m sure I’d be happy using <watched-box> on production, as it’s lightweight, has no dependencies, and has a straightforward approach.

For development, I had the idea of using Zach’s interesting little <resize-asaurus> web component. It wraps elements in another box that is resizeable via CSS and labels it with the current width. That way you don’t have to fiddle with the entire browser window to resize things — any given element becomes resizable. Again, just for development and testing reasons.

You’d wrap them together like…

<resize-asaurus>
  <watched-box widthBreaks="320px, 600px">
    <div class="card">
      ... 
    </div>
  </watched-box>
</resize-asaurus>

Which allows you to write CSS at breakpoints like…

.card {
   .w-gt-320px & { }
   .w-gt-600px & { } 
}

That’s surely not what the CSS syntax for container queries syntax will end up being, but it accomplishes the same thing with clear and understandable generated class names.

Example!

Live demo!


Playing With (Fake) Container Queries With watched-box & resizeasaurus originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/playing-with-fake-container-queries-with-watched-box-resizeasaurus/feed/ 2 308022
Toast https://css-tricks.com/toast/ https://css-tricks.com/toast/#comments Tue, 02 Jul 2019 22:40:51 +0000 http://css-tricks.com/?p=292113 One day, all the sudden, I started hearing jokes about toast. I had no idea what the context was. I assumed some friends just got started telling toast jokes, which isn’t rare by any means. But it turns out it …


Toast originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
One day, all the sudden, I started hearing jokes about toast. I had no idea what the context was. I assumed some friends just got started telling toast jokes, which isn’t rare by any means. But it turns out it was a whole thing. It got me thinking, jeez, if I can’t keep up with this stuff when it’s kinda my job, it must be extra tough for people who actually do work for a living.

Anyway. Thankfully Jeremy summed it up nicely:

First off, this all kicked off with the announcement of “intent to implement”. That makes it sounds like Google are intending to, well, …implement this. In fact “intent to implement” really means “intend to mess around with this behind a flag”. The language is definitely confusing and this is something that will hopefully be addressed.

Secondly, Chrome isn’t going to ship a toast element. Instead, this is a proposal for a custom element currently called std-toast. I’m assuming that should the experiment prove successful, it’s not a foregone conclusion that the final element name will be called toast.

The drama around it, hence the cause of all the jokes and such, was the fact that it felt like it came out of nowhere and was Chrome strongarming a feature through the standards process, or perhaps kinda skipping that process. Terence’s humorous post digs into that a bit more.

I’m not sure if Google is actually doing anything nefarious here. It’s behind a flag, so I guess the point of that is to explore and research and stuff. Feels very similar to kv:storage to me, a “native module” much like a “native custom element.”

But we should be extra watchful about stuff like this. If any browser goes rogue and just starts shipping stuff, web standards is over. Life for devs gets a lot harder and the web gets a lot worse. The stakes are high. And it’s not going to happen overnight, it’s going to happen with little tiny things like this. Keep that blue beanie on.

Regarding the element itself, it’s always a bit surprising to me to see what gets legs as far as new HTML elements. Toasts just seem like a positioned <dialog> to me, but I haven’t participated in any research or anything. They are popular enough for Bootstrap to have ’em:

See the Pen
Bootstrap Toasts
by Chris Coyier (@chriscoyier)
on CodePen.

I would have guessed something like dropdown menus or tabs would have been the strongest contenders for “native” web components.


Toast originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/toast/feed/ 3 292113
Creating a Custom Element from Scratch https://css-tricks.com/creating-a-custom-element-from-scratch/ https://css-tricks.com/creating-a-custom-element-from-scratch/#comments Wed, 20 Mar 2019 12:25:08 +0000 http://css-tricks.com/?p=284480 In the last article, we got our hands dirty with Web Components by creating an HTML template that is in the document but not rendered until we need it.

Next up, we’re going to continue our quest to create …


Creating a Custom Element from Scratch originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
In the last article, we got our hands dirty with Web Components by creating an HTML template that is in the document but not rendered until we need it.

Next up, we’re going to continue our quest to create a custom element version of the dialog component below which currently only uses HTMLTemplateElement:

So let’s push ahead by creating a custom element that consumes our template#dialog-template element in real-time.

Article Series:

  1. An Introduction to Web Components
  2. Crafting Reusable HTML Templates
  3. Creating a Custom Element from Scratch (This post)
  4. Encapsulating Style and Structure with Shadow DOM
  5. Advanced Tooling for Web Components

Creating a custom element

The bread and butter of Web Components are custom elements. The customElements API gives us a path to define custom HTML tags that can be used in any document that contains the defining class.

Think of it like a React or Angular component (e.g. ), but without the React or Angular dependency. Native custom elements look like this: . More importantly, think of it as a standard element that can be used in your React, Angular, Vue, [insert-framework-you’re-interested-in-this-week] applications without much fuss.

Essentially, a custom element consists of two pieces: a tag name and a class that extends the built-in HTMLElement class. The most basic version of our custom element would look like this:

class OneDialog extends HTMLElement {
  connectedCallback() {
    this.innerHTML = `<h1>Hello, World!</h1>`;
  }
}

customElements.define('one-dialog', OneDialog);

Throughout a custom element, the this value is a reference to the custom element instance.

In the example above, we defined a new standards-compliant HTML element, <one-dialog></one-dialog>. It doesn’t do much… yet. For now, using the <one-dialog> tag in any HTML document will create a new element with an <h1> tag reading “Hello, World!”.

We are definitely going to want something more robust, and we’re in luck. In the last article, we looked at creating a template for our dialog and, since we will have access to that template, let’s utilize it in our custom element. We added a script tag in that example to do some dialog magic. let’s remove that for now since we’ll be moving our logic from the HTML template to inside the custom element class.

class OneDialog extends HTMLElement {
  connectedCallback() {
    const template = document.getElementById('one-dialog');
    const node = document.importNode(template.content, true);
    this.appendChild(node);
  }
}

Now, our custom element (<one-dialog>) is defined and the browser is instructed to render the content contained in the HTML template where the custom element is called.

Our next step is to move our logic into our component class.

Custom element lifecycle methods

Like React or Angular, custom elements have lifecycle methods. You’ve already been passively introduced to connectedCallback, which is called when our element gets added to the DOM.

The connectedCallback is separate from the element’s constructor. Whereas the constructor is used to set up the bare bones of the element, the connectedCallback is typically used for adding content to the element, setting up event listeners or otherwise initializing the component.

In fact, the constructor can’t be used to modify or manipulate the element’s attributes by design. If we were to create a new instance of our dialog using document.createElement, the constructor would be called. A consumer of the element would expect a simple node with no attributes or content inserted.

The createElement function has no options for configuring the element that will be returned. It stands to reason, then, that the constructor shouldn’t have the ability to modify the element that it creates. That leaves us with the connectedCallback as the place to modify our element.

With standard built-in elements, the element’s state is typically reflected by what attributes are present on the element and the values of those attributes. For our example, we’re going to look at exactly one attribute: [open]. In order to do this, we’ll need to watch for changes to that attribute and we’ll need attributeChangedCallback to do that. This second lifecycle method is called whenever one of the element constructor’s observedAttributes are updated.

That might sound intimidating, but the syntax is pretty simple:

class OneDialog extends HTMLElement {
  static get observedAttributes() {
    return ['open'];
  }
  
  attributeChangedCallback(attrName, oldValue, newValue) {
    if (newValue !== oldValue) {
      this[attrName] = this.hasAttribute(attrName);
    }
  }
  
  connectedCallback() {
    const template = document.getElementById('one-dialog');
    const node = document.importNode(template.content, true);
    this.appendChild(node);
  }
}

In our case above, we only care if the attribute is set or not, we don’t care about a value (this is similar to the HTML5 required attribute on inputs). When this attribute is updated, we update the element’s open property. A property exists on a JavaScript object whereas an attribute exists on an HTMLElement, this lifecycle method helps us keep the two in sync.

We wrap the updater inside the attributeChangedCallback inside a conditional checking to see if the new value and old value are equal. We do this to prevent an infinite loop inside our program because later we are going to create a property getter and setter that will keep the property and attributes in sync by setting the element’s attribute when the element’s property gets updated. The attributeChangedCallback does the inverse: updates the property when the attribute changes.

Now, an author can consume our component and the presence of the open attribute will dictate whether or not the dialog will be open by default. To make that a bit more dynamic, we can add custom getters and setters to our element’s open property:

class OneDialog extends HTMLElement {
  static get boundAttributes() {
    return ['open'];
  }
  
  attributeChangedCallback(attrName, oldValue, newValue) {
    this[attrName] = this.hasAttribute(attrName);
  }
  
  connectedCallback() {
    const template = document.getElementById('one-dialog');
    const node = document.importNode(template.content, true);
    this.appendChild(node);
  }
  
  get open() {
    return this.hasAttribute('open');
  }
  
  set open(isOpen) {
    if (isOpen) {
      this.setAttribute('open', true);
    } else {
      this.removeAttribute('open');
    }
  }
}

Our getter and setter will keep the open attribute (on the HTML element) and property (on the DOM object) values in sync. Adding the open attribute will set element.open to true and setting element.open to true will add the open attribute. We do this to make sure that our element’s state is reflected by its properties. This isn’t technically required, but is considered a best practice for authoring custom elements.

This does inevitably lead to a bit of boilerplate, but creating an abstract class that keeps these in sync is a fairly trivial task by looping over the observed attribute list and using Object.defineProperty.

class AbstractClass extends HTMLElement {
  constructor() {
    super();
    // Check to see if observedAttributes are defined and has length
    if (this.constructor.observedAttributes && this.constructor.observedAttributes.length) {
      // Loop through the observed attributes
      this.constructor.observedAttributes.forEach(attribute => {
        // Dynamically define the property getter/setter
        Object.defineProperty(this, attribute, {
          get() { return this.getAttribute(attribute); },
          set(attrValue) {
            if (attrValue) {
              this.setAttribute(attribute, attrValue);
            } else {
              this.removeAttribute(attribute);
            }
          }
        }
      });
    }
  }
}

// Instead of extending HTMLElement directly, we can now extend our AbstractClass
class SomeElement extends AbstractClass { /* Omitted */ }

customElements.define('some-element', SomeElement);

The above example isn’t perfect, it doesn’t take into account the possibility of attributes like open which don’t have a value assigned to them but rely only on the presence of the attribute. Making a perfect version of this would be beyond the scope of this article.

Now that we know whether or not our dialog is open, let’s add some logic to actually do the showing and hiding:

class OneDialog extends HTMLElement {  
  /** Omitted */
  constructor() {
    super();
    this.close = this.close.bind(this);
    this._watchEscape = this._watchEscape.bind(this);
  }
  
  set open(isOpen) {
    this.querySelector('.wrapper').classList.toggle('open', isOpen);
    this.querySelector('.wrapper').setAttribute('aria-hidden', !isOpen);
    if (isOpen) {
      this._wasFocused = document.activeElement;
      this.setAttribute('open', '');
      document.addEventListener('keydown', this._watchEscape);
      this.focus();
      this.querySelector('button').focus();
    } else {
      this._wasFocused && this._wasFocused.focus && this._wasFocused.focus();
      this.removeAttribute('open');
      document.removeEventListener('keydown', this._watchEscape);
      this.close();
    }
  }
  
  close() {
    if (this.open !== false) {
      this.open = false;
    }
    const closeEvent = new CustomEvent('dialog-closed');
    this.dispatchEvent(closeEvent);
  }
  
  _watchEscape(event) {
    if (event.key === 'Escape') {
        this.close();   
    }
  }
}

There’s a lot going on here, but let’s walk through it. The first thing we do is grab our wrapper and toggle the .open class based on isOpen. To keep our element accessible, we need to toggle the aria-hidden attribute as well.

If the dialog is open, then we want to save a reference to the previously-focused element. This is to account for accessibility standards. We also add a keydown listener to the document called watchEscape that we have bound to the element’s this in the constructor in a pattern similar to how React handles method calls in class components.

We do this not only to ensure the proper binding for this.close, but also because Function.prototype.bind returns an instance of the function with the bound call site. By saving a reference to the newly-bound method in the constructor, we’re able to then remove the event when the dialog is disconnected (more on that in a moment). We finish up by focusing on our element and setting the focus on the proper element in our shadow root.

We also create a nice little utility method for closing our dialog that dispatches a custom event alerting some listener that the dialog has been closed.

If the element is closed (i.e. !open), we check to make sure the this._wasFocused property is defined and has a focus method and call that to return the user’s focus back to the regular DOM. Then we remove our event listener to avoid any memory leaks.

Speaking of cleaning up after ourselves, that takes us to yet another lifecycle method: disconnectedCallback. The disconnectedCallback is the inverse of the connectedCallback in that the method is called once the element is removed from the DOM and allows us to clean up any event listeners or MutationObservers attached to our element.

It just so happens we have a few more event listeners to wire up:

class OneDialog extends HTMLElement {
  /* Omitted */
  
  connectedCallback() {    
    this.querySelector('button').addEventListener('click', this.close);
    this.querySelector('.overlay').addEventListener('click', this.close);
  }
  
  disconnectedCallback() {
    this.querySelector('button').removeEventListener('click', this.close);
    this.querySelector('.overlay').removeEventListener('click', this.close);
  }  
}

Now we have a well-functioning, mostly accessible dialog element. There are a few bits of polish we can do, like capturing focus on the element, but that’s outside the scope of what we’re trying to learn here.

There is one more lifecycle method that doesn’t apply to our element, the adoptedCallback, which fires when the element is adopted into another part of the DOM.

In the following example, you will now see that our template element is being consumed by a standard <one-dialog> element.

Another thing: non-presentational components

The <one-template> we have created so far is a typical custom element in that it includes markup and behavior that gets inserted into the document when the element is included. However, not all elements need to render visually. In the React ecosystem, components are often used to manage application state or some other major functionality, like <Provider /> in react-redux.

Let’s imagine for a moment that our component is part of a series of dialogs in a workflow. As one dialog is closed, the next one should open. We could make a wrapper component that listens for our dialog-closed event and progresses through the workflow.

class DialogWorkflow extends HTMLElement {
  connectedCallback() {
    this._onDialogClosed = this._onDialogClosed.bind(this);
    this.addEventListener('dialog-closed', this._onDialogClosed);
  }

  get dialogs() {
    return Array.from(this.querySelectorAll('one-dialog'));
  }

  _onDialogClosed(event) {
    const dialogClosed = event.target;
    const nextIndex = this.dialogs.indexOf(dialogClosed);
    if (nextIndex !== -1) {
      this.dialogs[nextIndex].open = true;
    }
  }
}

This element doesn’t have any presentational logic, but serves as a controller for application state. With a little effort, we could recreate a Redux-like state management system using nothing but a custom element that could manage an entire application’s state in the same one that React’s Redux wrapper does.

That’s a deeper look at custom elements

Now we have a pretty good understanding of custom elements and our dialog is starting to come together. But it still has some problems.

Notice that we’ve had to add some CSS to restyle the dialog button because our element’s styles are interfering with the rest of the page. While we could utilize naming strategies (like BEM) to ensure our styles won’t create conflicts with other components, there is a more friendly way of isolating styles. Spoiler! It’s shadow DOM and that’s what we’re going to look at in the next part of this series on Web Components.

Another thing we need to do is define a new template for every component or find some way to switch templates for our dialog. As it stands, there can only be one dialog type per page because the template that it uses must always be present. So either we need some way to inject dynamic content or a way to swap templates.

In the next article, we will look at ways to increase the usability of the <one-dialog> element we just created by incorporating style and content encapsulation using the shadow DOM.

Article Series:

  1. An Introduction to Web Components
  2. Crafting Reusable HTML Templates
  3. Creating a Custom Element from Scratch (This post)
  4. Encapsulating Style and Structure with Shadow DOM
  5. Advanced Tooling for Web Components

Creating a Custom Element from Scratch originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/creating-a-custom-element-from-scratch/feed/ 6 284480
An Introduction to Web Components https://css-tricks.com/an-introduction-to-web-components/ https://css-tricks.com/an-introduction-to-web-components/#comments Mon, 18 Mar 2019 12:47:46 +0000 http://css-tricks.com/?p=284470 Front-end development moves at a break-neck pace. This is made evident by the myriad articles, tutorials, and Twitter threads bemoaning the state of what once was a fairly simple tech stack. In this article, I’ll discuss why Web Components are …


An Introduction to Web Components originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Front-end development moves at a break-neck pace. This is made evident by the myriad articles, tutorials, and Twitter threads bemoaning the state of what once was a fairly simple tech stack. In this article, I’ll discuss why Web Components are a great tool to deliver high-quality user experiences without complicated frameworks or build steps and that don’t run the risk of becoming obsolete. In subsequent articles of this five-part series, we will dive deeper into each of the specifications.

This series assumes a basic understanding of HTML, CSS, and JavaScript. If you feel weak in one of those areas, don’t worry, building a custom element actually simplifies many complexities in front-end development.

Article Series:

  1. An Introduction to Web Components (This post)
  2. Crafting Reusable HTML Templates
  3. Creating a Custom Element from Scratch
  4. Encapsulating Style and Structure with Shadow DOM
  5. Advanced Tooling for Web Components

What are Web Components, anyway?

Web Components consist of three separate technologies that are used together:

  1. Custom Elements. Quite simply, these are fully-valid HTML elements with custom templates, behaviors and tag names (e.g. <one-dialog>) made with a set of JavaScript APIs. Custom Elements are defined in the HTML Living Standard specification.
  2. Shadow DOM. Capable of isolating CSS and JavaScript, almost like an <iframe>. This is defined in the Living Standard DOM specification.
  3. HTML templates. User-defined templates in HTML that aren’t rendered until called upon. The <template> tag is defined in the HTML Living Standard specification.

These are what make up the Web Components specification.

HTML Modules is likely to be the fourth technology in the stack, but it has yet to be implemented in any of the big four browsers. The Chrome team has announced it an intent to implement them in a future release.

Web Components are generally available in all of the major browsers with the exception of Microsoft Edge and Internet Explorer 11, but polyfills exist to fill in those gaps.

Referring to any of these as Web Components is technically accurate because the term itself is a bit overloaded. As a result, each of the technologies can be used independently or combined with any of the others. In other words, they are not mutually exclusive.

Let’s take a quick look at each of those first three. We’ll dive deeper into them in other articles in this series.

Custom elements

As the name implies, custom elements are HTML elements, like <div>, <section> or <article>, but something we can name ourselves that are defined via a browser API. Custom elements are just like those standard HTML elements — names in angle brackets — except they always have a dash in them, like <news-slider> or <bacon-cheeseburger>. Going forward, browser vendors have committed not to create new built-in elements containing a dash in their names to prevent conflicts.

Custom elements contain their own semantics, behaviors, markup and can be shared across frameworks and browsers.

class MyComponent extends HTMLElement {
  connectedCallback() {
    this.innerHTML = `<h1>Hello world</h1>`;
  }
}
    
customElements.define('my-component', MyComponent);

See the Pen
Custom elements demo
by Caleb Williams (@calebdwilliams)
on CodePen.

In this example, we define <my-component>, our very own HTML element. Admittedly, it doesn’t do much, however this is the basic building block of a custom element. All custom elements must in some way extend an HTMLElement in order to be registered with the browser.

Custom elements exist without third-party frameworks and the browser vendors are dedicated to the continued backward compatibility of the spec, all but guaranteeing that components written according to the specifications will not suffer from breaking API changes. What’s more, these components can generally be used out-of-the-box with today’s most popular frameworks, including Angular, React, Vue, and others with minimal effort.

Shadow DOM

The shadow DOM is an encapsulated version of the DOM. This allows authors to effectively isolate DOM fragments from one another, including anything that could be used as a CSS selector and the styles associated with them. Generally, any content inside of the document’s scope is referred to as the light DOM, and anything inside a shadow root is referred to as the shadow DOM.

When using the light DOM, an element can be selected by using document.querySelector('selector') or by targeting any element’s children by using element.querySelector('selector'); in the same way, a shadow root’s children can be targeted by calling shadowRoot.querySelector where shadowRoot is a reference to the document fragment — the difference being that the shadow root’s children will not be select-able from the light DOM. For example, If we have a shadow root with a <button> inside of it, calling shadowRoot.querySelector('button') would return our button, but no invocation of the document’s query selector will return that element because it belongs to a different DocumentOrShadowRoot instance. Style selectors work in the same way.

In this respect, the shadow DOM works sort of like an <iframe> where the content is cut off from the rest of the document; however, when we create a shadow root, we still have total control over that part of our page, but scoped to a context. This is what we call encapsulation.

If you’ve ever written a component that reuses the same id or relies on either CSS-in-JS tools or CSS naming strategies (like BEM), shadow DOM has the potential to improve your developer experience.

Imagine the following scenario:

<div>
  <div id="example">
    <!-- Pseudo-code used to designate a shadow root -->
    <#shadow-root>
      <style>
      button {
        background: tomato;
        color: white;
      }
      </style>
      <button id="button">This will use the CSS background tomato</button>
    </#shadow-root>
  </div>
  <button id="button">Not tomato</button>
</div>

Aside from the pseudo-code of <#shadow-root> (which is used here to demarcate the shadow boundary which has no HTML element), the HTML is fully valid. To attach a shadow root to the node above, we would run something like:

const shadowRoot = document.getElementById('example').attachShadow({ mode: 'open' });
shadowRoot.innerHTML = `<style>
button {
  color: tomato;
}
</style>
<button id="button">This will use the CSS color tomato <slot></slot></button>`;

A shadow root can also include content from its containing document by using the <slot> element. Using a slot will drop user content from the outer document at a designated spot in your shadow root.

See the Pen
Shadow DOM style encapsulation demo
by Caleb Williams (@calebdwilliams)
on CodePen.

HTML templates

The aptly-named HTML <template> element allows us to stamp out re-usable templates of code inside a normal HTML flow that won’t be immediately rendered, but can be used at a later time.

<template id="book-template">
  <li><span class="title"></span> &mdash; <span class="author"></span></li>
</template>

<ul id="books"></ul>

The example above wouldn’t render any content until a script has consumed the template, instantiated the code and told the browser what to do with it.

const fragment = document.getElementById('book-template');
const books = [
  { title: 'The Great Gatsby', author: 'F. Scott Fitzgerald' },
  { title: 'A Farewell to Arms', author: 'Ernest Hemingway' },
  { title: 'Catch 22', author: 'Joseph Heller' }
];

books.forEach(book => {
  // Create an instance of the template content
  const instance = document.importNode(fragment.content, true);
  // Add relevant content to the template
  instance.querySelector('.title').innerHTML = book.title;
  instance.querySelector('.author').innerHTML = book.author;
  // Append the instance ot the DOM
  document.getElementById('books').appendChild(instance);
});

Notice that this example creates a template (<template id="book-template">) without any other Web Components technology, illustrating again that the three technologies in the stack can be used independently or collectively.

Ostensibly, the consumer of a service that utilizes the template API could write a template of any shape or structure that could be created at a later time. Another page on a site might use the same service, but structure the template this way:

<template id="book-template">
  <li><span class="author"></span>'s classic novel <span class="title"></span></li>
</template>

<ul id="books"></ul>

See the Pen
Template example
by Caleb Williams (@calebdwilliams)
on CodePen.

That wraps up our introduction to Web Components

As web development continues to become more and more complicated, it will begin to make sense for developers like us to begin deferring more and more development to the web platform itself which has continued to mature. The Web Components specifications are a set of low-level APIs that will continue to grow and evolve as our needs as developers evolve.

In the next article, we will take a deeper look at the HTML templates part of this. Then, we’ll follow that up with a discussion of custom elements and shadow DOM. Finally, we’ll wrap it all up by looking at higher-level tooling and incorporation with today’s popular libraries and frameworks.

Article Series:

  1. An Introduction to Web Components (This post)
  2. Crafting Reusable HTML Templates
  3. Creating a Custom Element from Scratch
  4. Encapsulating Style and Structure with Shadow DOM
  5. Advanced Tooling for Web Components

An Introduction to Web Components originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/an-introduction-to-web-components/feed/ 19 284470
A Guide to Custom Elements for React Developers https://css-tricks.com/a-guide-to-custom-elements-for-react-developers/ https://css-tricks.com/a-guide-to-custom-elements-for-react-developers/#comments Thu, 08 Nov 2018 14:59:54 +0000 http://css-tricks.com/?p=278134 I had to build a UI recently and (for the first time in a long while) I didn’t have the option of using React.js, which is my preferred solution for UI these days. So, I looked at what the built-in …


A Guide to Custom Elements for React Developers originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
I had to build a UI recently and (for the first time in a long while) I didn’t have the option of using React.js, which is my preferred solution for UI these days. So, I looked at what the built-in browser APIs had to offer and saw that using custom elements (aka Web Components) may just be the remedy that this React developer needed.

Custom elements can offer the same general benefits of React components without being tied to a specific framework implementation. A custom element gives us a new HTML tag that we can programmatically control through a native browser API.

Let’s talk about the benefits of component-based UI:

  • Encapsulation – concerns scoped to that component remain in that component’s implementation
  • Reusability – when the UI is separated into more generic pieces, they’re easier to break into patterns that you’re more likely to repeat
  • Isolation – because components are designed to be encapsulated and with that, you get the added benefit of isolation, which allows you scope bugs and changes to a particular part of your application easier

Use cases

You might be wondering who is using custom elements in production. Notably:

  • GitHub is using custom elements for their modal dialogs, autocomplete and display time.
  • YouTube’s new web app is built with Polymer and web components.

Similarities to the Component API

When trying to compare React Components versus custom elements, I found the APIs really similar:

  • They’re both classes that aren’t “new” and are able that extend a base class
  • They both inherit a mounting or rendering lifecycle
  • They both take static or dynamic input via props or attributes

Demo

So, let’s build a tiny application that lists details about a GitHub repository.

Screenshot of end result

If I were going to approach this with React, I would define a simple component like this:

<Repository name="charliewilco/obsidian" />

This component takes a single prop — the name of the repository — and we implement it like this:

class Repository extends React.Component {
  state = {
    repo: null
  };

  async getDetails(name) {
    return await fetch(`https://api.github.com/repos/${name}`, {
      mode: 'cors'
    }).then(res => res.json());
  }

  async componentDidMount() {
    const { name } = this.props;
    const repo = await this.getDetails(name);
    this.setState({ repo });
  }

  render() {
    const { repo } = this.state;

    if (!repo) {
      return <h1>Loading</h1>;
    }

    if (repo.message) {
      return <div className="Card Card--error">Error: {repo.message}</div>;
    }

    return (
      <div class="Card">
        <aside>
          <img
            width="48"
            height="48"
            class="Avatar"
            src={repo.owner.avatar_url}
            alt="Profile picture for ${repo.owner.login}"
          />
        </aside>
        <header>
          <h2 class="Card__title">{repo.full_name}</h2>
          <span class="Card__meta">{repo.description}</span>
        </header>
      </div>
    );
  }
}

See the Pen React Demo – GitHub by Charles (@charliewilco) on CodePen.

To break this down further, we have a component that has its own state, which is the repo details. Initially, we set it to be null because we don’t have any of that data yet, so we’ll have a loading indicator while the data is fetched.

During the React lifecycle, we’ll use fetch to go get the data from GitHub, set up the card, and trigger a re-render with setState() after we get the data back. All of these different states the UI takes are represented in the render() method.

Defining / Using a Custom Element

Doing this with custom elements is a little different. Like the React component, our custom element will take a single attribute — again, the name of the repository — and manage its own state.

Our element will look like this:

<github-repo name="charliewilco/obsidian"></github-repo>
<github-repo name="charliewilco/level.css"></github-repo>
<github-repo name="charliewilco/react-branches"></github-repo>
<github-repo name="charliewilco/react-gluejar"></github-repo>
<github-repo name="charliewilco/dotfiles"></github-repo>

See the Pen Custom Elements Demo – GitHub by Charles (@charliewilco) on CodePen.

To start, all we need to do to define and register a custom element is create a class that extends the HTMLElement class and then register the name of the element with customElements.define().

class OurCustomElement extends HTMLElement {}
window.customElements.define('our-element', OurCustomElement);

And we can call it:

<our-element></our-element>

This new element isn’t very useful, but with custom elements, we get three methods to expand the functionality of this element. These are almost analogous to React’s lifecycle methods for their Component API. The two lifecycle-like methods most relevant to us are the disconnectedCallBack and the connectedCallback and since this is a class, it comes with a constructor.

Name Called when
constructor An instance of the element is created or upgraded. Useful for initializing state, settings up event listeners, or creating Shadow DOM. See the spec for restrictions on what you can do in the constructor.
connectedCallback The element is inserted into the DOM. Useful for running setup code, such as fetching resources or rendering UI. Generally, you should try to delay work until this time
disconnectedCallback When the element is removed from the DOM. Useful for running clean-up code.

To implement our custom element, we’ll create the class and set up some attributes related to that UI:

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

    this.repoDetails = null;

    this.name = this.getAttribute("name");
    this.endpoint = `https://api.github.com/repos/${this.name}`    
    this.innerHTML = `<h1>Loading</h1>`
  }
}

By calling super() in our constructor, the context of this is the element itself and all the DOM manipulation APIs can be used. So far, we’ve set the default repository details to null, gotten the repo name from element’s attribute, created an endpoint to call so we don’t have to define it later and, most importantly, set the initial HTML to be a loading indicator.

In order to get the details about that element’s repository, we’re going to need to make a request to GitHub’s API. We’ll use fetch and, since that’s Promise-based, we’ll use async and await to make our code more readable. You can learn more about the async/await keywords here and more about the browser’s fetch API here. You can also tweet at me to find out whether I prefer it to the Axios library. (Hint, it depends if I had tea or coffee with my breakfast.)

Now, let’s add a method to this class to ask GitHub for details about the repository.

class Repository extends HTMLElement {
  constructor() {
    // ...
  }

  async getDetails() {
    return await fetch(this.endpoint, { mode: "cors" }).then(res => res.json());
  }
}

Next, let’s use the connectedCallback method and the Shadow DOM to use the return value from this method. Using this method will do something similar as when we called Repository.componentDidMount() in the React example. Instead, we’ll override the null value we initially gave this.repoDetails — we’ll use this later when we start to call the template to create the HTML.

class Repository extends HTMLElement {
  constructor() {
    // ...
  }

  async getDetails() {
    // ...
  }

  async connectedCallback() {
    let repo = await this.getDetails();
    this.repoDetails = repo;
    this.initShadowDOM();
  }

  initShadowDOM() {
    let shadowRoot = this.attachShadow({ mode: "open" });
    shadowRoot.innerHTML = this.template;
  }
}

You’ll notice that we’re calling methods related to the Shadow DOM. Besides being a rejected title for a Marvel movie, the Shadow DOM has its own rich API worth looking into. For our purposes, though, it’s going to abstract the implementation of adding innerHTML to the element.

Now we’re assigning the innerHTML to be equal to the value of this.template. Let’s define that now:

class Repository extends HTMLElement {
  get template() {
    const repo = this.repoDetails;
  
    // if we get an error message let's show that back to the user
    if (repo.message) {
      return `<div class="Card Card--error">Error: ${repo.message}</div>`
    } else {
      return `
      <div class="Card">
        <aside>
          <img width="48" height="48" class="Avatar" src="${repo.owner.avatar_url}" alt="Profile picture for ${repo.owner.login}" />
        </aside>
        <header>
          <h2 class="Card__title">${repo.full_name}</h2>
          <span class="Card__meta">${repo.description}</span>
        </header>
      </div>
      `
    }
  }
}

That’s pretty much it. We’ve defined a custom element that manages its own state, fetches its own data, and reflects that state back to the user while giving us an HTML element to use in our application.

After going through this exercise, I found that the only required dependency for custom elements is the browser’s native APIs rather than a framework to additionally parse and execute. This makes for a more portable and reusable solution with similar APIs to the frameworks you already love and use to make your living.

There are drawbacks of using this approach, of course. We’re talking about various browser support issues and some lack of consistency. Plus, working with DOM manipulation APIs can be very confusing. Sometimes they are assignments. Sometimes they are functions. Sometimes those functions take a callback and sometimes they don’t. If you don’t believe me, take a look at adding a class to an HTML element created via document.createElement(), which is one of the top five reasons to use React. The basic implementation isn’t that complicated but it is inconsistent with other similar document methods.

The real question is: does it even out in the wash? Maybe. React is still pretty good at the things it’s designed to be very very good at: the virtual DOM, managing application state, encapsulation, and passing data down the tree. There’s next to no incentive to use custom elements inside that framework. Custom elements, on the other hand, are simply available by virtue of building an application for the browser.

Learn more


A Guide to Custom Elements for React Developers originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/a-guide-to-custom-elements-for-react-developers/feed/ 3 278134