Gurigraphics / DOMinus.js

DOMinus.js is a reactive data binding library that turn HTML irrelevant.
4 stars 1 forks source link

Improving update granularity #13

Open thipages opened 5 years ago

thipages commented 5 years ago

Hi, Currently and if I understand well, updates thanks to DOM method of Dominus

  1. happen at the territory level (ie as a child of an existing Dom element, ultimately at the body level : DOM.add ("element", "body"); )
  2. Only those elements are considered for update and the update is a full HTML replacement through innerHTML Dom property.
  3. Also the underscore convention (eg element_input) allows to get the right root element (element) and update from that root if exists.

I have thought about another (complementary) way of updating DOM with more granularity but this requires additional concept. The idea is to use the HTML key (HTML[html_key]) as the id of the tag. This could be

The usage could be like this (if calculated)

var DOM = new Dominus();
// UID is generated behind the scene creating HTML[generatedUID] corresponding to myElement
var myElement = DOM.element ({tag:'div',class:'myClass'}); 
DOM.class.add(myElement.id, 'myClass2'); // or simplier DOM.class.add(myElement, 'myClass2');

In the proxy process, update could then be managed by the function replaceChild replacedNode = parentNode.replaceChild(newChild, oldChild); with parentNode=oldChild

What do you think? (hope I was clear)

Gurigraphics commented 5 years ago

Change only the "element that change" I think that is the "react.js" approach:

<div id="parent">
  <div id="child">10</div>  // change html to 11
</div>

-----------------------

<div id="parent">
  <div id="child">11</div> // updated only this child
</div>

Dominus is you who decides the granularity of the update.

<div id="parent"></div>

HTML.child ={
  id="child"
  html="10"
}
DOM.add("child", "#parent")

-----------------------

HTML.child.html = 11

<div id="parent">
  <div id="child">11</div> // updated only this child
</div>

In project design you decide what needs to change or not:

<div id="parent-area-imutable">
<div id="parent-area-mutable">
    <div id="child" class="child-mutable">10</div>  
</div>
<div class="child-imutable">11</div>  
<div class="child-imutable">12</div>  
<div class="child-imutable">13</div>  
</div>

To do this create different group modules for separate update:

HTML.header ={}
HTML.footer ={}
Etc

So they really are different approaches. It takes a very specific project to know when the advantage is greater. In most cases are milliseconds of difference.

thipages commented 5 years ago

Dominus is you who decides the granularity of the update.

Clear this is the territory concept. And currently the code needs to wrap a territory with "hard" html, correct?

If this is correct, why not doing it dynamically in order to avoid writing "hard" wrapper just saying reload yourself without hard wrapper? It seems to me that this is complementary and keep Dominus philosophy.

I may be wrong...

thipages commented 5 years ago

I start to understand if

Treating everything as "mutable" generates unnecessary processing, when there are elements that never change.

is linked with the processing of replaceChild method. Is this correct?

Gurigraphics commented 5 years ago

replaceChild is not the problem. The problem is to create a "real virtual DOM" with hierarchy of trees childs inside childs, to know who's child or parent, and where this element is located.

Imagine the object: HTML.level1.level2.level3 = {} Now change: HTML.level1.level2.level3 = 90 Check the contents of "target": PROXY.set( target, key, value ) { console.log( target ) }

How would you know the parent of this key? And after get ID, get element content and append by id or class?

I did not understand this: "avoid writing" hard "wrapper" .

I just use JSON.stringify. I transform the object into a string, compare it to the previous string and know which object it has changed or not to update or no.

thipages commented 5 years ago

I am going to sleep on it. Lets see

thipages commented 5 years ago

I have so much to say. I try to catch one item

How would you know the parent of this key?

Currently, I understand that Dominus needs the parent in order to update all levels (1, 2 and 3) -> But only level 3 is concerned by the update in the above example

I think we dont need the parent because we need to update only what has changed, ie level3 -> Knowing the key, we have the id and with replaceChild, we can update level3 only -> But we loose the hierarchical real virtual Dom, correct

Example (playcode here)

var DOM=new Dominus();
var HTML = DOM.HTML();
var count=0;

HTML.l1={html:"l1_l2"};
HTML.l1_l2={html:"l1_l2_l3"};
HTML.l1_l2_l3={html:"Start", onclick:"update()"};

DOM.add("l1", "#app");

function update() {
  console.log(count)
  count++;
  HTML.l1_l2_l3.html="update"+count;
}

Every time you click, logs show

Changed path: l1_l2_l3
Updated path: l1

so eveything is updated l1 : parent replaces its innerhtmlproperty and recreates l1, l2, l3 html code

Gurigraphics commented 5 years ago

Other Example:

HTML.header_level2 = [
 { tag:"li", id: "header_level2", html: "content_2" }, 
]

HTML.header_level1 = [
 { tag:"ul", id: "header_level1", html: "header_level2" }, 
]

HTML.header = {
  tag: "div",
  id: "header", 
  class: "header",
  html: "header_level1"
};

DOM.add("header", "#app");

The HTML

<div id="app">
  <div id="header" class="header">
    <ul id="header_level1">
    <li id="header_level2">content_2</li>
    </ul>
  </div>
</div>

The change

HTML.header.tag = "ul"

The proxy change

set( target, key, value ) {
      this.keys = {};
      target[ key ] = value   

 console.log( target.id ) // header

Then, "HTML.header" has changed

Then, now need replace "HTML.header" and "HTML.header_level1" and "HTML.header_level2", because this all is inside "HTML.header". This is the original approach.

The other change

HTML.header_level2.tag = "div"

The proxy change

set( target, key, value ) {
      this.keys = {};
      target[ key ] = value   

 console.log( target.id ) // header_level2

Then, "HTML.header_level2" has changed

Now, need replace only "HTML.header_level2" inside "HTML.header_level1"

DOM.get( "#header_level1" ).innerHTML = MOD.mount("header_level2")

Is that the idea?

thipages commented 5 years ago

Yes ! But Thats only ONE point that I propose to change. The other one is to update independently of Dom hierarchy

Gurigraphics commented 5 years ago

As independently ?

<div id="app">
  <div id="header" class="header">
    <ul id="header_level1">
    <li id="header_level2">content_2</li>
    </ul>
  </div>
</div>

header_level2 is inside header_level1 And this information needs to be found somewhere to append in this local.

thipages commented 5 years ago

no matter with the ID equals to the HTML_key

var DOM = new Dominus();
// UID is generated behind the scene creating HTML[generatedUID] corresponding to myElement
var myElement = DOM.element ({tag:'div',class:'myClass'}); 
DOM.class.add(myElement.id, 'myClass2'); // or simplier DOM.class.add(myElement, 'myClass2');
Gurigraphics commented 5 years ago

When an element changes, it changes inside a local: body, #app, div, etc.

<div id="1">
<div id="2"></div>
</div>

And you need to know that "2" is inside of "1" and no inside in "#app".

AddClass is like this:

Time 1 get parent

<div id="1">
<div id="2"></div>
</div>

Time 2 delete old child

<div id="1">
</div>

Time 3 add new child

<div id="1">
<div id="2" class="myClass2"></div>
</div>

So, change 2, also change 1. Because this is inside and is not independent.

thipages commented 5 years ago

This is exactly what I propose to improve I propose you that you code the first point

DOM.get( "#header_level1" ).innerHTML = MOD.mount("header_level2")

Then I will propose you a pull request for the second for review

Gurigraphics commented 5 years ago

Other example:

<div id="a0">
  <div id="a1"></div>
  <div id="a2"></div>
  <div id="a3"></div>
</div>

Replace class is simple. Is native:

document.getElementById("a2").classList.add('MyClass');

Is like:

DOM.get("#a2").classList.add('MyClass');

However, this only changes in the DOM. So you do not have control of the state of the application. Because this information does not save on any object.

And changing "tag" is different.

If change element "a2" for tag "ul", you no can simply inner or append or prepend in "a0". Because you need to know the other childs. Otherwise it will append in the wrong place or will delete them. And you do not have this information in this object "a2" .

thipages commented 5 years ago

ok, I see the point. Next step, so?

Gurigraphics commented 5 years ago

My conclusion is that the two extremes are wrong.

Then, always that "child" change -> re-render "parent" and all "childs".

And you can know that by name.

header_element header_element_child1 header_element_child1_child2 header_element_child1_child2_child3

If change: "header_element_child1_child2_child3" Then, re-render: "header_element_child1_child2" And no all group like before.

thipages commented 5 years ago

I think I agree so far. Thanks

However, code is not reflecting this currently, I see those changes in the code

  1. add method needs to be extended to every HTML_key (under the scene)
  2. currentPath needs to update the first immediate parent and not the root
Gurigraphics commented 5 years ago

Yeah. That's why it's not work.

HTML.header_contentList_2_0_0 = [
 { tag:"li", id: "header_contentList_2_0_1", html: "0",  class: "hide" },  
 { tag:"ul", id: "header_contentList_2_0_2", html: "header_contentList_3_0_0",  class: "hide" },
] 

HTML.header_contentList = [
 { tag:"li", id: "header_contentList_1", html: "0",  class: "hide" },  
 { tag:"li", id: "header_contentList_2", html: "header_contentList_2_0_0  99",  class: "hide" },
] 

HTML.header = { 
  tag:"ul", 
  id: "header", 
  html: "header_contentList"
}  
DOM.add("header", "#app")

If change "header_contentList_2", this is inside "header_contentList". And "header_contentList" is not an element div to re-render. This is an array. Then, you need to go back one more level, to re-render "HTML.header".

And everything comes back as it was before.

Gurigraphics commented 5 years ago

Others conclusions

HMTL.header
HMTL.header_array
HMTL.header_array_child1 (if change -> if object -> removeName(_array_child1) update (HMTL.header) 
HMTL.header_array_child2 (if change -> if object -> removeName(_array_child2) update (HMTL.header) 
HMTL.header_array_child2_array
HMTL.header_array_child2_array_child1 (if change -> if object -> removeName(_array_child1) update (HMTL.header_array_child1) 
HMTL.header_array_child2_array_child2 (if change -> if object -> removeName(_array_child2) update (HMTL.header_array_child2)

HMTL.header_array              (if change -> if array -> removeName(_array) update (HMTL.header) 
HMTL.header_array_child2_array (if change -> if array -> removeName(_array) update (HMTL.header_array_child2) 

Object Exceptions

HTML.header_array_child2_object_array_child2_object = {  //is object
  id: "header_array_child2_object_array_child2_object"
} 

HTML.header_array_child2_object_array = [     //is array
 { id: "header_array_child2_object_array_child1" },
 { id: "header_array_child2_object_array_child2" html: "header_array_child2_object_array_child2_object" }
] 

HMTL.header_array_child2_object = { //is object
  id: "header_array_child2_object" html: "header_array_child2_object_array"
}  

HTML.header_array = [           //is array
 { id: "header_array_child1" },
 { id: "header_array_child2" html: "header_array_child2_object"  }
] 

HTML.header = { 
  html: "header_array"
}  
DOM.add("header", "#app")

HMTL.header_array_child2 (if change -> if object -> removeName(_array_child2) update (HMTL.header) HMTL.header_array_child2_object (if change -> if object -> removeName(_object) update (HMTL.header_array_child2)

Result:

1- HMTL.header_array_child2        -> if object -> removeName(_array_child2)
2- HMTL.header_array_child2_object -> if object -> removeName(_object)

The two are objects and require diferent "update logic". Then, this logic (is "object", is "array") no work. Logic fail

Other logic

HMTL.header_array_child2        
  (if change -> if has 2"_" -> remove 2"_" -> removeName(_array_child1) update (HMTL.header) 

HMTL.header_array_child2_object 
  (if change -> if has 3"_" -> remove 1"_" -> removeName(_object) update (HMTL.header_array_child2)

HMTL.header_array_child2_object_array_child1 
  (if change -> if has 5"_" -> remove 2"_"  -> removeName(_array_child1) -> update (HMTL.header_array_child2_object) 

HTML.header_array_child2_object_array_child2_object
  (if change -> if has 6"_" -> remove 1"_"  -> removeName(_object) -> update (HTML.header_array_child2_object_array_child2) 

Simplify

if has 2"_" -> remove 2"_"
if has 3"_" -> remove 1"_"
if has 4"_" -> remove 1"_"
if has 5"_" -> remove 2"_"
if has 6"_" -> remove 1"_"
if has 7"_" -> remove 1"_"

Much Exceptions. Logic fail

Virtual Dom the return

Then, return for the need of the "virtual DOM" to control "childs" and "parents".

For example, store "id" and "parent" in object

var myElement_anyname2 = [      
 { parent: "anyid" },
 { parent: "anyid" }
] 

var myElement_anyname = [          
 { parent: "myElement" },
 { parent: "myElement", id: "anyid", html: "myElement_anyname2" }
] 

var myElement = { 
  html: "myElement_anyname"
}  

if( change ) -> get parent -> update parent

Conclusion

Apparently everything looks great.

However, it simplifies the syntax, but returns the problem of "excess mental ram memory" to organize the code.

One analogy is can put "biology books" on the shelf of the "geography library", with unnamed shelves and books.

Is like simplify the word "book_of_geography_of_mars" to "book" or "geography". Then, not know if the book is geography or mars, or a book or table. It improves the syntax and detonates the memory and knowledge.

And the more complex the project, the more you waste time thinking and find nothing. And that's what all frameworks already do (mainly in javascript).

The middle ground

And if join "parent" and "naming conventions"...

"HTML.header_element"
parent: "HTML.header"

"HTML.header_element_list"
parent: "HTML.header"

It seems to be a solution. But at the moment I'm not interested to create it. hehe I think it's a lot of work for little return. Since I already can control the size of groups or territories, I do not need them.

But if someone wants to create this can create. It's something that can improve some milliseconds of difference. Or, the worse the design of the project, better will be the improvement. Because they can put all in the same territory and create any spaghetti that the framework handles updating.

thipages commented 5 years ago

By the way, could be interesting to write a simple application like TODO MVC, Helping you select an MV* framework or other. Todo app is probably too simple to show the "excess mental memory" issue of other framework but may be a good start

Gurigraphics commented 5 years ago

Yes. My concern is more about "mental model". The "'M" of MVC. MVC does not help much when we choose inappropriate names to organize the project. Because we stay with three dishes of spaghetti.

In choosing names "quantity" can represent "quality". As I wrote before, it is possible to simplify "add_book_of_geography_in_screen" to "add". But then it is not possible to know what it adds or where. And comments often more pollute the reading of the code that help.

And it decrease code size and detonates "memory" and "understanding." It looks like a library with the books on the wrong shelves.

The more you add books -> the more chaos increases. The more complex the project and the worse the names -> the more time thinking and trying to understand and find things. The longer you do not deal with the project -> the more you forget or do not remember.

And although this sounds obvious, there is no quality in applying it. And even more so in javacript.

So I always try to separate everything into modules: EVENTS, DATA, SERVER, Etc And I wanted to do that too with HTML.

This "mental model" or "analogy" of library related to "physical library" does not even exist anymore. Likewise the M of the "MVC" also has no more association with "mental model".

"The essential goal of MVC is to bridge the gap between the mental model of the human user and the digital model in the computer "(Trygve Reenskau, 1979)

So if there are "bad mindset" that generate "bad practices"  this is due to the simplifications that also occur in education.

Anyway, after much breaking our heads one hour we learned.

thipages commented 5 years ago

The essential purpose of MVC is to bridge the gap between the human user's mental model and the digital model that exists in the computer. The ideal MVC solution supports the user illusion of seeing and manipulating the domain information directly. The structure is useful if the user needs to see the same model element simultaneously in different contexts and/or from different viewpoints.

http://heim.ifi.uio.no/~trygver/themes/mvc/mvc-index.html

thipages commented 5 years ago

So how to have a minimalist DOMinus application that shows its benefits?

Gurigraphics commented 5 years ago

Yes. This is the reference.

So how to have a minimalist DOMinus application that shows its benefits?

It is difficult to speak of the benefits without practical use. It's like trying to convince that apple is good without eating. What I plan to do is create an example project. But I doubt it's enough. Because in addition to benefits there are also personal preferences.

However, I need the Dominus to develop Plurality. Within Plurality perhaps everything becomes clearer. Organizing the code using "visual blocks" is the next level.

My inspiration to this is Stencyl and Smalltalk (from which came the concept of MVC).

This video presents this ideas. The recording that got bad. https://www.youtube.com/watch?v=ZVE3DiOzmAI

thipages commented 5 years ago

lets try to implement a example project. Need a starting point.

Gurigraphics commented 5 years ago

I sent a draft in the demo folder https://github.com/Gurigraphics/DOMinus.js/issues/22

thipages commented 5 years ago

Following our current thread discussion, I have written a quick and dirty code. It has no helper method, only object manipulation and written in typescript,.

However bin/hypertit.js is operational and can be placed in an html file. API is in /src

Can you tell me what it is wrong with such approach from your point of view? Thank you very

Gurigraphics commented 5 years ago

Different is not wrong. It's just different.

  let ht=window.ht;
    ht.callback=callback;

    let li1 = ht
        .addNode("li")
        .addChild("li1");

    let li2 = ht
        .addNode("li")
        .addChild("li2")
        .addEvent("mouseover",()=>console.log("li2 mouseover"));

    let ul = ht
        .addNode("ul")
        .addChild(li1)
        .addChild(li2)
        .addStyle("background-color","#cecece");
    ht.build(ul);

    let li3 = ht
        .addNode("li")
        .addChild("li3")
        .addEvent("click",()=>console.log("li3 click"));
    ul.addChild(li3);

    function callback(node,type,action,data) {
        console.log(node,type,action,data);
    }

I think how much more simpler is better.

// -----------------------------------------
// How this work - This is the part that matters least.
// -----------------------------------------
function createNode( div, tag, array ){
  template = ""
  for( index in array){
    template += `<`+tag+`>`+ array[ index ] +`</`+tag+`>`
  }
  var result = `<`+div+`>`+ template +`</`+div+`>` 
  return result 
}
`
// -----------------------------------------
// How use this - This is the part that matters most.
// -----------------------------------------
var array = [ node1, node2, node3, node4, node5 ]
var node = createnode("ul", "li", array )  
$("#app").append(node)

To use when something is not simpler than Jquery, better continue with Jquery. But to learn, how much more things you try make, better.

thipages commented 5 years ago

That was not the point. As I said, I have not yet implemented helpers. I will implement helpers, this will be clearer.

Gurigraphics commented 5 years ago

The code is better than mine. Let's see the usability then.

thipages commented 5 years ago

Thanks.

What I find "magic" in DOMinus is the array management through proxies. I dont know how work all array operations with DOMinus. Unit tests will help

Also the fact that it uses a minimal set of DOM method (innerHTML, getElementByxxx and now attributes), the rest is directly proxied.

For usuabilty, I thought about two modes

Gurigraphics commented 5 years ago

In the proxy I put a trap to intercept delete. In traditional way if use "delete HTML.element_memberList" this no delete. Because this no exist. The element is inside array "delete HTML.element_memberList[0]". Then, now native delete continues in DOM.remove(key), and then find parent and delete by id.

And missing separate "id" from DOM of "UID" from element or name. Because it's still mixed up. Then you no need set "id" in "arrayMemberList".

Mithril like mode no work more. Now maybe change to make something like that

var h = DOM.mount
var template = h({ 
  tag: "div", 
  html: h({ tag: "div", html: "value" })
})
DOM.add(template, "#app")

Can be used when no is required data persistence.

Or then:

var HTML = DOM.HTML
var TEMPLATE = DOM.TEMPLATE
var h = DOM.mount

TEMPLATE.name = h({ 
  tag: "div", 
  html: h({ tag: "div", html: "value" })
})
HTML.element = {
  tag: "div",
  template: "name"
}
DOM.add( "element", "#app")
Gurigraphics commented 5 years ago

Or :

TEMPLATE.base = function( value ){
  return h({ 
      html: h({ html: value })
  })
}
TEMPLATE.name1 = TEMPLATE.base("name1")
TEMPLATE.name2 = TEMPLATE.base("name2")
HTML.element = { template: "name1" } 

P.s: I always forget that "div" does not need a tag.

Gurigraphics commented 5 years ago

This your idea is good: ul.addChild (li3); Dominus would look like this HTML.ul.push(li3) But it will not always work on any object. Only in arrays.

There also is another approach:

Create the node only once and stringify and save it like template: var dataSaved = "

<div id={{value that change }}>{{value that change }}
"

Then findReplace {{}} = "

template1
"

Ember, angular, vue, etc use something like that. I do not know how much that would influence performance or can be useful.

thipages commented 5 years ago

Ok thanks for the tip. About performance, it can/will be a bootleneck or even deadend You know how to define and measure performance?

Gurigraphics commented 5 years ago

To functions: http://jsben.ch/MXFT3 To fullpage load in Google chrome tools show times: Scripting, Rendering, Painting, etc.

This jsben the result always appears different. It only serves to get a sense of difference. http://jsben.ch/qSd7X

Gurigraphics commented 5 years ago

I did another test. https://github.com/Gurigraphics/DOMinus.js/issues/22

I used this to mount childs:

FACTORY.base = function( newID, value ){ 

  var div = h( [
    { tag: "div", html: value },
    { tag: "button", parent: newID, html: "X", onclick: "EVENTS.removeItem( event )"},
  ])

  return newDiv = { tag:"div", id: newID, html: div }
}

For full reactive would be used this:

FACTORY.base = function( newID, value ){ 

  HTML[ newID ] =  [
    { tag: "div", html: value },
    { tag: "button", parent: newID, html: "X", onclick: "EVENTS.removeItem( event )"},
  ]

  return newDiv = { tag:"div", id: newID, html: "new" }
}

With this, all those childs would have unnecessary proxy observers, and that number would grow exponentially, because each attribute generates a new proxy to be able to be observed.

So it is much better to have control of which group or element is reactive and which group or element will not need any reactivity.

Something obvious, but that was not clear.

Gurigraphics commented 4 years ago

I add the missing methods

Methods like Jquery - without reactivity

var $ = DOM.get

$("#id").removeClass("foo") 
$(".class").addClass("foo")   
$("body").hasClass("foo") 
$("#id").toogleClass("foo") 
$("#id").hide() 
$("#id").show() 
$("#id").val() 
$("#id").html("text") 
$("#id").append("<div>after</div>") 
$("#id").prepend("<div>before</div>") 

Methods js native

$("#input").id
$("#input").name
$("#input").classList
$("#input").children
$("#input").childNodes
$("#input").parentNode
$("#input").parentElement
$("#input").nextElementSibling
$("#input").previousElementSibling
Etc

Childs

Childs look better in an array.

HTML.child1 = {
  html: "child1"
}
HTML.child2 = {
  html: "child2"
}
HTML.element = {
  html: ["child1", "child2"]
}

Oftentimes only the "parent-component" can have reactivity. So, instead of all be object, the childs can be only string:

var child1 = h({  html: "child1" })
var child2 = h({  html: "child2" })
HTML.box = {
  id: "box,
  html: child1+child2
}
Gurigraphics commented 4 years ago

Removed conventions of names

TERRITORiality

All the childs of an element receive the tag "parent" with the parent name.

 HTML.child1 = { 
   html: "content"
 } 
 HTML.header = {
   id: "header", 
   html: "child1"
 } 
 DOM.add("header", "#app")

 Result: 
<div id="header">
   <div parent="header">content</div>
</div>

Then, when a child is changed, parent is updated.

To remove an element:

 delete HTML.child1