مرور TodoMVC با Alpine: بخش ۳0:00
Alright, part three. Let's try to wrap this up. Here are the remaining tasks I have for us. So we're going to knock these out one by one. First up, add new task validation. So here's the issue. At the moment, if I hit enter, it'll create a new task even though I didn't provide anything. Let's fix that. So in my JS file, we'll open todos.app, I'll hide the sidebar, and right down here is where we add a todo.
Conditional Footer Display0:53
So why don't we knock that out too. Right down here, let's see, let's go to our footer. All right, this section here maps to all of that. So if I switch back, you'll see we temporarily hardcoded it to true, but now we can make it dynamic. Only show this footer if we have todos. So let's say todos.length. Okay, now I shouldn't see anything, but as soon as I add a task, we do see it. All right, so let's come on back to our to-do list, and we can check that off. And it doesn't even work.
Pluralizing Items Left2:32
Three items left. Not quite right. All right. Let's go back to index.html. Here's the section in questions. Why don't we clean this up, actually? Okay, so this word item needs to be plural based on the length of activeTasks. So think about it. In almost every case, it would be plural. Zero items left, two items left, three items left, ten items left.
In almost every case, it would be plural. Zero items left, two items left, three items left, ten items left. The one is the weird case. You'd say one item left, where it's singular. So we could start by doing it inline here. And we could say, well, if active.length is exactly one, then we want item, otherwise items. That would do it. We could even put this on its own line. Let's have a look.
We could even put this on its own line. Let's have a look. Run it. One item left, two items left, three items left. Let's get rid of all of it. And it works. Alternatively, if you want, you know what, I think it's fine. But yeah, alternatively, you could do things like this, where you say, let's call a method like pluralize. And we're going to pluralize the word item based on the length.
Implementing Toggle All3:39
like pluralize. And we're going to pluralize the word item based on the length. So you would just extract the logic here. That would be fine. But you know what? Keep it simple. I like this. So let's come on back. We're going a little faster than I thought, which is good. Next, implement toggleAll functionality.
Run it. So that's on, but I can turn it off. Why don't we use XModel to bind it? This will be something like, well, what are we binding it to? I want it, think about it, if all of them are complete, all of our tasks, then I believe according to their specification, this should be turned on. So we need to bind this checkbox to whether or not all of these are checked, or all of them are complete, basically. Okay, all complete. Let's go ahead and implement that now.
Okay, all complete. Let's go ahead and implement that now. And where are we going to put this? How about, yeah, maybe here. Can this be a getter? I think it can. Something like that. So how would we determine if all tasks are complete? Well, couldn't we just check, is the length of completedTasks equal to the length of allTasks?
Well, couldn't we just check, is the length of completed tasks equal to the length of all tasks? And if they are the same, that means all tasks are completed, right? So return this.todo.length equals this.completed.length. And I think that should do it. So let's give it a shot. Yeah, so notice it's not turned on here, but as soon as I complete all tasks, there we go. It does turn on. And it should be responsive.
It's not complete, all toDos, because remember, we are toggling it. So toggle, all toDos, maybe something like that. Let's try that. So we have add toDo, edit, cancel. Let's do it down here. So if I want to toggle all toDos, yeah, is that a good name? Is that clear? So we're really toggling the completion for all toDos, but it's okay, I guess. Now again, your first instinct might be to iterate over them. So we might say something like, okay, well, for each toDo, set the toDos completed status
Now again, your first instinct might be to iterate over them. So we might say something like, okay, well, for each toDo, set the toDo's completed status equal to the opposite of what it currently is. But this would not quite be right. Let me show you. We'll come on back. We check it, and it looks like it works. But then I check it again, looks like it works. But as soon as we change it up here, this one is completed. Then I click it, and we can see, oh, they're not in sync.
Let's go ahead and catch that, because remember, if we didn't catch that, and we just referenced it here, well, as soon as you got one item into the loop, it would change it, which means this would evaluate differently than it did on the previous iteration of the loop, right? So we need to go ahead and catch it. Then I could say, all right, iterate over all of the to-dos, and set the completed status equal to the opposite of whether it's allComplete. And yeah, it's a little confusing, but it makes sense if you think about it. Imagine the first time, something like this. Okay, you're going to click on this. Well, it would check, are all of the to-dos completed?
Are they all complete? Yes. So iterate again, and for each to-do, set the completed status to the opposite of true, which would turn them off. And even if we had one turned on, yeah, we're no longer doing the opposite of what it currently is. We're doing the opposite of the allComplete accessor. And that should work. And again, notice how everything is bound properly. So if I turn these off, I don't see clear completed.
Adding Local Storage Store9:56
Looking good. Next, add local storage support. Now, unfortunately, with Alpine 2, this will be a little bit more cumbersome than I would like due to the fact that Alpine 2 doesn't have support for deep watching. So deep watching would be a way of saying, monitor this toDos array. And if any of the objects or any of the properties within those objects, within that array, are changed, then trigger a callback. So you could imagine us writing a bit of logic that says, well, if at any point, if any of these toDos change, immediately write it to localStorage and be done with it. That would be ideal, and it would keep the code we have to write pretty darn minimal.
Now we could make this as fancy as we want. We could set up a full to-do store that is exclusively responsible for modifying the underlying to-do's. And then here, you would just defer to that. I don't necessarily want to do that. I feel like we're getting a little beyond Alpine at that point, and a little off topic. So instead, let's just do a basic form, and then you can expand on that if you want. We'll start by setting up a simple to-do store. I'll make this an object, but again, yeah, it could be a constructor function that returns an object, whatever you want.
I'll make this an object, but again, yeah, it could be a constructor function that returns an object, whatever you want. And maybe we could have like a retrieve method. Why don't we just see if we can store the toDos array directly on the store. So up until this point, we've been initializing it to an empty array. However, if we're going to switch over to local storage, and remember, this is provided natively in browsers, we could do window.localStorage.getItem. We need a key for it, something like todoStore. Now remember, at this point, there's nothing in local storage. So that would return, I don't know, null or undefined.
Now remember, at this point, there's nothing in local storage. So that would return, I don't know, null or undefined. So we could say, all right, return what you have there, or an empty array. However, if we're going to be saving an array of data, of course, you would want to stringify that, right? You can't save an array to local storage. You would instead stringify that array, and then parse it on the way out. So if we're getting this on the way out, we'd say JSON.parse. Parse whatever is returned from here, or parse an array. Okay, next, for our simple store, we need a way to update it or save it.
Parse whatever is returned from here, or parse an array. Okay, next, for our simple store, we need a way to update it or save it. So let's add a method called save. In this, again, we'll do kind of the same thing, just in reverse. Set the to-do store equal to the to-dos, but yet again, you can't send the array there. You'd have to stringify it. So I'd route this in JSON.stringify. Something like that. Okay. Next, I could grab this and do something like this.
This is just a simple explanation with no code.
So now, let's do the easiest one. How about this? When you add a to-do. So at the moment, we are pushing to that to-dos array. And then, yeah, if we're not going to isolate everything, I would need to call save to update local storage. And yeah, I think that should do it. So think about it. We've updated the to-dos. We want to save that to local storage.
We've updated the to-dos. We want to save that to local storage. We call a save method. I'm sorry. We don't need that one anymore because we have to-dos up here now. And then save will write to local storage and make it equal to the stringified contents of all of our to-dos. Okay. I'm going to give it a refresh. I will open up Chrome DevTools.
I'm going to give it a refresh. I will open up Chrome DevTools. I will go to the Storage tab, Local Storage, and it's empty right now. But as soon as I add a task, there we go. Here's our to-do store, and we have the stringified contents of all of our to-dos. Okay. So now, with any luck, if I give the page a refresh, it will remember it and then pull from localStorage and use that as the initial state. Let's add another one. And again, if we go back to storage, it's going to keep saving that.
Let's add another one. And again, if we go back to storage, it's going to keep saving that. You get the idea. Okay. But now, that only works for adding a task. So as soon as I delete two of them, yeah, notice we're not responding to that. So as soon as I refresh, they come right back. Yeah. So this is what I was talking about. If we're going to take a simple approach here, then you have to make sure that you call save.
So this is what I was talking about. If we're going to take a simple approach here, then you have to make sure that you call save whenever we want to persist the current to-do state. So that would take the shape of something like this. Let's see. So we hit the to-do, and then when it's complete, we either detect that the user deleted everything, which means they probably want to delete the to-do, or we can continue on. They've updated the body. They're ready to go. The only remaining step is to save.
There we go. Okay. So let's see if I missed anything. Let's delete one, give it a refresh. That remembers. In fact, let's just bring back the store so you can see it the whole time. All right. Let's stick with one task that updates. If we complete it, this should go from false to true. And it didn't.
Refactoring for Persistence16:36
Ah, yes. Yeah. So this is the sacrifice you make. The convenience is being able to do it inline. But if you then at some point want to hook in there and perform some kind of audit log or add local storage like we're doing here, it ends up being a little tricky if you don't have that watch support. So with that in mind, why don't we extract a method called, hmm, toggleCompletion for this to-do. We'll come on back.
this to-do. We'll come on back. How about we have toggleAllToDos. Yeah. So now I don't like this. So here's the problem. I'll paste that in. We accept the to-do. We update it, and then we persist. So real quick, let's make sure it works, and then we'll come back to the name.
We update it, and then we persist. So real quick, let's make sure it works, and then we'll come back to the name. I hit complete. And yeah, now that does change from false to true or reverse. So one issue here is I have toggleAllToDos, and you'll notice that is toggling the completion status. But then when I toggle a single to-do, the naming doesn't match up, right? And this is something you often run into. So my first thought is we could do something like toggleToDo. Now it's a little more clear that in this case we're toggling one to-do, and in this
So my first thought is we could do something like toggleToDo. Now it's a little more clear that in this case we're toggling one to-do, and in this case we're toggling multiple to-dos. But they're still not quite the same thing. This is setting it to the opposite of what it currently is. So you would think toggling all to-dos would then toggle all to-dos to the opposite of what they currently are. But they don't. So in this case, maybe the name is wrong. Maybe I want something like toggleAllComplete.
So in this case, maybe the name is wrong. Maybe I want something like toggleAllComplete. I'm not sure. Why don't we stick with that and live with it for a little bit? Okay, let's go back and update it. So we have here, that's now toggleAllComplete. And then this one, well, yeah, now we did change it to toggleToDo, but now that we've solved that problem, toggleCompletion may be better again. Yeah, so this ends up being the hardest stuff I do, is figuring out the names for things and ensuring that it's consistent and is clear a year from now.
Yeah, so this ends up being the hardest stuff I do, is figuring out the names for things and ensuring that it's consistent and is clear a year from now. So in this case, toggleCompletion, that's fine. But then also notice we have lots of cases where we have the word toDo in the method name, but in other cases we don't. So these are all things you should be thinking about. For example, if I had this set to editToDo, that name would be fine, but just make sure that it's consistent, because it's weird if we have addToDo and then to editToDo, you call a method edit rather than editToDo. You see what I mean?
call a method edit rather than editToDo. You see what I mean? We want that to be consistent. So that's what I'm thinking about here. So maybe that should be, hmm, toggleToDo, completion, it's a little wordy. But let's leave it at that, and again, we'll live with it. We can change it later. All right, let's have a look at how things are going here.
We can change it later. All right, let's have a look at how things are going here. I think we've implemented this feature, have we not? Complete it, give it a refresh, it still remembers it. We clear it. Does that work? Give it a refresh. No. So clearingCompleted is not saving to local storage. So let's have a look here.
So clearCompleted is not saving to local storage. So let's have a look here. All right, I hit clear, and that should update this to an empty array, and it's not. Okay, let's see what's happening. When I call, what is it, clearCompleted, yeah, that same thing where we can no longer do it inline. Why don't we call it clearCompletedToDos? Yeah, now we can put that, where should it go? Down here. Whoops, clearCompletedToDos, paste that in, and we would just say this.todoes equals this.activeTodoes,
Down here. Whoops, clearCompletedToDos, paste that in, and we would just say this.todoes equals this.activeTodoes, and then persist. Another option, by the way, would be something like, hmm, acceptACallable, and then you would do that here. The idea is the update method would trigger your callback and then automatically call save for you, and that way you wouldn't have to do it every time. But I still don't know if that's overly useful. Let's see, if you did it up here, you provide a callable here. You could then get rid of that, and this is what all of the calls would be.
Let's see, if you did it up here, you provide a callable here. You could then get rid of that, and this is what all of the calls would be. Not horrible, but I don't think necessarily better either. I don't know. Let's keep it like this. Let's keep it simple for now, because again, what you might want to do, if you want to take this further and practice working with an external store, is yes, sometimes you'll have something like this. Maybe toggleAllComplete actually defers to your store, and then you might say this.store if you have it accessible there, or to do store.toggleAllComplete.
Maybe toggleAllComplete actually defers to your store, and then you might say this.store if you have it accessible there, or to do store.toggleAllComplete. And so this ends up feeling weird sometimes, but it still allows you to isolate anything that interacts with your database or your persistence layer, and that's why we call it a store. It's in charge of that piece of state. And if you took that approach, nowhere in your code base would you be able to modify that state. You would have to go through those entry points. That's the basic idea there.
Optional Code Refactoring22:02
You would have to go through those entry points. That's the basic idea there. But again, I think it's a little beyond the scope of a Alpine tutorial series. So I think we've done just about everything here. All of those are done. The only remaining step would be any potential refactoring. I might put some of these together. You have your initial data. You have your accessors. If you wanted to organize this, you might throw those into something like that, maybe.
You have your accessors. If you wanted to organize this, you might throw those into something like that, maybe. Filters.active. I don't think that's necessary, though. Let's keep going. getAllComplete. And then here, sometimes for this, I know some people are sticklers for it, I don't mind single line returns. Added to do, that's fine. Another option you might want to do, just showing you a few things here, you could have
Added to do, that's fine. Another option you might want to do, just showing you a few things here, you could have a constructor function. So if you want to be a little more object-oriented, you would accept your body here. You would save it. This would allow you to store the ID, so if you're doing something like that. Now whenever you create a new to-do, we automatically add the ID. And we automatically set completed to false. So that would be your to-do constructor function. If you wanted to take that approach, then you could do something like this.
on it, you'll get the object. This is way beyond the scope of the tutorial, if we were to redo that. But yeah, this would basically give you a hook. In the php world, the equivalent would be something like the __set magic method or the __call magic method. Then you could do something like save to local storage. Or you could dispatch an event. Or try to call todoStore directly if you need to. So that could be something kind of fun to tinker around with if you want. Let's not go there.
And it just didn't require that much work once you understand the essentials of Alpine.
