-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Description
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);
}
}