Fez was created by @dux in 2024. Latest update was .
<ui-clock></ui-clock>
Fez('ui-clock', class {
HTML = `
<svg viewBox="-50 -50 100 100">
<circle class="clock-face" r="48" />
<!-- markers -->
{#each [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55] as minute}
<line class="major" y1="35" y2="45" transform="rotate({30 * minute})" />
{#each [1, 2, 3, 4] as offset}
<line class="minor" y1="42" y2="45" transform="rotate({6 * (minute + offset)})" />
{/each}
{/each}
<!-- hour hand -->
<line class="hour" y1="2" y2="-20" transform="rotate({30 * @state.hours + @state.minutes / 2})" />
<!-- minute hand -->
<line class="minute" y1="4" y2="-30" transform="rotate({6 * @state.minutes + @state.seconds / 10})" />
<!-- second hand -->
<g transform="rotate({6 * @state.seconds})">
<line class="second" y1="10" y2="-38" />
<line class="second-counterweight" y1="10" y2="2" />
</g>
</svg>
`
css = `
input {
border: 3px solid red !important;
}
svg {
width: 100%;
height: 100%;
}
.clock-face {
stroke: #333;
fill: white;
}
.minor {
stroke: #999;
stroke-width: 0.5;
}
.major {
stroke: #333;
stroke-width: 1;
}
.hour {
stroke: #333;
}
.minute {
stroke: #666;
}
.second,
.second-counterweight {
stroke: rgb(180, 0, 0);
}
.second-counterweight {
stroke-width: 3;
}
`
setVars() {
let time = new Date();
this.state.time = time
this.state.hours = time.getHours();
this.state.minutes = time.getMinutes();
this.state.seconds = time.getSeconds();
}
connect() {
this.setVars()
this.setInterval(this.setVars, 1000)
}
})
<ui-todo></ui-todo>
Fez('ui-todo', class {
// if you define static html, it will be converted tu function(fast), and you will be able to refresh state with this.render()
HTML = `
<h3>Tasks</h3>
{#if !@state.tasks[0]}
<p>No tasks found</p>
{/if}
{#for task, index in @state.tasks}
{#if task.animate} <!-- this is fine because this is string templating -->
<p fez-use="animate" style="display: none; height: 0px; opacity: 0;">
{else}
<p>
{/if}
<input
type="text"
fez-bind="state.tasks[{index}].name"
style="{ task.done ? 'background-color: #ccc;' : '' }"
/>
⋅
<input
type="checkbox"
fez-bind="state.tasks[{index}].done"
/>
⋅
<button onclick="$$.removeTask({ index })">×</button>
</p>
{/for}
<p>
<button onclick="$$.addTask()">add task</button>
⋅
<button onclick="$$.clearCompleted()">clear completed</button>
</p>
<pre class="code">{ JSON.stringify(this.state.tasks, null, 2) }</pre>
`
clearCompleted() {
this.state.tasks = this.state.tasks.filter((t) => !t.done)
}
removeTask(index) {
this.state.tasks = this.state.tasks.filter((_, i) => i !== index);
}
addTask() {
// no need to force update template, this is automatic because we are using reactiveStore()
this.counter ||= 0
this.state.tasks.push({
name: `new task ${++this.counter}`,
done: false,
animate: true
})
}
animate(node) {
// same as in Svelte, uf you define fez-use="methodName", method will be called when node is added to dom.
// in this case, we animate show new node
$(node)
.css('display', 'block')
.animate({height: '33px', opacity: 1}, 200, () => {
delete this.state.tasks[this.state.tasks.length-1].animate
$(node).css('height', 'auto')
})
}
connect() {
this.state.tasks = [
{name: 'First task', done: false},
{name: 'Second task', done: false},
{name: 'Third task', done: true },
]
}
})
<ui-form target="/api"> <p> <input type="text" name="info" value="a dude" /> </p> <p> <select name="num"> <option>one</option> <option>two</option> <option>three</option> </select> </p> <p> <label><input type="radio" name="name" value="Jakov" /> Jakov</label> <label><input type="radio" name="name" value="Vid" /> Vid</label> <label><input type="radio" name="name" value="Dino" /> Dino</label> </p> <p> <button>Submit</button> </p> </ui-form>
// render form
Fez('ui-form', class {
NAME = 'form'
css = `
border: 2px solid green;
border-radius: 5px;
padding: 15px;
margin: 15px 0;
label {
display: block;
cursor: pointer;
margin-bottom: 5px;
}
select option {
font-size: 16px;
}
`
submit(e) {
e.preventDefault()
alert(JSON.stringify(this.formData()))
}
connect() {
this.root.onsubmit = this.submit
}
})
<ui-time city="Zagreb"> <ul> <li> Slot state preservation demo: <b class="color-name"></b> </li> </ul> </ui-time>
Fez('ui-time', class {
NAME = 'div'
CSS = `
// :fez will be replaced with .fez-ui-time, so you can add local styles in global css
:fez {
border: 10px solid green;
border-radius: 10px;
padding: 10px;
button {
font-size: 16px;
}
}
`
HTML = `
<p>Param city: { @props.city }</p>
<p>Time now: <span class="time"></span></p>
<p>Random num: <span>{ Math.random() }</span></p>
<button onclick="$$.setRandomColor()">random color</button>
⋅
<button onclick="$$.render()">refresh & preserve slot</button>
<hr />
<slot />
`
getRandomColor() {
const colors = ['red', 'blue', 'green', 'teal', 'black', 'magenta', 'orange', 'lightblue']
return colors[Math.floor(Math.random() * colors.length)]
}
setRandomColor() {
const color = this.getRandomColor()
// this.find('.color-name').innerHTML = color
this.val('.color-name', color)
this.root.style.borderColor = color
}
getTime() {
return (new Date()).getTime()
}
setTime() {
this.val('.time', this.getTime())
}
afterRender() {
this.setTime()
}
connect() {
this.setInterval(this.setTime, 1000)
this.setRandomColor()
}
})
<input type="range" min="24" max="100" class="slider" id="icon-range" oninput="Fez('#icon-blue').setSize(this.value)" /> ⋅ <span onclick="Fez('#icon-blue').attr('color', $(event.target).text())"> <span class="pointer">red</span> <span class="pointer">blue</span> <span class="pointer">green</span> </span> <br /><br /> <ui-icon name="home"></ui-icon> <ui-icon id="icon-blue" name="settings" color="blue" onclick="alert(this.fez.props.color)" size="{{ document.getElementById('icon-range').value }}" ></ui-icon> <ui-icon color="red">delete</ui-icon>
$(document.head).append(`
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
`)
// component to render google fonts icons
Fez('ui-icon', class {
// default node name is div
NAME = 'span'
CSS = `
&.material-symbols-outlined {
font-variation-settings:
'FILL' 0,
'wght' 400,
'GRAD' 0,
'opsz' 24
}
`
setSize(size) {
this.$root.css('font-size', `${parseInt(size)}px`)
}
onPropsChange(name, value) {
if (name == 'color') {
this.$root.css('color', value)
}
if (name == 'size') {
this.setSize(value)
}
}
connect(root, props) {
this.copy('onclick')
const icon = this.props.name || this.root.innerHTML.trim()
this.color = this.props.color || '#00'
this.root.classList.add('material-symbols-outlined')
this.root.innerHTML = icon
}
})
<style> .fez-ui-pubsub { margin-top: 10px; } </style> <button onclick="Fez.publish('ping', Math.random())">ping from anywhere</button> ⋅ Fast ping: <button onclick=" clearInterval(window.fastPing); window.fastPing = setInterval(()=>Fez.publish('ping', Math.random()), 10);" >START</button> ⋅ <button onclick="clearInterval(window.fastPing)">END</button> <ui-pubsub> <h4>lisener 1</h4> <p class="target"></p> </ui-pubsub> <ui-pubsub> <h4>lisener 2</h4> <b class="target"></b> </ui-pubsub> <ui-pubsub> <h4>lisener 3</h4> Foo: <span class="target"></span> </ui-pubsub> <template id="pubsub-t"> <ui-pubsub> Dynamic lisener: <button onclick="Fez(this).$root.remove()">×</button> ⋅ <span class="target"></span> </ui-pubsub> </template> <br /> <button onclick="$(this).after($('#pubsub-t').html())">add listener</button> <script> setInterval(()=>Fez.publish('ping', Math.random()), 5000) </script>
Fez('ui-pubsub', class extends FezBase {
update (info) {
this.target.innerHTML = info
}
connect() {
this.target = this.find('.target')
this.subscribe('ping', this.update)
this.update('waiting for a ping...')
}
})
<ui-list>red,green,blue</ui-list> <hr /> <ui-list></ui-list>
// component to render google fonts icons
Fez('ui-list', class {
css = `
li {
font-weight: bold;
}
`
HTML = `
{#if @colors[0]} @ will be replaced with this.
<ul>
{#for color in @colors}
<li style="color: { color };">{ color }</li>
{/for}
</ul>
{else}
<p>no colors, here is object</p>
<h4>for loop, no index</h4>
{#for [key, value] in this.objectData}
<p>{ key } : { value }</p>
{/for}
<h4>each loop, with index</h4>
{#each this.objectData as [key, value], index }
<p>
{ key } : <i>{ value }</i> : { index }
</p>
{/each}
{/if}
`
connect() {
this.colors = this.root.innerHTML.trim().split(',')
this.objectData = {
foo: 'bar',
baz: 1234
}
}
})
second tab
second tab
second tab
<ui-tabs> <div title="Foo">first tab</div> <div title="Bar"> <p>second tab</p> <br /> <ui-tabs> <div title="Foo nested 2">first tab</div> <div title="Bar"> <p>second tab</p> <ui-tabs> <div title="Foo nested 3">first tab</div> <div title="Bar nested 3"> <p>second tab</p> </div> </ui-tabs> </div> </ui-tabs> </div> <div title="Baz"> <h4>image tab</h4> <img src="./demo/fez.png" /> </div> </ui-tabs>
Fez('ui-tabs', class {
css = `
--tabs-border: 1px solid #ccc;
.header {
margin-bottom: -2px;
position: relative;
z-index: 1;
& > span {
border: var(--tabs-border);
padding: 8px 15px;
display: inline-block;
border-radius: 8px 8px 0 0;
margin-right: -1px;
background: #eee;
cursor: pointer;
&.active {
background-color: #fff;
border-bottom: none;
}
}
}
.body {
border: var(--tabs-border);
padding: 8px 15px;
background: #fff;
& > div {
display: none;
&.active {
display: block;
}
}
}
`;
activateNode(node) {
node.parent().find('> *').removeClass('active')
node.addClass('active')
}
activate(num) {
this.active = parseInt(num)
const target = this.$root.find(`> div > div.header > span:nth-child(${num + 1})`)
this.activateNode(target)
this.activateNode(this.tabs[num])
}
connect(props) {
this.tabs = this.childNodes(n => $(n))
this.render([
this.n('div.header', this.tabs.map((tab, index) =>
this.n(`span`, tab.attr('title'), { onclick: `$$.activate(${index})` })
)),
this.n('.body', '<slot />')
]);
this.activate(0)
}
})