Fez demo components ⋅ playgroundGitHub repo

Fez was created by @dux in 2024. Latest update was .

ui-clock ⋅ featuturing SVG generation & reactive store

              
<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 ⋅ features reactive state, fez-use & fez-bind ⋅ ToDo MVC candidate (React, Vue, Angular)

              
<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 ⋅ Features form helpers and custom dom node name (FORM)

              
<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 ⋅ Features calling instance methods, afterHtml(), slot state preservation.

  • Slot state preservation demo:
              
<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()
  }

})

              
            

ui-icon ⋅ Features component to component communication

red blue green

delete
              
<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
  }
})

              
            

ui-pubsub ⋅ Features publish <> subscribe mechanism

⋅ Fast ping:

lisener 1

lisener 2

lisener 3

Foo:

              
<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 ⋅ Features list and object rendering

red,green,blue
              
<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
    }
  }
})

              
            

ui-tabs ⋅ Features alternative node builder and nested components

first tab

second tab


first tab

second tab

first tab

second tab

image 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)
  }
})