Ucom is a buildless declarative custom element framework. It comes in three flavors:
- ucom (
20.0kminified) (7.8kgzipped) - ucom_vue (
28.8kminified) (11.9kgzipped) - ucom_lite (
7.1kminified) (3.1kgzipped)
Notice This technology is currently unversioned. An alpha version should be arriving before Spring.
<script src="/js/ucom.js" type="module"></script>Create a template with a u-com attribute. It will be registered as a custom element using shadow dom. You can then use that element normally anywhere on your page.
<template u-com="my-component">
Now we're starting to get serious.
</template>
<!-- Use the custom element normally anywhere, even within another framework. -->
<my-component><my-component>For the event lifecycle use the element callbacks attributeChangedCallback, connectedCallback, disconnectedCallback.
<template u-com="my-component">
<script>
// This function will be merged into the custom element prototype.
export function connectedCallback() {
console.log('Element has been added to the DOM.')
}
export default class extends HTMLElement {
disconnectedCallback() {
console.log('Element has been removed from the DOM.')
}
}
</script>
</template>Create an immediately executed component app with an empty u-com attribute.
<template u-com>
<source src="/components/my-component.ucom/">
This is an inline, self-instantiated app.
You can use this to bootstrap, to create layouts, or to write quick one-offs.
<my-component></my-component>
</template>You can use the source tag inside of any component to declaratively import component dependencies.
<template u-com="my-component">
<source src="/components/other-component.html">
<header>Content</header>
<other-component><other-component>
</template><!-- /components/other-component.html -->
<div>This is another component.</div>You may use an importmap for component paths. All paths are resolved with import.meta.resolve.
<script type="importmap">{
"imports": {
"@com/": "/components/"
}
}</script>
<template u-com>
<source src="@com/widget-component.html">
<source src="@com/second-component.html">
<widget-component></widget-component>
<second-component></second-component>
</template>Use a base element to reduce redundancy. Settings on base are sticky until the next base.
<script type="importmap">{
"imports": {
"@com/": "/components/"
}
}</script>
<template u-com>
<base href="@com/">
<source src="widget-component.html">
<source src="second-component.html">
<widget-component></widget-component>
<second-component></second-component>
</template>Components can be imported lazily by adding the "lazy" attribute to either a source or base element.
<script type="importmap">{
"imports": {
"@com/": "/components/"
}
}</script>
<template u-com>
<base href="@com/" lazy>
<source src="widget-component.html">
<source src="second-component.html">
<widget-component></widget-component>
<second-component></second-component>
</template>Components can be specified as directory by using suffix .ucom. This is useful for bundling libraries, themes and data files into an easily redistributable component.
<template u-com>
<!-- To specify a component directory end with .ucom -->
<source src="/components/complex-component.ucom">
</template>The component will be a .html file with the same name as the .ucom directory.
@com/complex-component.ucom/
+-- complex-component.html
+-- important_library.js
+-- data_theme.css
+-- field_data.json
<!-- /components/complex-component.ucom/complex-component.html -->
<header>This component does a lot of complicated things.</header>
<main></main>
<script>
import lib from './important_library.js'
import data from './field_data.json' with { type: 'json' }
export function connectedCallback() {
const main = this.$('main')
lib.load(data)
lib.bind(main)
}
</script>
<style>
/* This CSS will not escape the shadow dom */
@import "./data_theme.css";
</style>Element IDs are sandboxed via shadow dom.
<template u-com>
<header id="header">Stylized Component</header>
<main>Component Body</main>
<style>
#header {
color: green;
}
main {
color: blue;
}
</style>
</template>You can "pierce" the shadow dom of a component with themes by adding the u-com attribute to the style or link element. Ucom will add this styling to all custom elements by adding it to its adoptedStyleSheets.
You may also use a dynamic CSS @import within the style tag of each of your components.
<!DOCTYPE html>
<head>
<!-- u-com on a link or style will cause it to be attached to all components "piercing" it. -->
<link u-com rel="stylesheet" href="/style/bootstrap-5.3.2.min.css">
<style u-com>
@import url("/style/test.css");
</style>
</head>Ucom has Alpine/Vue style templating, with the u- prefix.
Directives include; u-show, u-for, u-bind, u-html, u-on, u-ref, u-text, u-is, u-data.
<template u-com>
<div u-for="n in 10" u-on:click="alert(n)" u-text="n"></div>
</template>Use Vue style shortcuts. $ is short for u-text:, @ is short for u-on:
<template u-com>
<div u-for="n in 10" @click="alert(n)" $n></div>
</template>While displaying text a meta void element tag is converted to span. This is fine because the shadow root of the web component separates it from the main HTML document.
This was chosen as a compromise between the ugly verbose Alpine style and the more complicated Vue style. This way we don't need to parse text nodes with complicated regular expressions.
<template u-com>
<!-- The ugly Alpine style way -->
Exponential:
<div u-for="n in 5">
<span u-text="n ** 1"></span>, <span u-text="n ** 2"></span>, <span u-text="n ** 3"></span>
</div>
<!-- The pretty Ucom way -->
Exponential:
<div u-for="n in 5">
<meta $n=>, <meta $="n**2">, <meta $="n**3">
</div>
<!-- Precalculate using param -->
Exponential:
<div u-for="n in 5">
<param $n1="n**1">
<param $n2="n**2">
<param $n3="n**3">
<meta $n1>, <meta $n2>, <meta $n3>
</div>
</template>You can use the store to gain access to reactive data.
<template u-com>
<button @click="count++"><meta $count> times</button>
<div>Double it <meta $double></div>
<script>
export function $store({computed}) {
return {
count: 0,
double: computed($d => $d.count * 2),
}
}
export function connectedCallback() {
this.$effect(() => console.log('count: ', this.$data.count))
this.$effect(() => console.log('double: ', this.$data.double))
}
</script>
</template>You can also declare reactive data at its current context / block level by using a param tag. This style can be mixed with the $store export from the script.
<template u-com>
<param $count="5">
<button @click="count++"><meta $count> times</button>
</template>If you wrap a store value with the synced and persisted function then it will gain some features.
<template u-com>
<!-- Normal store counter -->
<button @click="normal++"><meta $normal> times</button>
<!-- Changes to this counter will be syncronized across all elements of the same name. -->
<button @click="sync++"><meta $sync> times</button>
<!-- This counter will be both syncronized and persisted across all instances of this element -->
<!-- of the same name (and page refreshes of this self-instantiated custom element) -->
<button @click="persist++"><meta $persist> times</button>
<div>You have clicked a total of <meta $total> times</div>
<script>
export function $store({computed, persisted, synced}) {
return {
normal: 0,
persist: persisted(0),
sync: synced(0),
total: computed($d => $d.normal + $d.persist + $d.sync),
}
}
</script>
</template>It's easy to add js properties and html attributes to your components.
<my-counter count="5"></my-counter>
<template u-com="my-counter">
<button @click="count++"><meta $count> times</button>
<script>
export function $props() {
return {
count: {
default: 0,
cast: parseInt,
},
}
}
</script>
</template>u-is allows for a dynamic tag to be defined based upon a store value.
<dyn-amic name="my-other-component"></dyn-amic>
<template u-com="dyn-amic">
<template u-is="name"></template>
<script>
export function $props() {
return {
name: '',
}
}
</script>
</template>