Async modal delete issue0:00
Let's bring support for asynchronous states to our dialogue so that even if the action takes a little bit of time, the UX feels great. All right, so the first thing I will do is put a little sleep into our delete function and I'll make it two seconds. And so now the problem with that setup is that the alert dialogue, the action button here, as soon as we click it, we'll close the model. So I will click it and check what happens.
as we click it, we'll close the model. So I will click it and check what happens. Click one two and after two seconds the puppy was successfully deleted. And you might have noticed we had the progress bar at the top, which is still a nice ui ux indicator, but we wanna make the model feel a little bit more connected with the action. And so the model should stay open while the action is being performed.
Review Radix async example1:02
And so the model should stay open while the action is being performed. Obviously we're going to have a loading spinner inside the button, and then when the action has succeeded or failed, the model can close away and show the display notification. If we look at the documentation for the alerts dialogue, actually we need to check the docks from the radix UI and the P component. And if I scroll here, we will find some interesting points.
and the P component. And if I scroll here, we will find some interesting points. The first is the root element has an open boolean property. That's handy. Let's skip going. And if I search for synchronous, we actually have an example right there. So you can see here the weight is basically the equivalent of our sleep function. And here now it's taking control of these open states that we've looked at just before.
Control dialog open state1:44
And here now it's taking control of these open states that we've looked at just before and passing that stage to the root components. In the PuppyDelete component, I'm going to use a little bit of React state. So const, open and setOpen is going to be my useState hook so that I can take control of the open state. And you can see that copilot instantly understood that I want to equip the alert dialogue,
And you can see that copilot instantly understood that I want to equip the alert dialogue, the root component here with the value of open and also an unopen change callback, where I can call this setOpen state setter. And I guess the unopen change is part of the props of the root component. And yep, here it is. The reason we have this unopen change is that we can still have the automated triggering of open and close.
is that we can still have the automated triggering of open and close. For example, the model is set to close by default, but I can still click on the dialog trigger and change the value to open. The reason it's closed by default is because our useState has a default of false. If I was setting this to true, I believe every single model would be open and stacked on top of each other.
I believe every single model would be open and stacked on top of each other and yep, that's exactly what happens. And so you can see I can cancel them one by one and the background is slowly revealing. That's not a great ux, but it's important you understand. The reason why we are able to open the dialogue is because this trigger is going to call the uncertainChange event and set the value to true. And so for the exact same reason, the
and set the value to true. And so for the exact same reason, the alert dialogue action is going to do the opposite and close the modal. So even if we are trying to control the state, I think that it's still instantly going to close the dialogue right now. So how do we go around this? Well, we do not have to use the dialogue trigger and the dialogue action components.
Replace action with submit3:29
Well, we do not have to use the dialogue trigger and the dialogue action components. They're here to provide the functionality, but if we take over the ownership of the open state, we can use regular buttons and decide what they should do and when they should change the state of the open state. So I'm quite happy with the dialogue trigger opening the model. That's nice, but here I do not want clicking the action to close the dialogue.
That's nice, but here I do not want clicking the action to close the dialogue. So after all, it is a button of type submit. So why don't we just use a regular button of type submit? This is a NUI button, but it is still going to submit the form. And so this is going to run and we are going to be able to call the destroy endpoint on the backend. Now try to think of what's going to happen to the dialogue.
endpoint on the backend. Now try to think of what's going to happen to the dialogue. Is it going to close immediately? Is it going to close after the delete action has finished? Is it going to stay open forever? Let's find out. All right, so I'm going to try delete. Josie, are you absolutely sure you want to delete Josie? Well, I don't want to, but we have to. That's for science. We're testing things out. So I'm going to click and you can see the loading spinner.
We're testing things out. So I'm going to click and you can see the loading spinner and the puppy was deleted successfully. Now what happened? Why did it change to Sheba? Oh, hang on. I have an idea. Let me refresh the page. Yeah, okay. I've left the useState open to be open by default. So let me go up here and we want this to be false, sorry about that, but let's do the experiment one more time. I will delete mini this time, uh,
but let's do the experiment one more time. I will delete mini this time, uh, and as I delete mini, the processing is happening, huh? The model closed. You might have thought, and I promise you I actually did that the model would stay open because we never tell it to close. We never changed the setOpen back to false, but it's closed.
We never changed the set open back to false, but it's closed. I can understand it, but I got tricked. And you might think, hey, this is because we don't have preserved states in the Inertia destroy function call, which is why it's resetting the state. But no, let me show you. Even if I was to add preserveState to true and I was to delete another poor puppy.
Even if I was to add preserveState to true and I was to delete another poor puppy or bunny, in that case, the model will still disappear when the puppy or bunny is deleted. So why is that? We specifically ask Inertia to preserve the state so that the model would stay open, but it's still closed. And if you think about it, the model is rendered or the trigger, the root component of the model is rendered.
Why modal still closes5:57
And if you think about it, the model is rendered or the trigger, the root component of the model is rendered inside the puppy card and when we delete the puppy, the whole puppy card gets deleted and unmounted. And so this is why the dialogue inside the puppy card is also being unmounted. It's not part of the application anymore. In a further lesson, a future lesson, when we do update functions.
In a further lesson, a future lesson, when we do update functions where the puppy doesn't get deleted and the puppy card remains, we'll be able to see how to handle the actual state. But in that case, for a delete action, the dialogue will be unmounted when the puppy card that contains the dialogue route is unmounted. Alright, so in this case, I guess we don't need the preserved state.
Add processing spinner UI6:38
Alright, so in this case, I guess we don't need the preserved state. True. It doesn't really do anything, but we can still make one improvement to our model and it's to incorporate the processing state from the useForm hook. So if I scroll to the top of the component, remember that we have extracted this processing helper from useForm, but we're not using quite yet, and this is the perfect use case here in the button.
but we're not using quite yet, and this is the perfect use case here in the button to display a loading spinner while the form is submitting. So maybe the first thing we can do is disable the button as well when the form is processing, even though Inertia is able to cancel requests, it's a nice way to prevent the user from submitting multiple requests. And then let me first show you an incomplete way to go about this.
And then let me first show you an incomplete way to go about this. So I'll separate this in lines and I wanna say that if we are processing questionMark, in that case we would render a loader circle. I think this one looks great icon from Lucid React and let's give it some class names. So maybe size 5, stroke white. Let's try actually a strokeForeground so it matches the brand of Shay and UI.
Let's try actually a stroke foreground so it matches the brand of Shay and ui. And the magic class here really is the animate spin, which is going to make it feel like a loading spinner. So let's close the loader circle and then if it's not processing, let's have a spin because we're going to add some classes later on and move that string inside of it. So if we processing loading circle, otherwise delete puppy text.
So if we processing loading circle, otherwise delete puppy text and uh, I'm going to remove this. Uh, I don't know when it showed up, but we certainly don't need that empty space here. Alright, so let's go give that a shot. Our next victim is Sasha. I'm going to click and look at the button. When I click delete. Whoa, this is not looking like I wanted.
When I click delete. Whoa, this is not looking like I wanted. I think the default button has some styles for the disabled state. So the default variant is default. And if I look at default here, or maybe in the common classes, we are going to have some disabled styles here. disabledOpacity 50, which in our case is not looking good at all.
Disabled opacity-50, which in our case is not looking good at all. I think I understand why it's here, but I don't want to use that in our specific use case. And so let's add a class name to the button and say opacity-100. Thank you very much. And tailwind-merge should take care of making this class win because we pass it after the base classes. Okay, it looked like the stroke
after the base classes. Okay, it looked like the stroke of the circle was also wrong, the foreground, but let's try Dali Bunny. This is a little bit better, but yet it looks like the foreground is actually black. And I'm talking about this stroke color here. Other side quests that's going up that CSS. And actually look at the defined colors and okay, yeah, foreground is a almost black color.
And actually look at the defined colors and okay, yeah, foreground is a almost black color. Oh yeah, okay. That's what we want. We want primary foreground. Of course it makes sense because the primary has a background of dark, but the default foreground is on white background. This is completely white and so that makes sense. Sorry about the side quest, but you might find it interesting.
Sorry about the side quest, but you might find it interesting. So stroke, primary foreground, and now our button should start looking good. Sorry, Trixie, your next, let's check the button. Aha, this is starting to look better. I think there's one more thing we could improve, although you might like the fact that the button shrinks down to only cover up the loading spinner instead of the text.
that the button shrinks down to only cover up the loading spinner instead of the text. But if you wanted the button to keep the exact same dimensions whenever the loading spinner uh, is showing, I'm gonna show you a technique for that. Alright, so step one, and this is why I added a span. I'm going to add a conditional class name here and let's use clsx for that. And basically, if we are processing, I want
and let's use CLSX for that. And basically, if we are processing, I want to add the invisible class, which in Tailwind is visibility:hidden. So it's going to hide the text but not remove it from the layout so it'll still occupy the same space but not be visible. For this to make sense, I need to move the span out of the conditional here because I want it to always show and then if it's processing, be invisible.
of the conditional here because I want it to always show and then if it's processing, be invisible. So I will change this from a turny to a shorthand and end without a second close like this. So it's not gonna work just yet. Matter of fact, it's going to be worse right now, but check what happens. As I click delete, you can see that the button is still having the text width plus the loading spinner width, and that's the last piece of the puzzle.
is still having the text width plus the loading spinner width, and that's the last piece of the puzzle. We don't want the loading spinner to sit next to the invisible text. We want it to sit on top of it. And so we are going to use an absolute position to do that. I need to give the parent a class of relative so that the loading circle can become an absolutely positioned child. But I actually want to have a little wrapper here.
absolutely positioned child. But I actually want to have a little wrapper here. So I'll add a div that wraps around the loading spinner like so. And that's going to allow me to make that div a position of absolute and inset 0 to occupy the entire space available. I'll show you the results. It is going to sort of work, but the spinner should be on the top left by default. And yep, that's still not great.
but the spinner should be on the top left by default. And yep, that's still not great. Arguably worse than when we begun. But the last, last, last trick is to center it somehow. I like to use display: grid and then place-items: center to center something vertically and horizontally. And this time with this code, it should look perfect. Quick recap. The button has a position of relative, and then if we are processing, we show a loading circle.
Quick recap. The button has a position of relative, and then if we are processing, we show a loading circle that is absolutely positioned and centered inside the existing space, the existing space being generated by this text that becomes invisible when we are processing. Hopefully that's crystal clear and let's go observe the result. Goodbye Lexi. Have a good sleep. Check it out. Very, very nice. I am happy with that.
Goodbye Lexi. Have a good sleep. Check it out. Very, very nice. I am happy with that. All right, and with that we have a fully functional delete functionality in our app, which is now a current app. It's just missing the U, the U that stands for update. And this is what we're going to do next.
