Check the Demo site https://dux.github.io/fez/
FEZ is a small library (20kb minified) that allows writing of Custom DOM elements in a clean and easy-to-understand way.
It uses
Latest version of libs are baked in Fez distro.
It uses minimal abstraction. You will learn to use it in 15 minutes, just look at examples, it includes all you need to know.
<script src="https://dux.github.io/fez/dist/fez.js"></script>
Uses DOM as a source of truth and tries to be as close to vanilla JS as possible. There is nothing to learn or "fight", or overload or "monkey patch" or anything. It just works.
Although fastest, Modifying DOM state directly in React / Vue / etc. is considered an anti-pattern. For Fez
this is just fine if you want to do it. Fez
basically modifies DOM, you just have a few helpers to help you do it.
It replaces modern JS frameworks by using native Autonomous Custom Elements to create new HTML tags. This has been supported for years in all major browsers.
This article, Web Components Will Replace Your Frontend Framework, is from 2019. Join the future, ditch React, Angular and other never defined, always "evolving" monstrosities. Vanilla is the way :)
There is no some "internal state" that is by some magic reflected to DOM. No! All methods Fez use to manipulate DOM are just helpers around native DOM interface. Work on DOM raw, use built in node builder or full template mapping with morphing.
Fez('ui-foo', class UiFoo extends FezBase)
<ui-foo bar="baz" id="node1"></ui-foo>
node1.fez.init()
when node is added to DOM and connect your component to dom.Fez
helper methods, or do all by yourself, all good.That is all.
Here's a simple counter component that demonstrates Fez's core features:
<!-- Define a counter component in ex-counter.fez.html -->
<script>
// called when Fez node is connected to DOM
init() {
this.MAX = 6
this.state.count = 0
}
isMax() {
return this.state.count >= this.MAX
}
// is state is changed, template is re-rendered
more() {
this.state.count += this.isMax() ? 0 : 1
}
</script>
<style>
/* compiles from scss to css and injects class in head */
/* body style */
background-color: #f7f7f7;
/* scoped to this component */
:fez {
zoom: 2;
margin: 10px 0;
button {
position: relative;
top: -3px;
}
span {
padding: 0 5px;
}
}
</style>
<button onclick="fez.state.count -= 1" disabled={{ state.count == 1 }}>-</button>
<span>
{{ state.count }}
</span>
<button onclick="fez.more()" disabled={{ isMax() }}>+</button>
{{if state.count > 0}}
<span>—</span>
{{if state.count == MAX }}
MAX
{{else}}
{{#if state.count % 2 }}
odd
{{else}}
even
{{/if}}
{{/if}}
{{/if}}
To use this component in your HTML:
<!-- Load Fez library -->
<script src="https://dux.github.io/fez/dist/fez.js"></script>
<!-- Load component via template tag -->
<template fez="/fez-libs/ex-counter.fez.html"></template>
<!-- Use the component -->
<ex-counter></ex-counter>
This example showcases:
this.state
automatically update the DOM{{ }}
for expressions, @
as shorthand for this.
{{#if}}
, {{:else}}
blocks for dynamic UIinit()
method called when component mounts<ui-button>
→ <button class="fez fez-button">
), making components fully stylable with CSS{{ }}
and [[ ]]
), control flow (#if
, #unless
, #for
, #each
), and block templatesstate
object automatically triggers re-renders on property changesfez-keep="unique-key"
attribute to preserve DOM elements across re-renders (useful for animations, form inputs, or stateful elements)Fez.styleMacro('mobile', '@media (max-width: 768px)')
and use as :mobile { ... }
:fez { ... }
) and global styles in the same componentformData()
, setInterval()
(auto-cleanup), onResize()
, and nextTick()
fez-bind
directive for automatic form synchronization<slot />
support with event listener preservationthis.globalState
proxy<template fez="path/to/component.html">
<fez-icon name="gear" />
→ <fez-icon name="gear"></fez-icon>
)FAST = true
to prevent flickeringFez.fetch()
includes automatic response caching and JSON/FormData handlingGLOBAL = 'ComponentName'
for easy accessinit
, onMount
, beforeRender
, afterRender
, onDestroy
, onPropsChange
, onStateChange
, onGlobalStateChange
Fez.DEV = true
Fez('#foo') // find fez node with id="foo"
Fez('ui-tabs', this) // find first parent node ui-tabs
Fez('ui-tabs', (fez)=> ... ) // loop over all ui-tabs nodes
// define custom DOM node name -> <foo-bar>...
Fez('foo-bar', class {
// set element node name, set as property or method, defaults to DIV
// why? because Fez renames custom dom nodes to regular HTML nodes
NAME = 'span'
NAME(node) { ... }
// alternative: static nodeName = 'span'
// set element style, set as property or method
CSS = `scss string... `
// unless node has no innerHTML on initialization, bind will be set to slow (fastBind = false)
// if you are using components that to not use innerHTML and slots, enable fast bind (fastBind = true)
// component will be rendered as parsed, and not on next tick (reduces flickering)
// <fez-icon name="gear" />
FAST = true
FAST(node) { ... }
// alternative: static fastBind() { return true }
// define static HTML. calling `this.render()` (no arguments) will refresh current node.
// if you pair it with `reactiveStore()`, to auto update on props change, you will have Svelte or Vue style reactive behaviour.
HTML = `...`
// Make it globally accessible as `window.Dialog`
// The component is automatically appended to the document body as a singleton. See `demo/fez/ui-dialog.fez` for a complete example.
GLOBAL = 'Dialog'
GLOBAL = true // just append node to document, do not create window reference
// called when fez element is connected to dom, before first render
// here you still have your slot available in this.root
init(props) { ... }
// execute after init and first render
onMount() { ... }
// execute before or after every render
beforeRender() { ... }
afterRender() { ... }
// if you want to monitor new or changed node attributes
// monitors all original node attributes
// <ui-icon name="home" color="red" />
onPropsChange(attrName, attrValue) { ... }
// called when local component state changes
onStateChange(key, value, oldValue) { ... }
// called when global state changes (only if component uses key in question that key)
onGlobalStateChange(key, value) { ... }
// called when component is destroyed
onDestroy() { ... }
/* used inside lifecycle methods (init(), onMount(), ... */
// copy original attributes from attr hash to root node
this.copy('href', 'onclick', 'style')
// set style property to root node. look at a clock example
// shortcut to this.root.style.setProperty(key, value)
this.setStyle('--color', 'red')
// clasic interval, that runs only while node is attached
this.setInterval(func, tick) { ... }
// get closest form data, as object. Searches for first parent or child FORM element
this.formData()
// mounted DOM node root. Only in init() point to original <slot /> data, in onMount() to rendered data.
this.root
// mounted DOM node root wrapped in $, only if jQuery is available
this.$root
// node attributes, converted to properties
this.props
// gets single node attribute or property
this.prop('onclick')
// shortcut for this.root.querySelector(selector)
this.find(selector)
// gets value for FORM fields or node innerHTML
this.val(selector)
// set value to a node, uses value or innerHTML
this.val(selector, value)
// you can publish globally, and subscribe locally
Fez.publish('channel', foo)
this.subscribe('channel', (foo) => { ... })
// gets root childNodes
this.childNodes()
this.childNodes(func) // pass function to loop forEach on selection, mask nodes out of position
// check if the this.root node is attached to dom
this.isConnected
// this.state has reactiveStore() attached by default. any change will trigger this.render()
this.state.foo = 123
// generic window event handler with automatic cleanup
// eventName: 'resize', 'scroll', 'mousemove', etc.
// delay: throttle delay in ms (default: 200ms)
this.on(eventName, func, delay)
// window resize event with cleanup (shorthand for this.on('resize', func, delay))
// runs immediately on init and then throttled
this.onResize(func, delay)
// window scroll event with cleanup (shorthand for this.on('scroll', func, delay))
// runs immediately on init and then throttled
this.onScroll(func, delay)
// requestAnimationFrame wrapper with deduplication
this.nextTick(func, name)
// get unique ID for root node, set one if needed
this.rootId()
// get/set attributes on root node
this.attr(name, value)
// hide the custom element wrapper and move children to parent
this.fezHide()
// automatic form submission handling if there is FORM as parent or child node
this.onSubmit(formData) { ... }
// render template and attach result dom to root. uses Idiomorph for DOM morph
this.render()
this.render(this.find('.body'), someHtmlTemplate) // you can render to another root too
})
/* Utility methods */
// define custom style macro
// Fez.styleMacro('mobile', '@media (max-width: 768px)')
// :mobile { ... } -> @media (max-width: 768px) { ... }
Fez.styleMacro(name, value)
// add global scss
Fez.globalCss(`
.some-class {
color: red;
&.foo { ... }
.foo { ... }
}
...
`)
// internal, get unique ID for a string, poor mans MD5 / SHA1
Fez.fnv1('some string')
// get dom node containing passed html
Fez.domRoot(htmlData || htmlNode)
// activates node by adding class to node, and removing it from siblings
Fez.activateNode(node, className = 'active')
// get generated css class name, from scss source string
Fez.css(text)
// get generated css class name without global attachment
Fez.cssClass(text)
// display information about fast/slow bind components in console
Fez.info()
// low-level DOM morphing function
Fez.morphdom(target, newNode, opts)
// HTML escaping utility
Fez.htmlEscape(text)
// create HTML tags with encoded props
Fez.tag(tag, opts, html)
// execute function until it returns true
Fez.untilTrue(func, pingRate)
// add scripts/styles to document head
// Load JavaScript from URL: Fez.head({ js: 'path/to/script.js' })
// Load JavaScript with attributes: Fez.head({ js: 'path/to/script.js', type: 'module', async: true })
// Load JavaScript with callback: Fez.head({ js: 'path/to/script.js' }, () => console.log('loaded'))
// Load JavaScript module and auto-import to window: Fez.head({ js: 'path/to/module.js', module: 'MyModule' })
// Load CSS: Fez.head({ css: 'path/to/styles.css' })
// Load CSS with attributes: Fez.head({ css: 'path/to/styles.css', media: 'print' })
// Execute inline script: Fez.head({ script: 'console.log("Hello world")' })
Fez.head(config, callback)
<!-- Remote loading for a component via URL in fez attribute -->
<!-- Component name is extracted from filename (ui-button) -->
<!-- If remote HTML contains template/xmp tags with fez attributes, they are compiled -->
<!-- Otherwise, the entire content is compiled as the component -->
<script fez="path/to/ui-button.fez.html"></script>
<!-- prefix with : to calc before node mount -->
<foo-bar :size="document.getElementById('icon-range').value"></foo-bar>
<!-- pass JSON props via data-props -->
<foo-bar data-props='{"name": "John", "age": 30}'></foo-bar>
<!-- pass JSON template via data-json-template -->
<script type="text/template">{...}</script>
<foo-bar data-json-template="true"></foo-bar>
<!-- override slow bind behavior -->
<foo-bar fez-fast="true"></foo-bar>
All parts are optional
<!-- Head elements support (inline only in XML tags) -->
<xmp tag="some-tag">
<head>
<!-- everything in head will be copied to document head-->
<script>console.log('Added to document head, first script to execute.')</script>
</head>
<script>
class {
init(props) { ... } // when fez node is initialized, before template render
onMount(props) { ... } // called after first template render
}
</script>
<script> // class can be omitted if only functions are passed
init(props) { ... }
</script>
<style>
b {
color: red; /* will be global style*/
}
:fez {
/* component styles */
}
</style>
<style>
color: red; /* if "body {" or ":fez {" is not found, style is considered local component style */
</style>
<div> ... <!-- any other html after head, script or style is considered template-->
<!-- resolve any condition -->
{{if foo}} ... {{/if}}
<!-- unless directive - opposite of if -->
{{unless fez.list.length}}
<p>No items to display</p>
{{/unless}}
<!-- runs in node scope, you can use for loop -->
{{each fez.list as name, index}} ... {{/each}}
{{for name, index in fez.list}} ... {{/for}}
<!-- Block definitions -->
{{block image}}
<img src={{ props.src}} />
{{/block}}
{{block:image}} <!-- Use the header block -->
{{block:image}} <!-- Use the header block -->
{{raw data}} <!-- unescape HTML -->
{{json data}} <!-- JSON dump in PRE.json tag -->
<!-- fez-this will link DOM node to object property (inspired by Svelte) -->
<!-- linkes to -> this.listRoot -->
<ul fez-this="listRoot">
<!-- when node is added to dom fez-use will call object function by name, and pass current node -->
<!-- this.animate(node) -->
<li fez-use="animate">
<!-- fez-bind for two-way data binding on form elements -->
<input type="text" fez-bind="state.username" />
<!--
fez-class for adding classes with optional delay.
class will be added to SPAN element, 100ms after dom mount (to trigger animations)
-->
<span fez-class="active:100">Delayed class</span>
<!-- preserve state by key, not affected by state changes-->>
<p fez-keep="key">...</p>
<!-- :attribute for evaluated attributes (converts to JSON) -->
<div :data-config="state.config"></div>
</div>
</xmp>
Inside init()
, you have pointer to this
. Pass it anywhere you need, even store in window.
Example: Dialog controller
<ui-dialog id="main-dialog"></ui-dialog>
Fez('ui-dialog', class {
init() {
// makes dialog globally available
window.Dialog = this
}
close() {
...
}
})
// close dialog window, from anywhere
Dialog.close()
// you can load via Fez + node selector
Fez('#main-dialog').close()
Fez includes a built-in fetch wrapper with automatic JSON parsing and session-based caching:
// GET request with promise
const data = await Fez.fetch('https://api.example.com/data')
// GET request with callback, does not create promise
Fez.fetch('https://api.example.com/data', (data) => {
console.log(data)
})
// POST request
const result = await Fez.fetch('POST', 'https://api.example.com/data', { key: 'value' })
Fez.onError
with kind 'fetch'Fez.LOG = true
to see cache hits and live fetches// Override default error handler
Fez.onError = (kind, error) => {
if (kind === 'fetch') {
console.error('Fetch failed:', error)
// Show user-friendly error message
}
}
Fez includes a built-in global state manager that automatically tracks component subscriptions. It automatically tracks which components use which state variables and only updates exactly what's needed.
this.globalState
proxyclass Counter extends FezBase {
increment() {
// Setting global state - all listeners will be notified
this.globalState.count = (this.globalState.count || 0) + 1
}
render() {
// Reading global state - automatically subscribes this component
return `<button onclick="fez.increment()">
Count: ${this.globalState.count || 0}
</button>`
}
}
// Set global state from outside components
Fez.state.set('count', 10)
// Get global state value
const count = Fez.state.get('count')
// Iterate over all components listening to a key
Fez.state.forEach('count', (component) => {
console.log(`${component.fezName} is listening to count`)
})
Components can define an onGlobalStateChange
method for custom handling:
class MyComponent extends FezBase {
onGlobalStateChange(key, value) {
console.log(`Global state "${key}" changed to:`, value)
// Custom logic instead of automatic render
if (key === 'theme') {
this.updateTheme(value)
}
}
render() {
// Still subscribes by reading the value
return `<div class="${this.globalState.theme || 'light'}">...</div>`
}
}
// Multiple counter components sharing max count
class Counter extends FezBase {
init(props) {
this.state.count = parseInt(props.start || 0)
}
beforeRender() {
// All counters share and update the global max
this.globalState.maxCount ||= 0
// Find max across all counter instances
let max = 0
Fez.state.forEach('maxCount', fez => {
if (fez.state?.count > max) {
max = fez.state.count
}
})
this.globalState.maxCount = max
}
render() {
return `
<button onclick="fez.state.count++">+</button>
<span>Count: ${this.state.count}</span>
<span>(Global max: ${this.globalState.maxCount})</span>
`
}
}