Last active
July 9, 2020 11:41
-
-
Save jaandrle/9161653c4fda1f7ce2328a996c8e066f to your computer and use it in GitHub Desktop.
HTML/DOM creating
<?php
html("div", array( 'id'=> "id", 'class'=> "className" ),
html("p",
html("strong", "TEXT")
)
);
function html(/* tag[, attrs | children ][, children ] */){
$args= func_get_args();
$args_count= count($args);
if($args_count===0) return "";
$tag= array_shift($args);
if($args_count===1) return "<{$tag}/>";
$args_2= array_shift($args);
if(!__html__isProps($args_2)){
$attrs= "";
$children= is_array($args_2) ? join("", $args_2) : $args_2;
} else {
$attrs= __html__attrs($args_2);
$children= '';
}
foreach($args as $index => $child){
$children .= $child;
}
return "<{$tag} {$attrs}>{$children}</${tag}>";
}
/*
* Reduce array of attribute/value pairs
* into a single is_string
*/
function __html__attrs($props){
$res= "";
foreach ($props as $key => $value){
$res .= "$key='$value'";
}
return $res;
}
/*
* Check if an $args value isProps
* an array of new props. If this Check
* fails, it either means we have a String
* child, or an array of components
* (hence the regex).
*/
function __html__isProps( $arr ){
if(!is_array($arr)) return false;
$test= array_keys($arr);
return !is_numeric($test[0]);
}
/*
* Merge two arrays, merging existing
* keys and values
*/
function mergeProps($old, $new){
$arr = array();
foreach($old as $oldkey => $oldval){
foreach($new as $newkey => $newval){
if ($oldkey !== $newkey||($oldkey !== 'class' && $oldkey !== 'style')){
$arr[$oldkey] = $newval;
} else {
$arr[$oldkey] = $oldval.' '.$newval;
}
}
}
return $arr;
}
- PHP Doc
- Example:
<?php
$dom = new DOMDocument();
$pre_p = $dom->createElement("p");
$p= $dom->appendChild($pre_p);
$p->nodeValue= "Test";
$p->style= "HI";
$p->setAttribute("onclick", "HI");
var_dump($dom->saveHTML());//=string(25) "<p onclick="HI">Test</p> "
init(window);
const main_el= document.getElementById("main");
form().mount(main_el);
function form(){
const { add, update, share }= $dom.component("DIV");
add("LABEL", { textContent: "Name:" });
add("INPUT", { type: "text", onkeyup}, -1);
add("HR", null, -1);
add("H1", { onupdate: [ { value: ""}, _=>({ textContent: "Hello "+_.value }) ] }, -1);
return share;
function onkeyup(){ update({ value: this.value }); }
}
function li_example(){
let counter= 1;
console.time();
for(let i=0, test_update; i<1; i++){
test_update= li({ nth: "A", first: counter++, last: counter++ });
test_update.mount(main_el);
li({ nth: "B", first: counter++, last: counter++ }).mount(main_el);
test_update.update({ nth: "UPDATED", first: counter++, last: "Updated" });
}
console.timeEnd();
}
function li({ nth, first, last }){
let counter= 0;
const { add, component, update, share }= $dom.component("LI", null);
add("UL", null);
component(sub_li({ nth, counter }));
add("LI", null, -1);
add("SPAN", { textContent: "First text in parent component: ", onupdate: [{counter}, function(_){_.counter&&this.remove()}] });
add("STRONG", {
onclick,
style: {cursor: "pointer", 'border-bottom': "1px solid black" },
onupdate: [ {first, counter}, _=>({ textContent: "Element no.: "+_.first+", clicked: "+_.counter+"times" })]
}, -1);
add("LI", null, -2);
add("SPAN", { textContent: "Last text in parent component: " });
add("STRONG", { onupdate: [ { last }, _=>({ textContent: _.last })] }, -1);
return share;
function onclick(){
counter++; update({counter});
}
}
function sub_li({ nth, counter }){
const { add, share }= $dom.component("LI", null);
add("SPAN", { textContent: "This is sub-component: " });
add("STRONG", {
style: "color: darkblue",
onupdate: [{nth, counter}, _=>({ textContent: _.nth+" ('counter' is visible also here: "+_.counter+" ;-))" })]
}, -1);
return share;
}
function init(global){
let $dom= {
empty: function(container){
let len= container.childNodes.length;
while(len--){ container.removeChild(container.lastChild); }
},
insertAfter: function(new_element, reference){
const { parentNode, nextSibling }= reference;
if(nextSibling) parentNode.insertBefore(new_element, nextSibling);
else parentNode.appendChild(new_element);
},
replace: function(el_old, el_new){
$dom.insertAfter(el_new, el_old);
el_old.remove();
}
};
/**
* This 'functional class' is syntax sugar around [`DocumentFragment`](https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment) for creating DOM components and their adding to live DOM in performance friendly way.
* @class $dom.component
* @constructor
* @param {String} el_name
* - Name of element (for example `LI`, `P`, `A`, ...).
* - This is parent element of component.
* @param {Object} attrs
* - The second argument for [`$dom.assign`](./$dom.{namespace}.html#methods_assign)
* - There is one change. It is supported using `onupdate` key ... see [`add`](#methods_add)
* - `onupdate` is array `[ object, function]`, the function is called during creating element and evry `update`calls
* - It returns additional `attrs`, for example this `attrs`: `{ className: "class", onupdate: [ { a }, _=>({ textContent: a }) ] }` => final `attrs= { className: "class", textContent: "A" }` (if `a="A"`)
* - it use similar algorithm like [`$dom.assign`](./$dom.{namespace}.html#methods_assign) (**no deep copy!!!**)
* @param {Object} params
* @param {Function|Boolean} params.mapUpdate
* - `[params.mapUpdate=undefined]`
* - This function (if defined) remap `update(DATA)` to varibales used in keys `attrs.onupdate` ... see [`add`](#methods_add)
* @return {$dom.component}
* - 'functional class instance': object `{ add, component, mount, update, share }`
* - `share` is Object for transfering methods somewhere else (like for using in another component, see [`component`](#methods_component))
* - `share= { mount, update, destroy, isStatic }`
*/
/** */
$dom.component= function(el_name, attrs, { mapUpdate }={}){
let /* holds `initStorage()` if `onupdate` was registered */
internal_storage= null;
const /* 'drawer' (container) for component elements */
fragment= document.createDocumentFragment();
let /* main parent (wrapper), container for children elements */
container,
/* store for all registered elements */
els= [], all_els_counter= 0,
/* current elements deep which holds indicies of elements:
- add(...);add(...); = final deep=[0,1];
- add(...);add(...,-1);add(...) = final deep=[1,2]; (by steps: [0], [0,1], [1,2])
- see `shift` in `add`
*/
deep= [];
add(el_name, attrs);
const share= { mount, update, destroy, isStatic };
return { add, component, mount, update, share };
/**
* This add element to component
* @method add
* @public
* @param {String} el_name
* - Name of element (for example `LI`, `P`, `A`, ...).
* @param {Object} attrs
* - `null|undefined` is also supported (`null` is probably recommendet for better readability)
* - The second argument for [`$dom.assign`](./$dom.{namespace}.html#methods_assign)
* @param {Array} attrs.onupdate
* - Pattern: `attrs.onupdate= [ Values: Object, Retuns_attrs_keys: Function ]`
* - This register listener/subscriber function (`Retuns_attrs_keys`) for keys (variables) in `Values`
* - Example: `[ {counter}, _=>({ textContent: counter }) ]` registers listerner to `counter`. When the `udate({ ... counter: something, ...})` is called this element changes `textContent`.
* - See [`update`](#methods_update)
* @param {Number} [shift= 0]
* - Modify nesting behaviur. By default (`shift= 0`), new element is child of previus element. Every `-1` means moving to the upper level against current one - see example.
* @example
* //#1
* const UL= document.getElementById('SOME UL');
* const { add }= $dom.component("LI", { className: "list_item" });//<li class="list_item">...</li>
* add("DIV", { textContent: "Child of .list_item", className: "deep1" });//<li class="list_item"><div class="deep1">...</div></li>
* add("DIV", { textContent: "Child of div.deep1", className: "deep2" });//...<div class="deep1"><div class="deep2">...</div></div>...
* add("DIV", { textContent: "Child of div.deep2", className: "deep3" });//...<div class="deep1"><div class="deep2"><div class="deep3">...</div></div></div>...
* add("DIV", { textContent: "Child of div.deep2", className: "deep3 mark" }, -1);//...<div class="deep2"><div class="deep3">...</div><div class="deep3">...</div></div>...
* //next add(*) schoul be child of div.deep3.mark, by -1 it is ch.of div.deep2, by -2 ch.of div.deep1, by -3 ch.of li.list_item because div.deep3.mark is on 3rd level
* add("DIV", { textContent: "Child of div.deep1", className: "deep2 nextone" }, -2);//this is on 2nd level
* add("DIV", { textContent: "Child of div.deep1", className: "deep2 nextone" }, -2);//this is on 0 level
* add("DIV", null); // just DIV without attributes
* ...
*/
function add(el_name, attrs, shift= 0){
recalculateDeep(shift);
attrs= attrs || {};
const prepare_el= document.createElement(el_name);
if(!all_els_counter) container= els[0]= fragment.appendChild(prepare_el);
else els[all_els_counter]= els[getParentIndex()].appendChild(prepare_el);
let el= els[all_els_counter];
if(attrs.onupdate){
if(!internal_storage) internal_storage= initStorage();
attrs_assign(attrs, internal_storage.register(el, ...attrs.onupdate));
delete attrs.onupdate;
}
all_els_counter++;
$dom.assign(el, attrs);
return el;
}
/**
* Method for including another component by usint its `share` key.
* @method component
* @public
* @param {Object} share
* @param {Number} shift
* - see [`add`](#methods_add)
*/
function component({ mount, update, isStatic }, shift= 0){
recalculateDeep(shift);
els[all_els_counter]= mount(els[getParentIndex()]);
if(!isStatic()){
if(!internal_storage) internal_storage= initStorage();
internal_storage.registerComponent(update);
}
all_els_counter+= 1;
}
/**
* Add element to live DOM
* @method mount
* @public
* @param {NodeElement} element
* - Element where to places this component
* @param {String} [type= "childLast"]
* - Change type of mounting
* - `childLast` places component as last child
* - `childFirst` places component as first child
* - `replaceContent` removes content of `element` and places component as child (uses `$dom.empty`)
* - `replace` replaces `element` by component
* - `before` places component before `element`
* - `after` places component after `element` (uses `$dom.insertAfter`)
*/
function mount(element, type= "childLast"){
switch ( type ) {
case "replace":
$dom.replace(element, fragment);
break;
case "replaceContent":
$dom.empty(element);
element.appendChild(fragment);
break;
case "before":
element.parentNode.insertBefore(fragment, element);
break;
case "after":
$dom.insertAfter(fragment, element);
break;
default:
if(type==="childFirst" && element.childNodes.length) element.insertBefore(fragment, element.childNodes[0]);
else element.appendChild(fragment);
break;
}
return container;
}
/**
* Method remove element form live DOM and returns null
* @method destroy
* @public
* @example
* let { share: test }= $dom.component("DIV", null);
* test.mount(document.body);
* test= test.destroy();
*/
function destroy(){
container.remove();
return null;
}
/**
* Updates `deep`
* @private
* @method recalculateDeep
* @param {Number} shift
* - see [`add`](#methods_add)
*/
function recalculateDeep(shift){
if(!shift) deep.push(all_els_counter);
else { deep.splice(deep.length+1+shift); deep[deep.length-1]= all_els_counter; }
}
/**
* Returns current `deep` (last element in array)
* @method getParentIndex
* @private
*/
function getParentIndex(){
return deep[deep.length-2];
}
/**
* Initialize internal storage
* @method initStorage
* @private
* @returns {Object}
* - `{ register, registerComponent, update, unregister}`
*/
function initStorage(){
const /* storage for component, functions for updates and mapping data keys and corresponding elements */
data= {},
components= [], els= new Map(),
functions= new Map(),
listeners= new Map();
let
els_counter= 0;
return {
register: function(el, init_data, fun){
Object.assign(data, init_data);
const el_id= els_counter++; els.set(el_id,el);
const init_data_keys= Object.keys(init_data);
for(let i=0, i_key, i_length= init_data_keys.length; i<i_length; i++){
i_key= init_data_keys[i];
if(!listeners.has(i_key)) listeners.set(i_key, [ el_id ]);
else listeners.set(i_key, [ ...listeners.get(i_key), el_id ]);
}
functions.set(el_id, fun);
return fun.call(el, init_data) || {};
},
registerComponent: function(update){
if(components.indexOf(update)===-1) components.push(update);
},
update: function(new_data_input){
const new_data= typeof mapUpdate==="function" ? mapUpdate(new_data_input) : new_data_input;
let out= false;
for(let i=0, i_length= components.length; i<i_length; i++){ if(components[i](new_data)&&!out){out=true;} }
if(!listeners.size) return out;
const /* keys to update (subscribers exits and was changed) */
new_data_keys= Object.keys(new_data)
.filter(key=>listeners.has(key)&&data[key]!==new_data[key]),
new_data_keys_length= new_data_keys.length;
if(!new_data_keys_length) return out;
Object.assign(data, new_data);
const els_for_redraw= [];
for(let i=0, i_listeners; i<new_data_keys_length; i++){
i_listeners= listeners.get(new_data_keys[i]);
for(let j=0, ji_listener, j_length= i_listeners.length; j<j_length; j++){
ji_listener= i_listeners[j];
if(els_for_redraw.indexOf(ji_listener)===-1) els_for_redraw.push(ji_listener);
}
}
for(let i=0, i_length= els_for_redraw.length; i<i_length; i++){ processChanges(els_for_redraw[i]); }
return true;
function processChanges(el_id){
const new_attrs= functions.get(el_id).call(els.get(el_id), data) || {};
const el= els.get(el_id);
if(el.parentNode===null) return unregister(el_id, new_data_keys);
else $dom.assign(el, new_attrs);
}
},
unregister
};
function unregister(el_id, data_keys){
functions.delete(el_id);
els.delete(el_id);
for(let i=0, i_key, listener, i_length= data_keys.length; i<i_length; i++){
i_key= data_keys[i];
listener= listeners.get(i_key);
if(listener.length===1) listeners.delete(i_key);
else listeners.set(i_key, listener.filter(el_idFilter));
}
function el_idFilter(v){ return v!==el_id; }
}
}
/**
* Method updates all registered varibles by keys `onupdates` and calls follower functions
* @method update
* @public
* @param {Object} new_data
* - When `$dom.component` is initialized, it is possible to register `mapUpdate`
* - **It's because internally, it is used `Object.assign` (no deep copy) to merge new data with older one!!!**
* @example
* const data_A= { a: "A" };
* const data_A_update= { a: "AAA" };
* const { add, mount, update }= $dom.component("UL", null);
* add("LI", { onupdate: [ { a }, ({ a })=>({ textContent: a }) ] });//`[ { a },` add listener for "a"
* mount(document.body);
* update(data_A_update);
* //BUT
* const data_B= { a: { b: "A" }};
* const data_B_update= { a: { b: "AAA" }};
* const { add, mount, update }= $dom.component("UL", null, { mapUpdate: d=>({ a: d.a.b }) });
* add("LI", { onupdate: [ { a: data_B.a.b }, ({ a })=>({ textContent: a }) ] });//`[ { a },` add listener for "a"
* mount(document.body);
* update(data_B_update);
*/
function update(new_data){
if(!internal_storage) return false;
return internal_storage.update(new_data);
}
/**
* Methods returns if it was `onupdate` used
* @method isStatic
* @public
* @return {Boolean}
* - If there is some listeners `onupdate`
*/
function isStatic(){
return !internal_storage;
}
function attrs_assign(attrs_A, attrs_B){
const attrs_B_keys= Object.keys(attrs_B);
for(let i=0, key, attr, i_length= attrs_B_keys.length; i<i_length; i++){
key= attrs_B_keys[i];
attr= attrs_B[key];
switch(key){
case "style":
if(typeof attr==="string"){
attrs_A[key]= attr;
} else {
if(typeof attrs_A[key]!=="object") attrs_A[key]= {};
for(let k=0, k_key, k_keys= Object.keys(attr), k_length= k_keys.length; k<k_length; k++){ k_key= k_keys[k]; attrs_A[key][k_key]= attr[k_key]; }
}
break;
case "style_vars":
if(typeof attrs_A[key]!=="object") attrs_A[key]= {};
for(let k=0, k_key, k_keys= Object.keys(attr), k_length= k_keys.length; k<k_length; k++){ k_key= k_keys[k]; attrs_A[key][k_key]= attr[k_key]; }
break;
case "dataset":
if(typeof attrs_A[key]!=="object") attrs_A[key]= {};
for(let k=0, k_key, k_keys= Object.keys(attr), k_length= k_keys.length; k<k_length; k++){ k_key= k_keys[k]; attrs_A[key][k_key]= attr[k_key]; }
break;
default:
attrs_A[key]= attr;
break;
}
}
}
};
/**
* Procedure for adding elements into the `parent` (in background use `createDocumentFragment`, `createElement`, `appendChild`)
* @method add
* @for $dom.{namespace}
* @param parent {NodeElement}
* * Wrapper (for example `<ul>`) where to cerate children elements (for example `<li>`)
* @param $$$ {...Array}
* * `[ [ __NAME__, __PARAMS__ ], [ __NAME__, __PARAMS__ ], ..., [ __NAME__, __PARAMS__ ] ]`
* * Element in array is automatically nested into the previous element. `[["UL",...], ["LI",...], ["SPAN",...]]` creates `<ul><li><span></span></li></ul>`
* * `__NAME__` **\<String\>**: Name of element (for example `LI`, `P`, `A`, ...)
* * `__PARAMS__` **\<Object\>**: Parameters for elements as "innerText", "className", "dataset", ...
* * see [$dom.assign](#methods_assign)
* * There is one change with using key "$", which modify elements order and it is not parsed by [$dom.assign](#methods_assign)
* * `__PARAMS__.$`: Modify nesting behaviur (accepts index of element in `$$$`). `[["UL",...], ["LI",...], ["LI",{$:0,...}]]` creates `<ul><li></li><li></li></ul>`
* @return {NodeElement}
* * First created element (usualy wrapper thanks nesting)
* @example
* $dom.add(ul_element,[
* ["LI", {className: "nejake-tridy", onclick: clickFCE}],
* ["SPAN", {innerText: "Prvni SPAN v LI"}],
* ["SPAN", {$:0, innerText: "Druhy SPAN v LI"}]
* ]);
* // = <ul><li class="nejake-tridy" onclick="clickFCE"><span>Prvni SPAN v LI</span><span>Druhy SPAN v LI</span></li></ul>
* // !!! VS !!!
* $dom.add(ul_element,[
* ["LI", {className: "nejake-tridy", onclick: clickFCE}],
* ["SPAN", {innerText: "Prvni SPAN v LI"}],
* ["SPAN", {innerText: "Druhy SPAN v LI"}]
* ]);
* // = <ul><li class="nejake-tridy" onclick="clickFCE"><span>Prvni SPAN v LI<span>Druhy SPAN v LI</span></span></li></ul>
*/
$dom.add= function(parent,$$$){
let fragment= document.createDocumentFragment();
let prepare_els= [], els= [];
for(var i=0, i_length= $$$.length; i<i_length;i++){
prepare_els[i]= document.createElement($$$[i][0]);
if(!i) els[i]= fragment.appendChild(prepare_els[i]);
else if(typeof $$$[i][1].$!=="undefined"){
els[i]= els[$$$[i][1].$].appendChild(prepare_els[i]);
delete $$$[i][1].$;
}
else els[i]= els[i-1].appendChild(prepare_els[i]);
$dom.assign(els[i], $$$[i][1]);
}
parent.appendChild(fragment);
if(i) return els[0];
};
/**
* Procedure for merging object into the element properties.
* Very simple example: `$dom.assign(document.body, { className: "test" });` is equivalent to `document.body.className= "test";`.
* It is not deep copy in general, but it supports `style`, `style_vars` and `dataset` objects (see below).
* @method assign
* @for $dom.{namespace}
* @param {NodeElement} element
* @param {Object} object_attributes
* - Object shall holds **NodeElement** properties like `className`, `textContent`, ...
* - For `dataset` can be used also `Object` notation: `$dom.assign(document.getElementById("ID"), { dataset: { test: "TEST" } }); //<p id="ID" data-test="TEST"></p>`.
* - The same notation can be used for **CSS variables** (the key is called `style_vars`).
* - **IMPORTANT CHANGE**: Key `style` also supports **text**, so `$dom.assign(el, { style: "color: red;" });` and `$dom.assign(el, { style: { color: "red" } })` is equivalent to `el.setAttribute("style", "color: red;");`
* - *Speed optimalization*: It is recommended to use `textContent` (instead of `innerText`) and `$dom.add` or `$dom.component` (instead of `innerHTML`).
* @example
* const el= document.body;
* const onclick= function(){ console.log(this.dataset.js_param); };
* $dom.assign(el, { textContent: "BODY", style: "color: red;", dataset: { js_param: "CLICKED" }, onclick });
* //result HTML: <body style="color: red;" data-js_param="CLICKED">BODY</body>
* //console output on click: "CLICKED"
*/
$dom.assign= function(element, object_attributes){
const object_attributes_keys= Object.keys(object_attributes);
for(let i=0, key, attr, i_length= object_attributes_keys.length; i<i_length; i++){
key= object_attributes_keys[i];
attr= object_attributes[key];
if(typeof attr==="undefined"){ if(element[key]){ delete element[key]; } continue; }
switch(key){
case "style":
if(typeof attr==="string") element.setAttribute("style", attr);
else for(let k=0, k_key, k_keys= Object.keys(attr), k_length= k_keys.length; k<k_length; k++){ k_key= k_keys[k]; element.style.setProperty(k_key, attr[k_key]); }
break;
case "style_vars":
for(let k=0, k_key, k_keys= Object.keys(attr), k_length= k_keys.length; k<k_length; k++){ k_key= k_keys[k]; element.style.setProperty(k_key, attr[k_key]); }
break;
case "dataset":
for(let k=0, k_key, k_keys= Object.keys(attr), k_length= k_keys.length; k<k_length; k++){ k_key= k_keys[k]; element.dataset[k_key]= attr[k_key]; }
break;
case "href" || "src" || "class":
element.setAttribute(key, attr);
break;
default:
element[key]= attr;
break;
}
}
};
global.$dom= $dom;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<ul id="main"></ul>
<script src="$dom_component.js"></script>
</body>
</html>
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment