FEZ is a small library (19kb unziped) that allows writing of Custom DOM elements in a clean and easy-to-understand way.
It uses
- Goober to enable runtime SCSS (similar to styled components)
- Stache to render templates (similar/same to Svelte templates)
- Idiomorph to morph DOM from one state to another (as React or Stimulus/Turbo does it)
Latest version of libs are baked in Fez distro.
It uses minimal abstraction. You will learn to use it in 3 minutes, just look at example, it includes all you need to know.
<script src="https://cdn.jsdelivr.net/gh/dux/fez-custom-dom-elements@latest/dist/fez.js"></script>
Uses DOM as a source of truth anf 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
basicly modifies DOM, you just have 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 manupulate DOM are just helpers around native DOM interface. Work on DOM raw, use jQuery, use built in node builder or full template mapping with morphing.
It great in combination with another widely used JS libs, as jQuery, Zepto, underscore of loDash.
- define your custom component -
Fez('ui-foo', class UiFoo extends FezBase)
- add HTML -
<ui-foo bar="baz" id="node1"></ui-foo>
- lib will call
node1.fez.connect()
when node is added to DOM and connect your component to dom. - use
Fez
helper methods, or do all by yourself, all good.
- lib will call
That is all.
- It can create and define Custom HTML tags, libs main feature. It uses native, fast browser interface to do it.
- It plays great with server generated code, because this is a component library. You are free to use any routing and server logic you prefer.
- Before
coonect()
, it will rename custom dom node name and create standard HTML node. For example<ui-button>
can be converted to<button class="fez fez-button btn btn-empty">...
. This makes allFez
components stylable in root (you can't styleui-button
). - I will use one file to define CSS, HTML and code.
- It does not need server side compiling.
- There is no magic as Svelte runes, React hooks, states and whatever. Plain vanilla JS classes with "few" documented functions.
- it can style components using SCSS, using goober.
- it has few useful built in helper methods as formData(), setInterval() that triggers only while node is connected, etc
- it has
<slot />
support - It has garbage collector, just add tags to HTML and destroy DOM nodes as you whish.
- It will close "HTML invalid" inline items before rendering
<fez-icon name="gear" />
-><fez-icon name="gear"></fez-icon>
- it has built in publish-subscribe, where only connected nodes will be able to publish and receive subs.
- It morphs DOM, state is preserved on changes.
- It can have full state <> template sync using
reactiveStore()
- It has no build in routing. This is lib for building DOM components. Works great with any server side rendering or libs like HTMLX or even React or Angular. Fez is great way to continue working on legacy JS apps that are too complicated to migrate. Just write new components in Fez.
// 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 generated css class name, from scss source string
Fez.css(text)
// 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) { ... }
// 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)
// <fez-icon name="gear" />
FAST = true
FAST(node) { ... }
// 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 = `...`
connect(props) {
// copy attributes from attr hash to root node
this.copy('href', 'onclick', 'style')
// clasic interval, that rune only while node is attached
this.setInterval(func, tick) { ... }
// get closest form data, as object
this.formData()
// mounted DOM node root
this.root
// mounted DOM node root wrapped in $, only if jQuery is available
this.$root
// node properties as Object
this.props
// gets single 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 globaly, and subscribe localy
Fez.publish('channel', foo)
this.subscribe('channel', (foo) => { ... })
// gets root childNodes. pass function to loop forEach on selection
this.childNodes(func)
// check if the this.root node is attached to dom
this.isAttached()
// on every "this.data" props change, auto update view.
this.data = this.reactiveStore()
// this.store has reactiveStore() attached by default. any change will trigger this,render()
this.store.foo = 123
// render template and attach result dom to root. uses Idiomorph for DOM morph
this.render(`
<!-- resolve any condition -->
{{if this.list[0]}}
<!-- fez-this will link DOM node to object property (inspired by Svelte) -->
<ul fez-this="listRoot">
<!-- runs in node scope -->
{{#each this.list as name, index}}
<!-- @ will be replaced with this. (inspired by CoffeeScript) -->
{{#each @list as name, index}}
<!-- you can use for loop -->
{{#for name, i in @list}
<!--
fez-use will call object function by name, and pass current node,
when node is added to dom (Inspired by Svelte)
-->
<li fez-use="animate">
<!-- $$ will point to fez instance, same as Fez(this) -->
<input onkeyup="$$.list[{{ index }}].name = this.value" value="{{ name }}" />
</li>
{{/list}}
</ul>
{{/if}}
`)
}
// you can render to another root too
this.render(this.find('.body'), someHtmlTemplate)
// alias to this.render(), to refresh state from static html template
this.refresh()
// execute after connect and initial component render
this.afterConnect() { ... }
// execute before or after every render
this.beforeRender() { ... }
this.afterRender() { ... }
// if you want to monitor new or changed node attributes
this.onPropsChange(name, value) { ... }
})
<!-- wrap JS in {{ }} to calc before node mount -->
<foo-bar
id="id-will-be-copied"
size="{{ document.getElementById('icon-range').value }}"
>
</foo-bar>
Examples are avaliable on jsitor or local GH pages.
- attaches HTML DOM to
this.root
- renames root node from original name to
static nodeName() // default DIV
- classes
fez
andfez-ui-foo
will be aded to root. - adds pointer to instance object to
fez
property (<div class="fez fez-ui-foo" onclick="console.log(this.fez)"
)- in parent nodes access it via
Fez(this)
with optional tag nameFez(this, 'ui-foo')
. It will look for closest FEZ node.
- in parent nodes access it via
- creates object for node attributes, accessible via
this.props
.<ui-foo name="Split">
->this.props.name == 'Split'
You can add global or local styles.
There is FEZ instance helper method this.formData()
, that will get form data for a current or closest form.
You can pass node DOM refrence, for a form you want to capture data from.
Inside connect()
, 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 {
close() {
...
}
connect() {
// makes dialog globally available
window.Dialog = this
}
})
// close dialog window, from anywhere
Dialog.close()
// you can load via Fez + node selector
Fez('#main-dialog').close()
Finds first closest Fez node.
-
Static
css()
method adds css globaly, todocument.body
.
-
List of given node attributes is converted to props object
-
Pointer to Fez root node.
-
jQuery wrapped root, if jQuery is present.
-
Called after DOM node is connected to Fez instance.
-
Copies atrributes from attribute object to root as node attributes. If attribute is false, it is skipped.
connect() => { this.copy('href', 'onclick', 'style', 'target') }
-
Moves all child nodes from one node to another node.
connect() => { // move all current child nodes to tmpNode const tmpNode = this.slot(this) // move all child nodes from node1 to node2 const tmpNode = this.slot(node1, node2) }
-
Inject htmlString as root node innerHTML
- replace
$$.
with local pointer. - replaces
<slot />
with given root
getData() => { alert('data!') } connect() => { this.render` <ul> {{#list}} <li> <input type="text" onkeyup="$$.list[{{num}}].name = this.value" value="{{ name }}" class="i1" /> </li> {{/list}} </ul> <span class="btn" onclick="$$.getData()">read</span> ` }
- replace
-
Returns rendered string.
-
Uses goober to render inline css. Same as React styled-components, it returns class that encapsulates given style.
Pass second argument as true, to attach class to root node.
const className = this.css(` color: red; &.blue { color: blue; } }` )
-
Get single property. It will first look on original root node, if not found it will look for attribute.
const onClickFunction = this.prop('onclick') const idString = this.prop('id')
-
If you want to monitor new or changed node attributes.
<fez-icon id="icon-1" name="gear" color="red" />
Fez('fez-icon', class { // ... onPropsChange(name, value) { if (name == 'color') { this.setColor(value) } } }) // same thing document.getElementById('icon-1').setAttribute('color', 'red') $('#icon-1').attr('color', 'red') Fez('#icon-1').setColor('red')
-
Creates reactive store that updates this.render() on change. You can pass another reactivity function.
Used in default TODO example.
this.reactiveStore({}, (o, k, v)=>{ window.requestAnimationFrame(()=>{ console.log(`key "${k}" changed to ${v}`) this.render()) }) })