Morphing Concept Overview0:00
Hello friends, welcome to the bonus episode of the series. As promised, we are going to build up our own Alpine.morph from scratch. If you recall from the morph episode, we, instead of just innerHTML replacing everything on the page every time a Livewire request comes back, we want to intelligently morph the page to match what came from the server instead of just replacing everything. But we brushed over and hand-waved through some really important algorithms, and I think that it's really valuable to see how this is actually built. And the best way to do that is by actually building our own morph function from scratch. So let's just do it. Okay, rather than doing it in place inside of the JavaScript we already have, let's create
Creating the Demo Page0:39
So let's just do it. Okay, rather than doing it in place inside of the JavaScript we already have, let's create a brand new file called morph.html. And this way we have a blank slate to start from, okay? And now in public we'll create this morph.html in our Laravel app, and okay, we're good here. Let's create a button that's going to actually do the morphing, all right? And we'll create two elements, and the first element will be, let's say, onPage or inBrowser or something like that. And this will be an h1 of foo, and then the second element will be fromServer. And this is going to represent the HTML that comes back from the server.
All right, and then we'll add some JavaScript. So we want, when we hit this button, we want to call some JavaScript. So we'll create a function called morph that accepts two elements, element1, element2. Okay, and console.log element1 and element2. That's going to be our first implementation of morph. Just spit out the two elements, the one that we have on the page and the one we're morphing it to. Okay, so to get this button to call this function, let's just do normal HTML onClick. And then we can call morph, and we can pass in this element and this element. And one quick note here, let me render this on the page.
So we'll pass that in. And then the second one, we can use the index of 1. So let's refresh. Let's hit morph. And there we go. We have a function that gets run, and we get access to the first element, the one that's supposed to represent what's on the page, and the second element, the one that's supposed to represent the HTML that comes back from the server. Great. So these are L1 and L2, and I kind of hate those names.
Patching Text Content4:04
we're changing. And to is like a template. It's an ideal. It's just a thing for us to reference, to change from to. Okay, so now that we got some of that mental model-y instruction stuff out of the way, let's do real stuff. Let's make this function actually turn this h1 into this h1, and we're going to do the simplest thing possible. We're literally going to do from textContent equals to textContent. So textContent is both a getter and a setter.
We're literally going to do from textContent equals to textContent. So textContent is both a getter and a setter. We're using it as a setter right here and a getter right here, and we're saying, hey, let's take this from right here, and then to set this textContent to the to textContent. So this to word is definitely going to get annoying and weird. Okay, let's hit morph, and bam, there we go. Our first little morph scenario works. Let's extract this into a function, and we'll call it patchTextFrom and to. Okay, function patchTextFrom to. Rerun it.
Replacing Different Tag Types5:21
We also want to change the element name, and so this is how these sorts of diffing algorithms, when I say diff, I mean taking the difference of two things. These diffing algorithms, when there's a completely different element name entirely, the system should treat it as a completely new element and kill the old one and add the new one, rather than morphing its text or its attributes or something like that. So let's do this up front, but we're going to put it inside a conditional because we don't always want to replace the element's name with another element. So let's do this. Let's say if, and our first thing we're checking for is if these element tag names are different entirely, and we know that they are in this case.
Let's say if from.tagName, which is a getter for a string of the tag name, I'll show you that briefly. So here's our h1, we can type tagName, and we get h1. So if from.tagName is not equal to to.tagName, then we want to replace from with to, and there's a convenient method to do this, it's called replaceWith, and then we can pass in to, and also return out because we don't want to do anything else after that. We're completely replacing the elements before we do any patching.
Diffing and Patching Attributes8:22
Any HTML attributes we need to morph. We need this to match everything from here. So let's do that. Let's create a new function after patchText. And this is going to be called patchAttributes. Okay. And then we'll create a new function called patchAttributes. Ooh, we've got a bunch of space here and we don't want all that space. Okay. So patchAttributes.
Okay. So patch attributes. And all right. What do we want to do now? Well, we are going to loop through all of the attributes on the two in the ether, because that's the ideal template thing. We want to find all the attributes that from should have, and then apply them to from. And to get the attributes off an element, let's say $zero.attributes. That's how we get the attributes off an element. So let's loop through them.
So we're going to use for...of, okay, we're going to say for...of, and this is where you're looping through the values of an object. That's when you use for...in is when you're looping the keys of an object. So for...of is I've used much more often than, than for...in. All right. So the object in question is going to be two.attributes because we want to loop through the attributes that from is supposed to have. And then here we could say, let attribute of two.attributes. And now we could say from.setAttribute(attribute.name, attribute.values). This is a function on every element where you can set attributes.
And now we could say from setAttribute, attribute.name and attribute.values. This is a function on every element where you can set attributes. There's hasAttribute, setAttribute, removeAttribute. It's a nice little set of APIs here. So let's, let's try this. We hit morph and bam. We've morphed the first input into the second input by changing those dates. That's great. But we're not completely done because consider this. Let's say that we had a disabled = 'disabled'.
But we're not completely done because consider this. Let's say that we had a disabled equals disabled. Okay. And we refresh. So this one is disabled. It's an input text. We hit morph. It turned into input date, but it's still disabled because if you think about it, all we did was go through attributes on the two element, which was only type. And we looped through and we went, okay, is there a type?
we did was go through attributes on the two element, which was only type. And we looped through and we went, okay, is there a type? Yes, there is. Then make it set to date. Okay, great. We'll set it to date. And then that's it. The loop stops. So we need a second loop that goes through the four attributes, sorry, the from attributes and says, Hey, does this exist on two?
So we need a second loop that goes through the four attributes, sorry, the from attributes and says, Hey, does this exist on two? Okay. It does leave it. Does this exist on two? It doesn't remove it so that we can match it. So let's do that. Let's create another loop. But instead of looping through the two attributes, we will loop through the from attributes right here.
But instead of looping through the two attributes, we will loop through the fromAttributes right here. Okay. So let attribute of fromAttributes. And now this time let's do an if statement. We'll say if two dot hasAttribute, and we actually want to say if two doesn't have attribute or hasAttribute, then we'll say attribute dot name and attribute dot value. Okay. So if it doesn't have it, then we want to remove it. So we'll say from dot removeAttribute and then pass in the attribute dot name.
So if it doesn't have it, then we want to remove it. So we'll say from removeAttribute and then pass in the attribute.name. Okay. And let's see if this works. So we hit morph and we get a syntax error, unexpected token. I probably missed a closing ). Okay. Hit morph. And there we go. We've changed input to type date and we've removed the disabled attribute.
And there we go. We've changed input to type date and we've removed the disabled attribute. So this is a, this is a good algorithm. One little refactor I want to make when you're doing something like this and you're accessing, you know, like two properties on an object and you have to repeat yourself with attribute. dot everywhere. We could instead use array destructuring, so, or sorry, object destructuring. So let me show you that we can just do name and value just like that. And then instead of attribute. dot, we can just call whatever it is, name and value. So it cleans it up a little bit.
Recursively Patching Children12:29
We're changing tag names. We're changing text and we're changing attributes, which is great. That's actually like a good portion of it. Now the big complex part, the last little bit to this algorithm is when you're working with elements with children. So consider this. We have a ul with an alia of foo. And then let's say that from the server, we get a ul with an alia of bar. Okay. Let's see what would happen with our current algorithm.
Okay. Let's see what would happen with our current algorithm. Maybe we'll try to predict it. We go through morph. We see are the tag names the same? Yep. They are. Okay. Keep going. patch text.
Keep going. Patch text. Okay. From text content to text content. So it's basically going to say, Hey, make this UL equal to the text content of this UL. And that's going to create an issue for us. Watch this. We hit morph and from just turns into bar with no UL. If you take a look at in the browser, it's just a UL with bar inside.
We hit morph and from just turns into bar with no UL. If you take a look at in the browser, it's just a UL with bar inside. And the reason that is, is because if we take this UL with everything else inside of it and we say textContent, it's going to ignore all the child elements and just give you the pure text that's inside of that element. And that's not what we want. So let's put an if statement inside of this patchText. Okay. We'll say if we'll say if from, or I suppose, what should we say? We'll say if two.
We still want to patch the attributes. So let's see what happens. We morph and okay, nothing happened, which is actually good. It's better than the bad thing happening. So now's the final bit. This last one is patchChildren from two. And it's complicated because it has to be recursive. If you think about it, if there's a deeply nested tree of HTML elements, you have to, we have to do this patchText and patchAttributes and children on every single one. So we're going to get a little recursive, which can always be a little mind bending.
we have to do this patch text and attribute and children on every single one. So we're going to get a little recursive, which can always be a little mind bending, but I will say I've done a lot of recursive stuff in the past. I don't know bit when creating these frameworks and this is one of the more friendlier ones. Okay. So let's create a new function called patchChildren and we'll have from and to. Okay. Now inside of here, here's, here's a really important concept and let me actually bring up the Alpine docs again. So let's go to alpinejs.dev and my internet is being slow.
Okay. So that's just to kind of refresh you with a visualization of that part of the algorithm, this patch children. So here's what we're going to do. We're going to create two temporary variables called, let's say fromChild. And this will be, cause remember, we're going to have, we're going to go through each list like one by one. So fromChild is going to be set to this for the first loop. The second loop, it'll be this and third loop this. So fromChild equals from.first.element.child.
So childFrom and childTwo equals the first child of from and the first child of two. So just so we see that when we pass in the UL and we go to patchChildren, the first child from is going to be this li and the first two from or whatever childTwo is going to be this li. Okay. Making sense. So let's add, I don't know, let's add another one here just so that we can see it a little bit more clearly. This will be the first iteration. Okay.
This will be the first iteration. Okay. All right. So those are our temporary variables here. childFrom and childTwo. And now let's create a loop. We're going to create a while loop while childTwo. And what does this mean? Well, inside every iteration of this while loop, we're going to reset childTwo equal to childTwo.nextElementSibling.
it should read like plain language to you. Okay. So this is us looping through the Idea elements, right? This is in the two. It's not, we're not looping through the froms just yet. We're looping through the twos. Okay. And now what we can do is basically just call morph on each one of those children. We could say that, well, the from is childFrom, and the two is childTwo. And this is good, but if we run it, it's actually, there's a bug in it.
That the first one got handled and not the second one. And that's because childFrom never got updated. It was always the same. So the same way that we're setting childTwo equal to the next sibling, we need to set childFrom. So this is what I mean by like two lists simultaneously looping through, obviously we can't loop through two things at once. We need to choose one to loop through, but we have these temporary variables and they kind of represent, uh, you know, looping through this through two things at the same time. So this is the first iteration.
There's two scenarios though that we have not covered and that is adding new children and removing children. So let's start with adding a new child. So let's say that we want three of these. And right now there's only two children. And we refresh, we go to morph and we get an error, cannot read properties of null reading tagName. So what happened? Well, what happened is, and maybe just to clear things up, let's do this console.log(childTwo) and childFrom, and I probably should have reversed the order of those.
So what's happening here is a scenario where we basically need to say if child from, if not child from, meaning it's not set, then we want to add the current child to, to the from. So it may not make sense, especially doesn't make sense with me talking it out. So I'm just going to write it out. If not child from, so if that one doesn't exist, then let's say from.append(child two). Now we could just leave it at this. We're going to, I think we're going to get an error if we do this. Yeah.
And that's what's happening. So the algorithm checks out. Now we morph and we shouldn't get an error. It says nextElementSibling. Okay. Check this out. Right. This child from, we're trying to call nextElementSibling and it's already null. So what we need to do is say child from is equal to, and actually we can make this equal to the result of the appendedChild right here.
So we need a second loop where we say while childFrom, because remember childFrom is the one we left off on the last one here. We can say while childFrom, basically we just want to say childFrom.remove(). But before we run this, we would actually get an infinite loop here because we're removing childFrom, from the browser, but we're not destroying this object. So it's just like saying while true and it'll be infinite. We need to also find the next childFrom in the list so that we can remove it and then find the next one and remove that one. But there's a problem with this. And the problem is that if we remove the element, it now doesn't have a next sibling.
But there's a problem with this. And the problem is that if we remove the element, it now doesn't have a next sibling. So we need to create a temporary variable. Let's say toRemove equals childFrom. And now we can set childFrom to its next element and set toRemove to dot remove. Okay, refresh and now morph. And it worked. Okay, so we morphed each one, each child, and then we got to the end. We found that there was an extra one left over in the from, so we removed it. Okay.
And we're simultaneously walking two DOM trees at the same time. But if you followed it, if you understood what was going on, and even if you didn't, you can just black box this. But this is the part that makes our whole morph algorithm completely recursive. And I will say that we're done. That's it. The morph stuff is done. We've created a working morph function. This is it. So let's take it and let's actually replace alpine.morph with it.
Supporting HTML String Input25:28
This is it. So let's take it and let's actually replace alpine.morph with it. Let's do it. So, well, the only thing actually before we do that, I lied. This is taking two elements in. If you recall, our alpine.morph accepts an element and a string of HTML. So it's the equivalent of us doing this. Let's just pass in .outerHTML, which is a string of the HTML. And now we try to morph it and we get an error because, of course, this is supposed to be an element.
And now we try to morph it and we get an error because, of course, this is supposed to be an element. So we need a new if statement that says if, let's say, typeof two is equal to string. So if that two variable is a string, then we want to turn it into an element. And there's a few different ways to do this. I'm going to do the most simple way. It's not the fastest or anything, but I'm going to say let temp equal document.createElement. And we'll create a brand new element. It can be whatever. We're just going to make it a div.
It can be whatever. We're just going to make it a div. Then we can say temp.innerHTML is equal to that string of HTML. And now two is equal to temp.firstElementChild. You didn't have to follow that, but that's just like an easy way to turn an HTML string into a DOM element. It's not the fastest. It's not what I use in Alpine itself, but it's just the way to kind of use your own knowledge to do that. Okay.
Integrating with Livewire26:56
It's just the root right here. All right. Zoom back out. Some to-do. Okay. And let's take all of this code, these functions right here. Okay. And let's put them inside of our Livewire JavaScript. Okay. And here we've done that.
Okay. And here we've done that. Here's our Livewire JavaScript. And now Alpine.morph, we can just call our morph function, refresh, and check it out. It worked. And we didn't get any errors. And I can prove that in the console. Nice. So we have all of our morph goodness and we created our own algorithm. Now good job.
Limitations and wire:key27:25
So we have all of our morph goodness and we created our own algorithm. Now good job. We did it. You've built it from scratch. You understand how it works, but there are a few little gotchas maybe, or things that I would like to talk about related to what we just built. First let's talk about the things that this doesn't handle. So the things that actual Alpine.morph handles is if you're using Livewire, you've probably seen wire:key. If you get weird issues with morph DOM and DOM diffing, you can add wire:key.
this element should be this element. So what it's going to do is just set this foo to this bar and this bar to this foo. So let's try this. Let's morph. Okay. Now let me demonstrate this problem here. So if I grab, we'll refresh the page. Let's grab this foo and let's say $foo and dot, um, we'll add just something on it. Like, Hey, equals there.
on it. Like, Hey, equals there. Okay. So again, if I have this first ally, this foo, I say $zero.hey, I get there. Okay. And now if we hit morph, this foo looks like it just got moved over here, but let's take a look at this foo now. Okay. And call.hey, whoops.
Okay. And call dot, Hey, whoops. And you saw that it's undefined. It's still on this first element. So $zero.Hey, here is still there. So a way that you can, you know, make this a little bit easier to track in Livewire is you would say wire:key equals, and then maybe foo or maybe one or whatever you want. As long as it's a unique key, you would say wire:key equals two and so on. And then if you did this in your, you know, your Blade foreach loop or something, what comes back from the server will have the appropriate keys.
And that's just not how it works. And it rears its head in lots of ugly ways. Another one is maybe like, here's another problematic morph scenario. And these are just limitations of the algorithm. Like, you know, it's not artificial intelligence. It can't learn or know or predict that you wanted this element to be here. It can only use its own algorithm. You can make it smarter, but let's say this, let's say that there was an if statement wrapped around this element here, and then you did some Livewire thing that, that toggled this if to false.
And that's fine for a little list. It doesn't matter. The HTML is still going to match, but it's not fine. If you have an if statement in Blade and you have it around a, uh, sorry, a new Livewire component, right? And then that gets removed. That's going to try to turn this li into a Livewire, but you know, there's just weird stuff. If you have some, you know, Alpine with data on it or an event listener or something, those things are all going to get wiped out.
Comparing to Alpine Source32:27
Okay. Have I made your head spin enough? But this is it. These are the deep morph concerns. You've seen the algorithm. You really know it in and out. Let me also this, we haven't addressed Alpine at all. You have to do extra stuff to truly preserve Alpine state and all this stuff, which we're not going to get into. Let me actually open up Alpine and show you the actual morph code.
not going to get into. Let me actually open up Alpine and show you the actual morph code. It's very straightforward. Now that you've seen what we've done here, there's a morph function that takes a from and a to and a bunch of options, whatever. And then it calls this patch function, which is more like our morph function. And here we say, hey, if these things are different elements, then patch the whole element and return. Otherwise, go through and if it's a text or comment, whatever patch node value, which is the same thing as our patch text.
Otherwise, go through and if it's a text or comment, whatever patchNode value, which is the same thing as our patchText. And then here, patchAttributes, and that's the same thing that we wrote. And then patchChildren, again, just like what we did inside patchChildren. We have something similar. We have a while loop with the currentTo, and again, this handles a lot more. It handles keys. There's some smart stuff in here, look ahead stuff, all sorts of extra stuff. But in general, there it is. currentTo equals currentFrom equals, and then another loop to remove elements that
But in general, there it is. Current to equals current from equals, and then another loop to remove elements that need to be removed. So it's very, it's basically exactly like the algorithm that we just wrote. There's just more concerns in it. So hopefully, this was extremely beneficial for you. I hope you learned a lot. And now that you know this stuff, you are a Livewire superhero. This is one of the most complicated parts of Livewire. And now you understand it from the ground up, under the hood, you've seen it.
