DEV Community

artydev
artydev

Posted on

Statefull components in plain JS - No Signals , no Hooks

Why on earth use 'hooks' or 'signal' to update a simple counter as we have all seen as examples in hyped frameworks ?

Use simple tools for simple tasks.

MVU is one of those simple tools.

The entire source is here (Thanks to Eckehard Fiedler creator of DML):

var _base
var _baseStack = []

function selectBase(ID) {
    // Save old base
    _baseStack.push(_base)
    if (_baseStack.length > 100) {
        alert("DML error: _baseStackOverflow in bushBase()")
        _baseStack = []
    }
    // select new base, either ID or element
    if (typeof(ID) === 'string')
        _base = document.getElementById(ID)

    _base = ID
    return _base
}

function sb(ID) {
    selectBase(ID)
} // Alias shortcut

/*------------------------------------------------------
  read curent _base
  ------------------------------------------------------*/
function getBase() {
    return _base
}

/*------------------------------------------------------
   get current stack position
  ------------------------------------------------------*/
function DMLgetSP() {
    return _baseStack.length
}

/*------------------------------------------------------
  set Stackpointer for stored value
  ------------------------------------------------------*/
function DMLsetSP(SP, msg = "DMLsetSP") {
    if (SP > _baseStack.length)
        alert("Error in " + msg + ", Stack pointer below desired SP")
    else
        while (_baseStack.length > SP)
            _base = _baseStack.pop()
    return _baseStack.length
}

/*------------------------------------------------------
   check, if current position is equal to chk
   chk is the stacklength before push, gives stack mismatch alert
  ------------------------------------------------------*/
function DMLchkSP(oldCnt = 0, txt = "Missing unselectBase()") {
    if (DMLgetSP() != oldCnt)
        alert("DML error: _baseStack size mismatch - " + txt + ", before: " + oldCnt + ", after: " + DMLgetSP())
    return _baseStack.length
}

/*------------------------------------------------------
  Stack-Prüfung für Einzelfunktionen
  f: ()=>{return new construct(xyz) }
  ------------------------------------------------------*/
function checkSP(f, txt) {
    let sp = DMLgetSP()
    let ret = f()
    DMLchkSP(sp, txt)
    return ret
}

/*------------------------------------------------------
   restore last base from stack, returns Stack position.
   if cnt set, unselectBase is called cnt times
   oldCnt is provided for test purpose. If SP after unselectBase
   is different, error message is displayed
  ------------------------------------------------------*/
function unselectBase(cnt = 1, oldCnt = -1, msg = "unselectBase") {
    for (let i = 0; i < cnt; i++) {
        if (_baseStack.length <= 0) {
            alert("DML error: _baseStack empty in popBase()")
            break
        } else
            _base = _baseStack.pop() // restore old stack

        if (oldCnt >= 0) {
            if (DMLgetSP() != oldCnt)
                alert("DML error: _baseStack size mismatch - " + msg + ", before: " + oldCnt + ", after: " + DMLgetSP())
        }
    }
    // if chk, check Stacklength after pop == chk
    return _baseStack.length
}


/* ----------------------------------------------------------------------------
   Check, if node is element or string
   create span from string
   if c is object, return Object only
   ----------------------------------------------------------------------------*/
function chk_node(c) {
    if (typeof(c) == "string") {
        let ret = create("span", null, null)
        ret.innerHTML = c
        return ret
        //  return textNode(c)
    } else return c
}


function setAttributes(el, attrib) {
    if (typeof(attrib) == "string") // Check for string attribute          
        attrib = {
            "style": attrib
        } // Convert strings to {"style",attrib}

    // set attributes
    if (typeof(attrib) == "object") {
        // Slpit JSON, set attributes individually
        Object.keys(attrib).forEach(function(key) {
            let val = attrib[key]
            if (key != "style") {
                el.setAttribute(key, val); // Normal attributes
            } else { // set Style parameters individually
                let ar = val.split(';'); // Split style Elements
                ar.forEach(function(pair) {
                    if (pair) { // If not empty    
                        let kv = pair.split(":")
                        if (kv.length == 2) {
                            let p = kv[0].trim()
                            let v = kv[1].trim()
                            el.style.setProperty(p, v); // Set property
                        }
                    }
                })
            }
        })
    }
    return (el)
}

/*------------------------------------------------------
   general short cut function. Creates an element and appends
   to existing object. Type is string like 'div'
  ------------------------------------------------------*/
function createAt(obj, typ, attrib) {
    let ret = document.createElement(typ)
    setAttributes(ret, attrib)
    obj.appendChild(ret)
    return ret
}


// ----------------------------------------------------------------------------
// create object with content and attributes. Content can be text or object
// attrib is an JSON-object {"id":"test", "class": myclass}
// ----------------------------------------------------------------------------
function create(typ, attrib, c) {
    let el = document.createElement(typ)
    let cIsArray = (Array.isArray(c))
    if (c) {
        if (typeof(c) == 'number') {
            c = String(c)
        }

        if (typeof(c) == 'string') {
            el.innerHTML = c
        } else {
            if (cIsArray) {
                el.append(...c)
            } else {
                el.appendChild(c);
            }

        }
    }
    if (attrib) {
        setAttributes(el, attrib)
    }
    return el
}


// ----------------------------------------------------------------------------
// Append object at current base
// ----------------------------------------------------------------------------
function appendBase(c) {
    let e = chk_node(c)
    if (_base) _base.appendChild(e)
    else {
        // if (document.body) document.body.append(e)
        if (document.body) document.body.appendChild(e)
        else {
            console.log("null Body found: " + c.textContent)
            return
        }
    }
    return (e)
}
// ----------------------------------------------------------------------------
// Append object at current base without check for String
// ----------------------------------------------------------------------------
function _appendBase(c) {

    if (_base)
        _base.appendChild(c)
    else
        document.body.append(c)
    return (c)
}

// ----------------------------------------------------------------------------
// make: create element with content and appendBase at current base
// attributes is an JSON-object {"id":"test", "class": myclass}
// ----------------------------------------------------------------------------
function make(typ, attrib, c) {
    return appendBase(create(typ, attrib, c))
}

// wrapper on make
const m = (type) => (content, attribs) => {
    let elt = make(type, attribs, content)
    elt.name = type
    return elt
}

function div(s, attrib) {
    return make("div", attrib, s)
}

const dom = (base = div("", null)) => selectBase(base)
const udom = unselectBase;

function _html(html) {
    var template = document.createElement('template');
    html = html.trim();
    template.innerHTML = html;
    const elt = template.content.firstChild
    appendBase(elt);
    return elt
}

function html(template) {
    const text = typeof template == "string" ? [template] : [template[0]]
    for (let i = 1; i < arguments.length; i++) {
        text.push(arguments[i], template[i])
    }
    return _html(text.join(''))
}
const MVU = {
    m,
    dom,
    udom,
    html
}

export {
    MVU
}
Enter fullscreen mode Exit fullscreen mode

Here is a demo a famous the famous statefull counters :


import { MVU } from "./mvu.js"

const {dom, udom, html} = MVU;

const qs = (elt, tag) => elt.querySelector(tag);

function Counter (start = 0) {
  let c = start;
  const incValue = () => qs(view, 'span').innerText = ++c;
  const view = html`
    <div>
      Value : <span>${c}</span>
      <button>INC</button>
    </div>`
    qs(view, "button").onclick = incValue;
  return view
}

function applyStyle (elt, bgcolor = "deeppink") {
   elt.style = 
    `border-bottom:1px solid white;
     background:${bgcolor};
     color:white;
     padding:10px;`
}

function App () {
   const app = dom ()
     const c1 = Counter (3);
     const c2 = Counter (12);
     const c3 = Counter (4);
   udom ();
   [c1,c2,c3].forEach(c => applyStyle(c));
   app.style.width = "300px";
   return app;
}

document.body.append(App())
Enter fullscreen mode Exit fullscreen mode

You can test it here StateFullCounters

Top comments (1)

Collapse
 
artydev profile image
artydev • Edited

Hi Duda,
Thank you for your response.
In fine all frameworks use DOM API to modify a page, so they use innerHTML or innerText.
They just take care of sanitizing before injection in the page.
That is the responsability of the developpers.
After all, with those frameworks you have no control on how this sanitization is done....

Give MVU a try and see where you go too far before using a Framework.
Look at the examples on the MVU series or DML Site.

See this example Users

And it's performance on UsersOnPageSpeed

See what you are missing from Lit

Regards