GithubHelp home page GithubHelp logo

petit-dom's Introduction

petit-dom

A minimalist virtual DOM library.

  • Supports HTML & SVG elements.
  • Supports Render functions and Fragments.
  • Custom components allows to build your own abstraction around DOM elements.
  • Directives allows you to attach custom behavior to DOM elements.

Installation

The library is provided as a set of ES modules. You can install using npm or yarn and then import from petit-dom (see example below).

$ npm install --save petit-dom

or

$ yarn add petit-dom

Note however no transpiled build is provided. The library will work with all recent versions of Node and major browsers. If you're targeting older platforms, make sure to transpile to the desired ES version.

To run the examples, you can run a local web server (like npm http-server module) from the root folder of the project. Since all example use ES6 modules, you can simply navigate to the example you want and load the desired HTML file.

Usage

If you're using Babel you can use JSX syntax by configuring the jsx runtime

{
  "presets": [
    [
      "@babel/preset-react",
      { "runtime": "automatic", "importSource": "petit-dom" }
    ]
  ]
}
import { render } from "petit-dom";

//  assuming your HTML contains a node with "root" id
const parentNode = document.getElementById("root");

// mount
render(<h1>Hello world!</h1>, parentNode);

// patch
render(<h1>Hello again</h1>, parentNode);

Alternatively you can use the classic Babel transform via /* @jsx h */ on the top. You can also use the raw h function calls if you want, see examples folder for usage.

petit-dom also supports render functions

import { render } from "petit-dom";

function Box(props) {
  return (
    <div>
      <h1>{props.title}</h1>
      <p>{props.children}</p>
    </div>
  );
}

render(<Box title="Fancy box">Put your content here</Box>, parentNode);

render functions behave like React pure components. Patching with the same arguments will not cause any re-rendering. You can also attach a shouldUpdate function to the render function to customize the re-rendering behavior (By default props are tested for shallow equality).

Custom components

Besides HTML/SVG tag names, fragments and render fucntions, the h function also accepts any object with the following signature

{
  mount(self);
  patch(self);
  unmount(self);
}

Each of the 3 functions will be called by the library at the moment suggested by its name.

The self argument which is an aboject holding the following properties:

  • render(...): To create/update DOM content for the component
  • props: the current props passed to the JSX element (or h function)
  • oldProps: the previous props, it's value is undefined inside mount

You can also attach arbitrary properties to the object, they will persist between different invocations.

See examples folder for how to define some custom components.

Directives

You can also attach custom behaviors to DOM nodes. Directives allows you to obtain references to DOM nodes and manage their lifecycle.

A directive is an object with the current interface

{
  mount(domElement, value);
  patch(domElement, newValue, oldValue);
  unmount(element, lastValue);
}

There's an example of a simple log directive in the examples folder.

API

h(type, props, ...children)

Creates a virtual node.

  • type: a string (HTML or SVG tag name), or a custom component (see above)

  • props: in the case of HTML/SVG tags, this corresponds to the attributes/properties to be set in the real DOM node. In the case of components, { ...props, children } is passed to the appropriate component function (mount or patch).

render(vnode, parentDom, options = {})

renders a virtual node into the DOM. The function will initially create a DOM node as specified the virtual node vnode and append it to the children ofparentDOM. Subsequent calls will update the previous DOM node (or replace it if it's a different tag).

Optionally, you can use options to pass custom directives, for example:

  let log = { ... }, // defines a log directive
  render(<Something />, parent, { directives: { log } });

petit-dom's People

Contributors

andriichumak avatar dependabot[bot] avatar ikeryo1182 avatar yelouafi avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

petit-dom's Issues

Possible bug: `Uncaught DOMException: Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node.`

Consider this runnable example:

<body><script type="module">

import { h, mount, patch } from "https://unpkg.com/petit-dom?module"

window.onload = ()=> {
    document.body.appendChild(mount(oldNode))
    patch(newNode, oldNode)
}


const oldNode = 
h('div', {}, [
    h('h1', {}, ['TxT']),
    h('div', {}, [
        h('div', {}, [
            h('div', {}, ['TxT']),
            h('pre', {}, [
                h('code', {}, [
                    h('span', {}, ['TxT']),
                    '\n'
                ])
            ]),
            h('div', {}, []),
            h('div', {}, []),
            ""
        ])
    ]),
    h('p', {}, ['TxT']),
    "\n",
    h('pre', {}, [
        h('code', {}, ["TxT"])
    ]),
    h("p", {}, ["TxT"])
])

const newNode = 
h('div', {}, [
    h('h1', {}, ['TxT']),
    h('div', {}, [
        h('div', {}, [
            h('div', {}, ['TxT']),
            h('p', {}, ['``']),
            h('div', {}, []),
            h('div', {}, []),
            h('div', {}, []),
            ""            
        ])
    ]),
    h('pre', {}, [
        h('code', {}, [
            h('span', {}, ['TxT']),
            '\n'
        ])
    ]),
    h('p', {}, ['TxT']),
    "\n",
    h('pre', {}, [
        h('code', {}, [
            h('span', {}, ['TxT']),
            '\n'
        ])
    ])
])

</script></body>

I'll get the error vdom?module:676 Uncaught DOMException: Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node.. This has also happened on numerous occasions before, I usually solve it by adding keys to elements, but in this case I can't do that...

patch fails when root element changes

Hi again! Noticed something unexpected while playing with petit-dom and reduced it to this example:

https://codepen.io/zaceno/pen/qYEQPw?editors=0011 Basically, if the top element is different in two vnodes A and B, then patch(A, B) will do nothing.

I did notice patch returns the new, appropriate element, making it possible for me to swap the old one out for the new one myself. So I'm guessing this is intended behavior, but I didn't see anything about it in the README, and it caught me off guard.

Again, no problem to handle myself, but since it is so easy for a user to handle, perhaps this is something petit-dom could take care of internally? (I could PR it I think, if you're interested)

PS: I double checked in Chrome also this time, before filing an issue ๐Ÿ˜…

Link doesn't render - Bug

Look at this example:

<!doctype html>
<html lang="en">
<head>
  <title>petit-dom Link Bug</title>
</head>
<body>

  <script src="../../dist/petit-dom.dev.js"></script>
  <script>
    const {h, mount} = petitDom

    const vnode = h('div', null, [h('a', {href:'www.example.com'}, ['link'])]);

    document.body.appendChild(mount(vnode));
  </script>
</body>

</html>

The link won't render on the latest Chrome 66. When I edit the element in the inspector it says <a xlink:href="www.example.com">link</a> The xlink is creating the error, it works when I remove the href: XLINK_NS in the vdom.js.

Support for JSX Fragment

Is there a way to use fragments with petit-dom?

export default <>
  <h1></h1>
  <h2></h2>
</>;

I'm trying to use petit-dom to render into a shadow-dom, and it works great, but having a single parent is not ideal because my shadow dom ends up having an unnecessary wrapper element. I know other vdom libraries support this, but I'm not sure about the tradeoffs, if any.

keep input-cursor position between patches?

Please see this example on codepen: https://codepen.io/zaceno/pen/BxaENr?editors=0010

I'm rendering a plain text-input, and expect to capture the value every oninput. Each time I update (patch) the ui. (A quite normal scenario, I think. For every input, you want to do some validation, helpful formatting, et c. Hence: update the ui).

It works fine except when you try to enter something in the middle or beginning of the input. In that case, the cursor jumps to the end of the input. Not entirely unexpected, since I'm rerendering and the previous cursor position isn't kept track of anywhere.

So, I could correct for this by using a custom component class where I can store the cursor position in the component state between renders. But that seems like overkill for something so trivial.

Is there some simpler way I could handle this, so I can still use a plain h('input', {...}) without making a component? If not, may I suggest keeping track of cursor position as a feature-request?

Receive old props and content in component patch method

It seems that the only way to keep track of changes to props in component (e.i. why patch was called) is to put old data on DOM object custom property, which seems wrong.

Would it makes sense to pass old props / content to patch method of custom component? Something like componentInstance.patch(domNode, newProps, newContent, oldProps, oldContent). This way developer could figure out what exactly has changed and apply targeted patch on DOM.

I realize this leads to a wider topic of component internal state, which defeats the purpose of micro library. You probably want to keep it simple. On the other hand old props and content are available when patch is called anyway, so why not?

I could make a PR if you think this is a good idea.

Bug - Can't handle nodes with with empty strings or strings with only spaces.

Hi @yelouafi the example I sent before doesn't work with strings with spaces also...

<body>
<script type="module">
import {h, patch, mount} from 'https://unpkg.com/petit-dom?module'

const oldVnode =  h("div", {}, [
  h("div", {}, [
    h("h1", {}, ["Text"]),
    h("div", {}, ["Text"]),
    h("div", {}, ["Text"]),
    h("div", {}, ["Text"]),
    h("div", {}, ["Text"])
  ]),
  h("div", {}, [
    h("div", {}, [
      "   ",
      h("div", {}, ["Text"]),
      h("pre", {}, [h("code", {}, [h("span", {}, ["Text"]), " "])]),
      h("div", {}, []),
      h("div", {}, []),
      " "
    ])
  ]),
  h("p", {}, ["Text"]),
  " ",
  h("pre", {}, [h("code", {}, ["Text"])]),
  h("p", {}, ["Text"])
]);

const newVnode = h("div", {}, [
  h("div", {}, [
    h("h1", {}, ["Text"]),
    h("div", {}, ["Text"]),
    h("div", {}, ["Text"]),
    h("div", {}, ["Text"]),
    h("div", {}, ["Text"])
  ]),
  h("div", {}, [
    h("div", {}, [
      " ",
      h("div", {}, ["Text"]),
      h("p", {}, ["Text"]),
      h("div", {}, []),
      h("div", {}, []),
      " "
    ])
  ]),
  h("pre", {}, [h("code", {}, [h("span", {}, ["Text"]), " "])]),
  h("p", {}, ["Text"]),
  " ",
  h("pre", {}, [h("code", {}, [h("span", {}, ["Text"]), " "])])
]);

document.body.appendChild(mount(oldVnode))
patch(newVnode, oldVnode)
</script>
</body>

Rendering array of vnodes

Thanks for this great work! Is this the fastest virtual dom on the web? :)

Is there an easy way to render an array of vnodes straight to document.body for example?
Or does this require separate mounting of each element?

Unmount of component not called?

Hi!

See here: https://codepen.io/zaceno/pen/WJwmge

I'm essentially trying to define a component <FadeOut> which ... you guessed it: fades out it's content.

My idea was to make a basic "pass-through" component like this:

class FadeOut {
    mount(_, child) {
        this.vnode = child
        return mount(child)
    }
    patch (_, __, ___, child) {
        var oldVNode = this.vnode
        this.vnode = child
        return patch(this.vnode, oldVNode)
    }
    unmount (el)ย {
        console.log('UNMOUNTING', this)
    }
}

...And then (hopefully) I would somehow be able to access the element of the child (perhaps by capturing it earlier in mount or patch) so I could fade it out slowly before removing it for real.

But apparently unmount is not even called! Is this a bug, or am I missing something?

Hydrate existing markup

How can I reuse existing markup from within the container that is being patched? I'm asking because I'm looking into SSR with hydration. Thanks!

broken vdom indexOf ?

The following would log -1 in console, instead of 6:

function indexOf(a, suba, aStart, aEnd, subaStart, subaEnd, eq) {
  var j = subaStart,
    k = -1;
  var subaLen = subaEnd - subaStart + 1;
  while (aStart <= aEnd && aEnd - aStart + 1 >= subaLen) {
    if (eq(a[aStart], suba[j])) {
      if (k < 0) k = aStart;
      j++;
      if (j > subaEnd) return k;
    } else {
      k = -1;
      j = subaStart;
    }
    aStart++;
  }
  return -1;
}

var slong = 'happy cat in the black';
var sshort = 'cat in the';

indexOf(
  slong,
  sshort,
  0,
  slong.length - 1,
  0,
  sshort.length - 1,
  (a, b) => a === b
);

However, with slong = 'cat in the' and sshort = 'in' the result is 4.

The reason is that the char by char parse stops when a match is found but, while keep looping, aEnd - aStart + 1 >= subaLen becomes false so that last part of the string is never checked if the right boundary is not as long as the searched string itself.

Is this meant to behave this way? If that's the case, wouldn't be wiser to call the function halfIndexOf ?

Thanks for any sort of clarification

Changing VNode type at root fails

Changing type of VNode at root fails.
Version: 0.3.3

`
import { h, render } from "petit-dom";

const parentElement = document.getElementById("app");

render(h("h1", null, "Hello"), parentElement);
render(h("h2", null, "Hello"), parentElement);
`

The code above makes the browser complain

TypeError: Cannot read property 'childNodes' of undefined

Here is a link to sandbox for reproducing: https://codesandbox.io/s/confident-knuth-o41be

New DOM-tree is not synced with vdom-tree, leads to exception.

Hi! I might found another bug, I still get "Failed to execute 'replaceChild' on 'Node': The node to be replaced is not a child of this node." sometimes, and I think that this time it is because to the vdom-tree is not the same as the DOM-tree after a patch. Take a look at this example:

<body>
    <div id="app-root"></div>
    <script type="module">
    import {h, patch, mount} from 'https://unpkg.com/petit-dom?module'
    
    
    var oldVnode = 
    h('div', {}, [
        h('p', {}, [
            'Text',
            h('code', {}, ['Text']),'Text',
            h('code', {}, ['Text']),'Text'
        ]),
        h('div', {}, [])
    ])

    var newVnode = 
    h('div', {}, [
        h('p', {}, [
            h('a', {}, ['Text'])
        ]),
        'Text',
        h('p', {}, [
            h('a', {}, ['Text'])
        ]),
        'Text',
        h('p', {}, [
            'Text',
            h('a', {}, ['Text']),
            'Text'
        ]),
        h('div', {}, [])
    ])

    document.querySelector('#app-root').appendChild(mount(oldVnode))

    patch(newVnode, oldVnode)

    /* Will render this as HTML, the first "code" should be a "link tag"

        <div>
            <p>
                <code>Text</code>
            </p>
            Text
            <p>
                <a>Text</a>
            </p>
            Text
            <p>
                Text
                <a>Text</a>
                Text
            </p>
            <div></div>
        </div>

    */


    function checkSimlilarity(vdomNode, domnode){
        if(domnode.nodeType === 3) {
            const textErr = domnode.textContent !== vdomNode._text
            if(textErr) {
                console.error(vdomNode, domnode)
                throw 'Text not the same'
            }
            return
        }
        const lenErr = vdomNode.content.length !== domnode.childNodes.length
        const typeErr = vdomNode.type.toLowerCase() !== domnode.tagName.toLowerCase()
        if(lenErr){
            console.error(vdomNode, domnode)
            throw 'Wrong number of children'
        }
        if(typeErr){
            console.error(vdomNode, domnode)
            throw 'Wrong type'
        }

        for(let i = 0 ; i < vdomNode.content.length; i++){
            checkSimlilarity(vdomNode.content[i], domnode.childNodes[i])
        }
    }

    checkSimlilarity(newVnode, document.querySelector('#app-root').firstChild)
      
    
    </script></body>

So the vdom-library thinks that the child of the first p-tag is an a-tag, but it actually is code-tag. Correct me if I'm wrong, but what I think happens is that the subsequent patch will then raise the exception.

Bug - Uncaught DOMException: Failed to execute 'insertBefore' on 'Node'

@yelouafi I think I've found another one, I've created a tool that outputs the vdom trees that failed to patch, and recursively convert them into javascript strings :) Edit: I think it has to do with empty strings, i filtered them out from contents in patch and mount, and then it'll work.

<body>
<script type="module">
import {h, patch, mount} from 'https://unpkg.com/petit-dom?module'

const oldVnode =  h("div", {}, [
  h("div", {}, [
    h("h1", {}, ["Text"]),
    h("div", {}, ["Text"]),
    h("div", {}, ["Text"]),
    h("div", {}, ["Text"]),
    h("div", {}, ["Text"])
  ]),
  h("div", {}, [
    h("div", {}, [
      "",
      h("div", {}, ["Text"]),
      h("pre", {}, [h("code", {}, [h("span", {}, ["Text"]), ""])]),
      h("div", {}, []),
      h("div", {}, []),
      ""
    ])
  ]),
  h("p", {}, ["Text"]),
  "",
  h("pre", {}, [h("code", {}, ["Text"])]),
  h("p", {}, ["Text"])
]);

const newVnode = h("div", {}, [
  h("div", {}, [
    h("h1", {}, ["Text"]),
    h("div", {}, ["Text"]),
    h("div", {}, ["Text"]),
    h("div", {}, ["Text"]),
    h("div", {}, ["Text"])
  ]),
  h("div", {}, [
    h("div", {}, [
      "",
      h("div", {}, ["Text"]),
      h("p", {}, ["Text"]),
      h("div", {}, []),
      h("div", {}, []),
      ""
    ])
  ]),
  h("pre", {}, [h("code", {}, [h("span", {}, ["Text"]), ""])]),
  h("p", {}, ["Text"]),
  "",
  h("pre", {}, [h("code", {}, [h("span", {}, ["Text"]), ""])])
]);

document.body.appendChild(mount(oldVnode))
patch(newVnode, oldVnode)
</script>
</body>

Output:

bugReport.html:1 Uncaught DOMException: Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node.
    at applyDiff (https://unpkg.com/[email protected]/src/vdom?module:676:14)
    at diffOND (https://unpkg.com/[email protected]/src/vdom?module:643:3)
    at diffChildren (https://unpkg.com/[email protected]/src/vdom?module:459:18)
    at patchContent (https://unpkg.com/[email protected]/src/vdom?module:250:5)
    at patch (https://unpkg.com/[email protected]/src/vdom?module:225:7)

Cannot read property 'childNodes' of undefined

Consider this runnable example:

<body>
<div id="root"></div>
<script type="module">
    
import {h, patch, mount} from 'https://unpkg.com/petit-dom?module'

var oldVnode = h('div', {}, [null,null,null,h('style', {}, []),h('div', {}, [h('div', {}, [h('div', {}, [h('div', {}, []),h('div', {}, [])]),h('div', {}, [h('div', {}, []),h('div', {}, [])]),h('div', {}, [h('div', {}, []),h('div', {}, [])])]),h('div', {}, [h('div', {}, [null]),null,null,h('div', {}, [null])])])])

var newVnode = h('div', {}, [null,null,null,h('style', {}, []),h('div', {}, [h('div', {}, [h('div', {}, [h('div', {}, []),h('div', {}, [])]),h('div', {}, [h('div', {}, []),h('div', {}, [])]),h('div', {}, [h('div', {}, []),h('div', {}, [])])]),h('div', {}, [h('div', {}, [null]),null,null,h('div', {}, [null])])])])

var root = document.getElementById('root')
root.appendChild(mount(oldVnode))
patch(newVnode, oldVnode, root)

</script>
</body>

Output

vdom.js?module:238 Uncaught TypeError: Cannot read property 'childNodes' of undefined
    at patchChildren (vdom.js?module:238)
    at patch (vdom.js?module:85)
    at patchInPlace (vdom.js?module:216)
    at patchChildren (vdom.js?module:255)
    at patch (vdom.js?module:85)
    at bugProgramTemplatePatchFalied.html:12

Null doesn't remove previous node

Consider this runnable example:

<body>
<div id="root"></div>
<script type="module">
    
import {h, patch, mount} from 'https://unpkg.com/petit-dom?module'

var oldVnode = h('div', {}, ['Hello there!'])
var newVnode = h('div', {}, [null])

var mounted = mount(oldVnode)
var root = document.getElementById('root')
root.appendChild(mounted)

patch(newVnode, oldVnode, mounted)

</script>
</body>

Output

<div id="root"><div>Hello there!</div></div>

Shouldn't this remove the textnode? So you can do isSomething ? Vdom() : null

Handle input type="range"

Hi again, sorry to spam you :)

I'm trying to get input type="range" to work, but I believe it triggers the updateInput() on line 52 in vdom.js. The setProps() function is called on line 61 with null as oldProps, so all the min, and max attribute will always be updated even if they need not to...

Allow conditional rendering

It is very handy to be able to conditional rendering, like

isOpen && Component()
list.length && list.length.toString()

It is not possible right now. This is how I solved it in h.js. Should we add this to the library?

function noNode(arg){
  return arg === null || arg === false || arg === undefined || arg === 0
}
 export function maybeFlatten(arr, isSVG) {
  for (var i = 0; i < arr.length; i++) {
    var ch = arr[i];
    if (isArray(ch)) {
      return flattenChildren(arr, i, arr.slice(0, i), isSVG);
    } else if (!isVNode(ch)) {
      arr[i] = { _text: ch == null ? "" : ch };
      arr[i] = { _text: noNode(ch) ? "" : ch };
    } else if (isSVG && !ch.isSVG) {
      ch.isSVG = true;
    }

Documentation for shouldUpdate

Hi!
I'm trying to use petit-dom with render functions, and I would like to be able to control if the algorithm should stop patching at a particular position in the vdom tree. What do you mean with "You can also attach a shouldUpdate function to the render function"?

I saw in the source that the type could either be a function or a string. I'm using the api right now like this:

var ButtonComponent = () => 
petit.h('button',{
 onclick:function(){},
 style: 'background:green' 
}, ['button'])

And then using the button somewhere
h('div', {}, [ButtonComponent()]

How should I tell a component to ignore parts of the dom, for example that have been modified externally?

Why remove O (nlogn) algorithm?

I see that the new version has become o (n) algorithm. Why?

In the past, I always thought that the advantage of Petit is that it uses the algorithm of the longest common subsequence, why to remove it?

What is the trade-off, or is there any benchmark for you to do so?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.