question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Discussion: Web Components might be the right tool and save you time

See original GitHub issue

Hi! First thing thanks for all great Tailwindlabs tools and I apologize if this is not the right channel, feel free to move this discussion or close it.

I am more of a backend guy but in my, admittedly low, knowledge of Web Components I think they might be the right tool to use for headlessui.

Quoting Adam

Right now just React and Vue, but we would like to explore other frameworks eventually. Just a lot of work to build a library like this for even one framework, have to start somewhere!

I totally understand that you already use Vue and React for your projects and do not want to invest too much time in other technologies but I really think building Web Components with Stenciljs is going to be pretty straight forward for you since it uses TypeScript and JSX and it will save you time since you only need to write once. I have not worked previously with Stenciljs, TypeScript or React but I was able to port the switch component in half a day.

What would be the benefits of using Web Components instead of framework specific ones?

  • One codebase that works with any framework, a thin wrapper might be needed.
  • Can use components without any framework, drop them in your html and apply what’s needed with alpine.js or vanilla javascript.
  • IMHO code looks easier to read

I am no expert in front end development so in the next days I’m going to publish the code for everyone to check and find possible shortcomes. Hopefully people more knowledgeable than me can contribute in the discussion.

Use with Alpine.js Alpine.js component needs to be initialized after webcomponent has been rendered the first time

Click to show code
<div class="mt-6" x-data-defer="{ isChecked: true }">
  <h-switch
    class="flex items-center space-x-4"
    :checked="isChecked"
    @switched="isChecked = !isChecked"
  >
    <label label>Enable notifications</label>
    <button
      button
      class="relative inline-flex flex-shrink-0 h-6 transition-colors duration-200 ease-in-out border-2 border-transparent rounded-full cursor-pointer w-11 focus:outline-none focus:ring"
      :class="[ isChecked ? 'bg-indigo-600' : 'bg-gray-200' ]"
    >
      <span
        class="inline-block w-5 h-5 transition duration-200 ease-in-out transform bg-white rounded-full"
        :class="[ isChecked ? 'translate-x-5' : 'translate-x-0' ]"
      ></span>
    </button>
  </h-switch>
</div>

<script>
  const components = document.querySelectorAll("[x-data-defer]");
  components.forEach(comp => {
    comp.addEventListener("componentRendered", function handleConnected() {
      comp.setAttribute("x-data", comp.getAttribute("x-data-defer"));
      Alpine.initializeComponent(comp);
      comp.removeEventListener("componentRendered", handleConnected);
    });
  });
</script>

Use with Vue.js (vue-cli) Vue.js needs to be configured to recognize custom elements

Click to show code
//vue.config.js
const vueConfig = {};

vueConfig.chainWebpack = config => {
  config.module
    .rule("vue")
    .use("vue-loader")
    .loader("vue-loader")
    .tap(options => {
      options.compilerOptions = {
        ...(options.compilerOptions || {}),
        isCustomElement: tag => /^h-/.test(tag),
      };
      return options;
    });
};

module.exports = vueConfig;
// main.js
import { defineCustomElements } from "headlessui-webcomponents/dist/esm/loader";

// Bind the custom elements to the window object
defineCustomElements();
// CustomSwitch.vue
<template>
  <div>
    <h-switch
      class="flex items-center space-x-4"
      :checked="modelValue"
      @switched="this.$emit('update:modelValue', $event.detail)"
    >
      <label label>{{ label }}</label>
      <button
        button
        class="w-11 focus:outline-none focus:ring relative inline-flex flex-shrink-0 h-6 transition-colors duration-200 ease-in-out border-2 border-transparent rounded-full cursor-pointer"
        :class="[modelValue ? 'bg-indigo-600' : 'bg-gray-200']"
      >
        <span
          class="inline-block w-5 h-5 transition duration-200 ease-in-out transform bg-white rounded-full"
          :class="[modelValue ? 'translate-x-5' : 'translate-x-0']"
        ></span>
      </button>
    </h-switch>
  </div>
</template>

<script>
export default {
  name: "CustomSwitch",
  emits: ["update:modelValue"],
  props: {
    modelValue: { type: Boolean, default: false },
    label: { type: String, default: "" },
  },
};
</script>

Use with React I did not try with React but there are extensive instructions in Stencilejs website and Stencilejs should also be able to output React components that wrap the webcomponent. https://stenciljs.com/docs/react

Use with Angular I did not try with Angular but there are extensive instructions in Stencilejs website and Stencilejs. https://stenciljs.com/docs/angular

Use with Ember I did not try with Ember but there are extensive instructions in Stencilejs website and Stencilejs. https://stenciljs.com/docs/ember

Implementation with Stenciljs This is the switch component ported as webcomponent

Click to show code
import {
  Component,
  Element,
  Event,
  EventEmitter,
  h,
  Prop,
  State,
} from "@stencil/core";
import { Keys } from "../../utils/keyboard";
import { useId } from "../../hooks/use-id";

@Component({
  tag: "h-switch",
  shadow: false,
})
export class HSwitch {
  @Element() host: Element;

  /**
   * The status of checkbox
   */
  @Prop() checked: boolean = false;

  /**
   * Event emitted on status change
   */
  @Event({ bubbles: false }) switched: EventEmitter<boolean>;

  /**
   * Event emitted after component is rendered
   */
  @Event() componentRendered: EventEmitter;

  @State() button: HTMLButtonElement;
  @State() label: HTMLLabelElement;
  @State() id: number;

  // This is called only first time
  componentWillLoad() {
    this.id = useId();
    this.label = this.host.querySelector("[label]");
    this.button = this.host.querySelector("[button]");

    if (this.label) {
      this.label.id = "headlessui-switch-label-" + this.id;
      this.label.addEventListener("click", () => this.handleLabelClick());
    }

    this.button.id = "headlessui-switch-" + this.id;

    if (this.button.tagName === "BUTTON") {
      this.button.tabIndex = 0;
      this.label && this.button.setAttribute("aria-labelledby", this.label.id);
      this.button.setAttribute("role", "switch");
      this.button.addEventListener("click", e => this.handleClick(e));
      this.button.addEventListener("keyUp", e => this.handleKeyUp(e));
      this.button.addEventListener("keyPress", e => this.handleKeyPress(e));
    }
  }

  render() {
    this.button.setAttribute("aria-checked", this.checked.toString());

    return <slot></slot>;
  }

  componentDidRender() {
    this.componentRendered.emit();
  }

  private handleLabelClick() {
    this.button?.click();
    this.button?.focus({ preventScroll: true });
  }

  private handleClick(event) {
    event.preventDefault();
    this.toggle();
  }

  private handleKeyUp(event) {
    if (event.key !== Keys.Tab) event.preventDefault();
    if (event.key === Keys.Space) this.toggle();
  }

  private handleKeyPress(event) {
    event.preventDefault();
  }

  private toggle() {
    this.switched.emit(!this.checked);
  }
}

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Reactions:15
  • Comments:5

github_iconTop GitHub Comments

12reactions
MrCrayoncommented, Feb 6, 2021

Thanks for your reply! Naturally I respect your decision, I will keep exploring Web Components in my spare time hoping this might be useful in the future. I published my code here https://github.com/MrCrayon/headlessui-webcomponents so hopefully someone else can join in the effort and we can discover if it’s really doable.

I think it would be nice if we could keep the conversation here for who is interested to use headlessui in frameworks not yet supported but if you think receiving notifications from this issue is going to distract you right now just say the word I already opened the Discussions section in my repo.

About your specific issues for Web Components:

Accessibility Did you have anything specific in mind? I did not find anything that is not related to shadow DOM (that we don’t need) or that is not common with React or Vue components too.

Children/slots are always rendered Yes but I only used the default slot, basically what the user writes is what is rendered plus the functionalities that the web component adds. Switch component was the easier one so I’ll try to understand if this can be done also with the other components.

Children/slots require an additional runtime library to write them Not sure what you mean with that, if you write components with React or Stenciljs you are switching library it’s not additional.

Decorators I am too much of a newbe about mantaining packages to understand this. When you run stenciljs compiler the end result is going to be some vanilla javascript files. Most of the people will just need instructions on how to use them in their application and are not going to care if the web component was built with Typescript and decorators that btw are optional.

Glue code To be honest, Alpinejs only needs to be initialized at the right time and for Vue, once is setup, you just need to create a component with your custom markup and style and that what’s expected even using headlessui-vue package. React is another story because of the way it manages attributes and events but that’s hardly a Web Component fault and stencilejs is able to automatically do the wrapping and output React components.

Thanks and have a nice day!

2reactions
xt0rtedcommented, Jan 29, 2021

I’ve been working on a web components version for a couple of my projects, but recent events caused me to have to put it on hold. It’s not public right now since it’s mostly a proof of concept, but the current working version is on that page.

When github/catalyst was open sourced I started to replace some of my custom code with it and plan on looking into github/template-parts and github/jtml where appropriate. My hope is those will be application level dependencies and not needed for the component library.

I’m hoping to pick this back up next week and get the transition to catalyst finished up so I can make the repo public to get some feedback. I’ll be sure to update this issue when I do.

Read more comments on GitHub >

github_iconTop Results From Across the Web

The Flaws Of Web Components (And Possible Solutions)
Learn about possible problems with Web Components and how to solve them in Manuel Rauber's article. Third part of his Web Component article ......
Read more >
Web Components Are Easier Than You Think | CSS-Tricks
A thousand lines of JavaScript to save four lines of HTML. ... I'm here to tell you that you—yes, you—can create a web...
Read more >
Announcing Open Web Components - DEV Community ‍ ‍
We believe web components provide a standards-based solution to problems like reusability, interopability, and encapsulation. Furthermore, we ...
Read more >
The Ultimate Guide to Web Components
Welcome to The Ultimate Guide to Web Components blog series! We're going to discuss the state of Web Components, help unravel best practices ......
Read more >
Editor support for WebComponents · Issue #776 - GitHub
I think we should leave this discussion to each framework. As long as they generate JSON of a specific format, VS Code can...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found