AI Generated Video Summary
1. Introduction to Alpine JS
So we're going to create this tiny scrappy version of AlpineJS today, and we're going to basically make a mess doing it, then we'll walk it back and refactor it using some of the techniques that I've used inside Alpine. So let's do it.
2. Creating the Dom Walker and Handling Flame Click
3. Evaluating Expressions and Mutation Observers
And this expression, let's just log out this expression, so we can see that it worked. We want to actually listen for a click event and then evaluate it. Now we're almost there. We're just going to add one more piece of real world complexity to this mess. That's going to be a mutation observer. In Alpine, you can actually change or remove these attributes without reloading the page and Alpine will pick up those changes. So let's write some mutation observer code that allows us to hook in to actually removing that attribute. What do we want to do when the attribute has been removed? Well, we want to remove the event listener that we just added above. So let's do that. Now we have add event listener.
So if we refresh it evaluates alert, so hot, but we don't want that. We want to actually listen for a click event and then evaluate it. So let's do that. L dot add event listener click. And now we can put a little function in here. Let's do that. All right, refresh. And now it's not going to alert until I click great. That worked now we're almost there.
4. Refactoring and Designing the Code
And now we can remove that event listener when the attribute has been removed. All right. Click so hot. Now, if we go in here and we remove this attribute, we click, and now we're unable to see that, that alert because the event listener has been removed.
5. Extracting Code into a Function
Like if we're writing this code for consumption, this is all we need. So I have a utils.js file where we're going to store all these functions. Let's create a function called walk that accepts the root element and a callback. We can extract the code surrounding the callback into the walk function. This is a basic extraction where you extract the code into a function and accept a callback. We have walk L and everything else should work fine. Let's run this and make sure it worked. Cannot read properties of undefined has attribute because we're not passing in the element into the callback. Everything works great. Let's continue until we find something that's not perfect. This is perfect. This is perfect. Here's a big if statement, if L dot has attribute.
Like if we're writing this code for consumption, this is all we need. All this stuff is implementation details.
Okay. So I have a utils.js file where we're going to store all these functions. Let's create a function. We're going to call it, walk, and we accept the root element and then the actual Walker, which we'll just call callback. And then we can extract all this code here. The top half pop here, run that callback, which is going to be all of this code that will pass in. So let's pull this up right here. Okay. And then this bottom chunk, we'll pull that out in here. Okay. So again, this is a pretty basic extraction where you just extract the code surrounding something into a function and accept a callback for the inside of what that code was wrapping. So we have walk L and everything else should work pretty much fine. Uh, this is the only bit we need to change here. And we'll also need to import this into our file here. And let's run this and make sure that it worked. It did not work. Cannot read properties of undefined has attribute. Because we're not passing in the element into the callback here. Say both files refresh. And there we go. Everything works great. All right. Let's start from the top again until we find something that's not perfect. This is perfect. This is perfect. Okay. Here's a big, if statement, if L dot has attribute.
6. Inverting Conditionals and Using Guard Clauses
When encountering a big conditional, I like to invert the conditional and return early using guard clauses. This makes the code more readable and avoids excessive nesting.
And when I come across something that doesn't work, I'm going to If L dot has attribute. And when I come across an if statement like this, there's a few things that go on in my head. In general, conditionals, add to the complexity you need to store in your head when you're reading your own programs, because it's like all of this code only works when there is. An attribute on the element called flame click. So I need to keep that knowledge in my head that all this code, we need to be inside of that conditional, um, which, you know, as you read, it's just like adds to that load. So one little thing that I like to, that I like a little technique that I like, um, when I have a, you know, a big conditional is invert the conditional and return early. So in this case, we can say if not L has attribute, then return. And now we can basically run the rest of the code with knowing that we have that attribute, but without that extra level of indentation. And in this scenario, it's not the best refactoring, but I just want to note that I use these, they're called guard clauses. I use them all the time. It's like, get the exceptional cases out up at the top so that you can write the code at the happy path. And if you have like four levels of nesting and you turn those into four guard clauses, that is so much more readable than various, you know, levels of nesting throughout the function. All right. So that's one little technique.
7. Using Callbacks and Avoiding Null Values
Instead of using an if statement, use a callback to receive a value. Create a function called getAttribute to extract the expression. Avoid potential null values by using a receiver callback. This ensures that the expression exists and avoids errors. Continue with the code implementation.
Another one that I like is instead of using an if statement use a callback to receive a value. So let's just, let's extract this, this L dot get attribute into a little function called get attribute. Where we pass in the element. And the name of the attribute that we want to get, which is flame click. Okay. And let's just write that function and then we'll talk as we go. Get attribute element and name. Okay. And something like this, we'll pull this into here and we'll call this name. And now the first thing you might think to do is just return this expression. And so you have something like let expression equals kind of thing, but then you're back to where you started. You have to say, if not expression, and that's a null value, that's a potentially null value. And when you're working with code, as you know, like. Values that can potentially be null are death to your programs because it's so hard to figure out that the very few cases or many cases where they are null and then you get all those errors, like trying to, you know, treat null as a function or call property on null or whatever. So what I would rather do in this scenario is create a receiver callback. So instead of setting a variable called expression, what if we create this little receiver callback where we get expression as the first parameter? Now, everything inside here has access to expression, but we're always guaranteed that expression exists. If it doesn't, this callback just won't run. So let's turn that function into a receiver function. So if not, has the name we're looking for return early, otherwise, let's add this little parameter here, this receive, otherwise, instead of returning it, let's receive that value. All right. Make sure this works. It's not going to work because we didn't import this. Now refresh and everything works. Great. Alright. Let's keep going.
8. Abstraction and Handler Extraction
We create a function called when attribute is removed to handle the abstraction of the mutation observer. We add the attribute to the page and ensure it works. The code is now more readable and tidier. We extract the handler variable into a separate function called on.
So we have our walk function. We have our receiver get attribute. That's pretty good. We have this add event listener, and we're going to extract this in a minute. But the next really imperfect thing that I see is just that there's an easy abstraction here. This mutation observer, there's code above and there's code below. So it makes it a really quick function abstraction. So let's do that. We're going to create a function called when attribute is removed. And then we'll pass in the element, the name of the attribute, and then a callback to run when that attribute is removed. So we can basically just take the code like this, run the callback and then the rest of the code below it, we can put below that.
Okay, perfect. So now we have one attribute is removed and we can add that to the page here. One attribute is removed L and then this flame click. Okay. And then our callback here. Okay. So that looks pretty good. Let's make sure that it works. Okay. That works. And now if we remove this attribute, the click has been removed. All right. So this is much more readable already. If we're just like even just squinting at it, giving the squint test, you can see that everything is much tidier. We have a lot of these basic functions extracted the last one, the last imperfect thing while we're walking down is that we have to set this handler variable so that we can both add it as an event listener and then remove it as an event listener later on. And this isn't so bad, but pretend that there's like a hundred lines of code between here and here. And that just doesn't feel great to have this temporary variable being referenced in such different parts of the code base and to extract it. Like, like if we're going to make a little function, so let's do that. Let's make a little function called on where you pass in the element, the event name that you want, and then the handler, and then let's do that.
9. Refactoring the Event Listener
So let's say on L click and then handler, you know, that's fine. But this remove event listener, how can we make this bit part of this abstraction? A technique I like to use is to return the removal of the event listener as a function from the on handler. This allows for cleaner code and avoids the need for a separate temporary variable. Let's import on and continue with the refactoring process.
So let's say on L click and then handler, you know, that's fine. Okay. But this remove event listener, how can we make this bit part of this abstraction, this on event listener and technique that I like to use when you're trying to extract something from two different layers of nesting. So before so far, we've just extracted things on a single level of conditionals or functions or whatever. Just a single level of indentation. This is something we want to extract both here and here. So it gets a little dicey or we can't just copy and paste stuff into here. So a technique that I like to use for functions that do something like this on, it actually adds an event listener. Often there's like a tear down or some sort of cleanup procedure for that procedure. Like every action has an opposite reaction. Like this action, this add event listener. There also is the removal of that event listener that's related to adding it. And to keep that in this function, we can return it as a function. So check this out. If we take this, I'll remove event listener and let's actually duplicate this and say, remove event listener like this, now we can return it from this on handler, and then we don't need to have this handler as a separate temporary variable we can see let off on L click of L expression, and then down here we can just run off. So this is something that I love, and there's so many times where this is useful, um, often, you know, when you're registering event listeners or pushing on to some kind of stack, you might want to return some method or function to remove from that stack or bus to cache or it's just like a really handy pattern that I really like return a cleanup procedure from a function that that has a cleanup procedure. So, okay, so great. So I'm really happy with this. Let's import on and make sure everything works. Okay. So this is like step one in the refactoring. I think we've got to a point where this looks pretty good, but it's really just a more readable version of what we started with, it's the same structure. We're walking the Dom. We're getting an attribute. We're listening for a click. And then when the attribute is removed, we're pulling that click off. There's so many structural changes we can still make. So let's keep walking through those.
10. Using Context and Parameter Currying
The get attribute and when attribute is removed functions have similar parameters. To reduce duplication and improve readability, we can modify get attribute to return a function called on remove. This new function will accept a callback and internally call when attribute is removed. By utilizing the context from get attribute, we can create a function that only requires the callback. This technique, known as parameter currying, simplifies the code and makes use of context. Alternatively, we can use the bind function to achieve the same result.
Okay. So the first one we're going to do, we have get attribute and when attribute is removed, they have basically the same parameters that you send them. And at this level of code, like this very outside in part, where we're just walking the Dom getting attribute. I feel like we don't need to have all this knowledge. I feel like we don't need to have, to use, get attribute. And when attribute is removed, it feels like a lot of duplication. What if instead this get attribute returned us a function called on remove past it in the parameter of our little receiver function here that we could just use instead. And we wouldn't have to pass all of these parameters in, I think it would clean this up a lot.
11. Refactoring to a Declarative Approach
12. Exporting and Booting Directives
And if I export the function, it's a public function. If I don't, it's just a private function used within the file. We can add properties by writing variables at the top. Let's export the function call directive, where we pass in the name of the directive and a callback for initialization. We need to store the directives in an array called 'directives' and push an object with the name and initialize function. The 'boot' function will iterate through the directives and execute the code for each directive, passing the attribute, element, and initialize function.
And if I export the function, it's a public function. If I don't, it's just a private function used within the file. And I can add what's the equivalent of properties by just writing variables up at the top. And you'll see what I mean. So let's export this function call directive, where we pass in the name of the directive and then a callback that handles the initialization. We could call this maybe initialize so that it is more readable or something.
Okay. And now here we're not, we don't have the current elements, so we have to kind of change the structure. We need to store all these directives so that we can boot them later. So let's create a little property called let directives equals. And it's just an array. And then here we'll say directives push, and let's push on a little object with the name and then this initialize function. Okay. So when the code runs, when this directive runs, it's just going to basically hold these over for us in this variable, this property called directives until we use them down in boot. So let's create that boot function. All right. And boot accepts an element.
And now boot is going to go through those directives. So directives.for each, and this will be a directive, an individual one. And now this individual directive, we can take this code that we had before. And now we would actually run it like for this directive, we'll get the attribute. We already have the element. We'll say directive, dot name. And in this case, we could just pass in directive dot initialize, and maybe we'll do that just for now to make sure that it all works, but actually we're not going to do that. Instead of passing it, initialize, we're going to pass in our own callback here. And then call this initialize function. And remember, initialize is what we pass in to the directive and initialize expects this object of parameters. So let's just paste that in here. Okay. So we have element, we need expression.
13. Inverting Flow and Writing Readable Code