fiduswriter/diffDOM

Nothing happens when I call diff.

redbastie opened this issue · 25 comments

I'm just trying to diff the fetched response of a PHP script via javascript.

I have an index.php script which contains a uniqid() call inside the h1 tag:

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title>Hello, world!</title>
</head>
<body>
    <h1><?php echo uniqid(); ?></h1>

    <button onclick="morph()">Morph</button>

    <script src="diffDOM.js"></script>
    <script src="morph.js"></script>
</body>
</html>

The diffDOM.js is from here: https://github.com/fiduswriter/diffDOM/blob/gh-pages/browser/diffDOM.js

Here are the contents of my morph.js file:

function morph() {
    const domParser = new DOMParser();

    const currentHtml = document.documentElement.innerHTML
    const currentDom = domParser.parseFromString(currentHtml, 'text/html')

    fetch('index.php').then(response => {
        return response.text()
    }).then(newHtml => {
        const newDom = domParser.parseFromString(newHtml, 'text/html')
        const dd = new diffDOM.DiffDOM();
        const diff = dd.diff(currentDom, newDom)

        dd.apply(currentDom, diff)

        console.log('should be morphed')
    })
}

When the button on the page is pressed, I want just want it to change the h1 value. I've console.log on the currentDom and newDom variables and they WERE different. Unfortunately, nothing happens on the actual page itself, and I receive no console errors.

Why isn't this working? What am I doing wrong?

Hey, could you make a minisite with all those things included? For example on a site such as https://jsfiddle.net/ .

What is the output of

console.log(diff) after the line that goes const diff = dd.diff(currentDom, newDom)?

So I added this to my morph.js:

console.log(currentDom)
console.log(newDom)
console.log(diff)

Here is what I get in the console:

[Log] #document (morph.js, line 14)
<html>
<head>…</head>
<body>
<h1>5ff35126b376c</h1>
<button onclick="morph()">Morph</button>
<script src="diffDOM.js"></script>
<script src="morph.js"></script>
</body>
</html>

[Log] #document (morph.js, line 15)
<!DOCTYPE html>
<html lang="en">
<head>…</head>
<body>
<h1>5ff35127daf24</h1>
<button onclick="morph()">Morph</button>
<script src="diffDOM.js"></script>
<script src="morph.js"></script>
</body>
</html>

[Log] [] (0) (morph.js, line 16)

Note the difference between <h1>5ff35126b376c</h1> and <h1>5ff35127daf24</h1> which isn't being applied :(

And why do you do:

const newDom = domParser.parseFromString(newHtml, 'text/html')
const dd = new diffDOM.DiffDOM();
const diff = dd.diff(currentDom, newDom)

rather than just

const dd = new diffDOM.DiffDOM()
const diff = dd.diff(currentDom, newHtml)

Please see my reply above for the log output.

Yes, I saw that. But why do you create dom nodes rather than just diffing the HTML string itself? diffDOM will convert it into a virtual dom anyway, so it is not helped by you giving it dom nodes rather than HTML to work with.

This:

const diff = dd.diff(currentHtml, newHtml)

Now throws this error in the console:

Unhandled Promise Rejection: Error: Top level nodes have to be of the same kind.

Same error for this:

const diff = dd.diff(currentDom, newHtml)

Yes, your currentHtml is the child of the document whereas the newDom is a document instance. The document is not a dom node. The child is one. So you need to diff the HTML nodes or the body nodes or the equivalent HTML, not the document instances.

https://developer.mozilla.org/en-US/docs/Web/API/DOMParser shows you that you get a document instance in return, not a dom node.

If this doesn't help and you need more help, please create a jsfiddle for me to inspect.

Can you tell me what library would diff an entire document if this one can't?

No, I don't think you understood. You basically try to diff what is available under window.document rather than window.document.firstElementChild. It's like asking for the difference between my garage and my neighbor's car - one is a car and the other one is a garage, so therefore they are not comparable. What you'll likely want to do is look at the difference between your car and your neighbor's car. I doubt you'll find some specialist that will tell you what the difference between both car and garage are simultaneously.

I have a garage with a car in it.

My car was blue, but in the blink of an eye someone painted it red.

When blink my eyes and look at the garage, I want to see that the car is now red, without changing the whole garage.

Right, so you are comparing the cars inside the garage, not the garages. That's also what you need to do here.

If you compare the garages instead, they may be very different - for example one could have bikes inside of it rather than cars. Or Steve Jobs may be starting Apple in there. And then how do you compare that?

The document could be XHTML rather than HTML, for example. But I doubt that's what you are interested in, right?

What you want to compare are the HTML nodes or the body nodes.

The problem is that fetch is grabbing the whole garage, but I only want to diff the changes to the car and not the whole garage on the page.

This is for server side rendering...maybe I'm approaching this wrong.

How about exchanging:

        const diff = dd.diff(currentDom, newDom)

        dd.apply(currentDom, diff)

        console.log('should be morphed')
        

with

        const diff = dd.diff(currentDom.firstElementChild, newDom.firstElementChild)

        dd.apply(currentDom.firstElementChild, diff)

        console.log('should be morphed')

Unless you are switching back and forth between XHTML, HTML and SVG as the window.document, this is likely what you really should be doing.

If I do that, is it going to replace the entire element, or only the changes within that element?

The changes within that element and the attribute changes on that element. But the element we are talking about is the <HTML>-element, so this is top-most element. What changes are you afraid of not catching that way?

I just want it to change anything that changed within the element, so that the entire page doesn't reload.

Hope this makes sense.

yes, so exactly that is what this library is for. The entire page does not reload. The point is just that window.document is not a dom node, so you cannot diff it either. Not with this library and likely not with any other library either.

If the <head>-element always stays the same, you might even just diff currentDom.body with newDom.body.

I think we are getting super close. This is the new code:

function morph() {
    const domParser = new DOMParser();
    const currentHtml = document.documentElement.innerHTML
    const currentDom = domParser.parseFromString(currentHtml, 'text/html')
    const currentBody = currentDom.body

    fetch('index.php').then(response => {
        return response.text()
    }).then(newHtml => {
        const newDom = domParser.parseFromString(newHtml, 'text/html')
        const newBody = newDom.body
        const dd = new diffDOM.DiffDOM();
        const diff = dd.diff(currentBody, newBody)

        dd.apply(currentBody, diff)

        console.log(diff)
    })
}

This is the result of console.log(diff):

[Log] Array (1) (morph.js, line 17)
0 a {action: "modifyTextElement", route: [1, 0], oldValue: "5ff35dc8795d4", newValue: "5ff35dcff38f7", toString: function, …}

Array Prototype

However, the page still did not change...

Change

const currentHtml = document.documentElement.innerHTML
    const currentDom = domParser.parseFromString(currentHtml, 'text/html')
    const currentBody = currentDom.body

to

const currentBody = document.body

It's probably easier to just write document.body in all the places where you used currentBody.

It worked. <3

congrats!