00:00
(upbeat music) So with your HTML forms, there's gonna be times where you need to be able to send up
00:08
an array of data or even an array of objects. And our cast and crew members are one such instance for our movies. Let's add them in down here
00:17
just above our update movie button. So let's start by jumping into our creator edit page right above that button. We can give ourselves a little heading to mark that this is where they will be.
00:26
Do font bold and just say this is the crew members. And we'll just focus on the crew members here for now.
00:32
We will have a div with an ID of crew list. This is the div that will list out all the crew members attached to the movie within.
00:41
So we'll have this as class, flex, flex column, and we'll have one child inside of this list per crew member that we are attaching.
00:49
So we'll have a gap of three there as well. And in here is where we can do an each to loop over them. So we'll do an each member in, and we'll call this variable crew members
00:58
whenever we get around to querying it. Then we'll end our each. And inside of here, we'll have another div class flex, and we'll do a gap of three here as well
01:08
because we're gonna have multiple fields. The first field that we need to have is a select, and the options for the select are gonna be Cineasts. So we can copy either our director or writer field
01:17
as a good starting point for this input, pasting it in its place. The label we can do as crew member.
01:24
The name we will circle back to momentarily, whether or not it's selected. Instead of doing movie dot something, instead we're gonna query this information
01:33
directly from the crew movies and cast movies pivot tables. So we'll be working directly with columns as they're defined inside of the table here.
01:42
So this will be our member dot cynist_id, as that's what the column is literally called within our crew movies pivot table.
01:52
And that'll be in charge of discerning whether or not this particular option is selected as we loop over our cynist options.
01:58
Our next form input for the individual crew member that we're on is going to be a self-closing form input. The type's gonna be text, so we can omit that
02:07
with a label of title so that we can discern the title of this individual crew member for this particular movie. And the name for that will be title as well.
02:16
Additionally, we can add in the default value for this. Again, that's gonna come from our member dot, and we have a title column directly on that pivot table.
02:24
Lastly, we wanna be able to remove this member from the movie. So we'll have a button of type button so that whenever we click this button, it doesn't actually submit our form.
02:34
We'll have an onclick handler. In onclick, what we wanna do is reach up to the parent of the button that encapsulates all of the fields specific to the individual crew member.
02:43
So that would be this div right here, which happens to be the direct parent of our button. So we can get access to this button element via this,
02:51
reach for its parent element, and then remove that parent element, thus in turn removing all of the individual fields
02:59
that we have specific to this crew member. And the way that we're going to save this on our controller is to sync the data. So by simply removing it out of our HTML form,
03:08
it's going to serve as a delete anytime that we come to update or save our crew members. And we can put a little and times character, which will be a little X to serve
03:18
as the indicator for that button. Okay, so that should do it for our field specific to a crew member. Now what we need to do is add the ability
03:25
to add an additional crew member to this crew list. So underneath this crew list, right above our submit or create button,
03:33
we can add in another button of type button. Onclick will add a crew member, and we'll create that function here momentarily.
03:42
And we can give this a label of add crew member. Now this will be a client side function. So we'll want to plop this inside of a script tag.
03:50
So we'll add a script tag down at the bottom of our page with a function add crew member defined. For this function, what we're going to want to do
03:59
is be able to essentially plop one of the contents that we're looping over here for our crew members, so that we'll be able to select what that new crew member is
04:08
and define their title as well. If you're using something like HTML or Unpoly, you would be able to reach out to your server to get back that HTML to append into your document.
04:17
We're not, so we're going to reapply this exact HTML somewhere that we can easily access it from our client side script. And to make this easily doable,
04:27
we're going to cut out all of that HTML that we're looping over and move it into a component. So let's scroll on up to, we should have components movie here.
04:35
Let's right click that, create a new file. This will be our crew member fields or something of the sort. There's not a very distinct name that would be applicable for this
04:45
and paste the contents that we had cut out in. These are the fields applicable to one single crew member that we're binding into a movie. We'll circle back and fill in the names here.
04:54
Oh, I defaulted to filling in the name here. We'll circle back and fill in both of these names here momentarily, because we're going to need to specify them in a specific way. So let's jump back into our creator edit
05:03
and let's add in our self-closing movie.crewmemberfields component. Inside of here, we'll need to pass the member in
05:11
as well as the Cinest options that we have specifically for the select, so that we'll be able to select the applicable crew member Cinest record.
05:20
We can give this component here a copy and outside of our form, above our script, let's go ahead and add in a template tag
05:28
and give this an ID of crew template or something of the sort and paste the component inside.
05:35
So now EdgeJS will render our crew member fields both inside of this template, as well as for each one of our crew members already bound into our movie.
05:44
This section here will serve as kind of the default value whenever we're editing. Any pre-existing crew members will load in here via Edge. Otherwise, we'll get a template that we'll be able to clone
05:54
and append in anytime that we click our add crew member button. Now we have a little bit of more work to do here. So first and foremost, we're not going to have a member for this one.
06:04
So we'll want to pass it in as null, meaning that we need to jump into this component and make sure that that is nullable. So we can just use optional chaining for both references of our member
06:14
to make sure that that is valid. Next, we need to take care of the names for both of these. Now, there's a number of different ways we can go about adding a name in for this
06:22
and square brackets are going to be necessary in order for us to send this up as an array or even an object. So we can give it a baseline name of crew
06:32
and it will send up as such. And we can specify that crew is going to be an array by adding square brackets to the end of it, similar to how you would in TypeScript
06:40
if you have a type that is an array. Then we can specify kind of an object key specifically for this field by doing another bracket pair and adding the name of the object property
06:50
that we want this to be applicable to, which in this case, ID would probably be good as the value is going to be sent as ID. We can do something similar down here for our title as well.
07:00
So we'll have crew, add that into an array, do another set of brackets and give it a property name of title. Now I'm pretty sure we're going to have an underlying issue
07:10
with this approach, but we'll go ahead and test it out and see how it goes. So in order for us to test this out, we need to rig up our add crew member button. So outside of our add crew member function,
07:19
what would be good is to already have a reference to our crew template, as well as our crew list. So let's go ahead and query both of those.
07:26
So we could do const crew list equals document.getElementByID and pass in the crew list ID there.
07:34
And we could do const crew template equals document.getElementByID crew template.
07:41
Within our crew list, what we can do is append child, essentially a clone of our crew template.
07:48
So we can take our crew template, reach for the content, tell it to clone the node, pass in true there to clone it deeply. And then this should clone the crew templates contents
07:58
and append that element into our crew list. So let's give it a save. And then let's jump into our movie validator and let's rig this up in a way where it's gonna come up invalid,
08:08
just so that we can easily get back and see exactly what it is that we're posting up. So let's do crew and let's say vine date.
08:16
We're certainly not gonna be posting a date up for our crew. So that should most certainly fail. And then on our creator edit, let's go and scroll up to the top
08:24
and let's plop an instance of our flash messages. So let's inspect it out, flash messages, and we can reach for all of them just so that we can easily see exactly what it is
08:33
that we're posting up here as we submit and test out our crew list functionality. Okay, so we don't need to do anything in our movies controller yet because our validator should prevent us from getting there.
08:43
And it looks like we have an error, cannot read Sinus ID. I'm gonna go ahead and refresh that to see if that was, okay, yep. That was just something we saved along the way with an invalid state.
08:51
So we are good to go now. If we hit add crew member, look at that, we get our crew member field. We have our crew member selects. So we could select somebody there,
09:00
type in some title, and we can also clear them out. So if we added another one, but we only wanted one, we can clear that out. Let's start by trying to update our movie.
09:08
Okay, you can see our validation most certainly failed and you can see that we're getting back crew ID with a value of 33 and a title of some title.
09:16
So this is sending up as an object rather than an array. However, if we come back down here and we add in two crew members,
09:25
and let's make sure that they're a little bit different, and let's go ahead and post again, you'll see that now crew is still an object,
09:31
but ID and title is now an array of values where it was previously just a string. And I'm going to double check myself
09:40
to make sure I'm remembering that correctly as well. So if we post that back up, you can see, yep, most definitely just a string. So what we kind of need to do
09:48
is normalize its expectations a little bit by specifying an index of our array that we want the items to be members of. So if we hide this back away,
09:58
within our crew members fields, if we specify now an index within here, so we'll switch these two back ticks,
10:06
both of them right down here, and inject in an index, which will provide as a prop to this component. So an index just like so, give that a save,
10:16
jump back out to our creator edit, and then we'll need to grab the index out of our loop there, pass an index in here, and we're not going to have a specific index
10:26
to be able to patch in here. So that's where things get a little tricky on this front. Instead, we want to do index with some sort of placeholder that we can replace.
10:34
So maybe a capitalized index string. And then instead of directly cloning the node and impending it directly in, we're instead going to want to get the HTML of the template.
10:44
So rather than cloning it as we were before, reaching for the content, we'll want to instead just do crew template inner HTML.
10:52
And this will allow us to easily replace all instances of that index placeholder that we added into that component
11:00
with the const next index, which is our crew list children length. Because instead of our crew list,
11:09
we have one child div per crew member that we're adding in. So that's going to equate to the next index that we want to append into that list.
11:18
So we can pass that in here, next index, just like so. Then in order for us to be able to append the child in, we're going to need a node.
11:25
We could do inner HTML and append in that way, but that's going to re-render the entire DOM, which would get rid of any adjustments that we had previously made to this section of our form.
11:35
So we do want to use append child here. So we can do const temp and create a new element. We just patch that as a div,
11:44
and then we can do temp inner HTML equals our HTML, and then reach for that temp.firstElementChild, like so.
11:54
So now if we give this a save, we jump back into our browser. Let's start by adding a single crew member in and see how this posts. So let's select somebody there,
12:02
some title there, update our movie, and you can see exactly how that switched things up. So rather than doing crew as an object
12:10
with individual ID and title properties with either a string or array there, it's now crew is an array,
12:17
and we have a single object with ID and title as properties inside of there. So if we add two in now, so let's add that twice, add crew member, add crew member,
12:27
select some random people here, and give them some title one and some title two, update our movie again, you'll see that we still get back an array,
12:36
but now we have two different objects inside of our array with some title one and some title two. This is really ideal for how we're gonna want to
12:44
patch it up on our server side inside of our controller. So we'll want to keep forward with this particular syntax here, but there is a downside that we'll need to account for
12:53
later on here as well. But for now, let's go ahead and get this saving. So let's first jump into our movie validator and update our validator to accept in exactly as we're posting it.
13:02
So we're gonna have a vine array, and we're gonna wanna make sure that that array is optional because we may not have any crew members being sent up. Inside of our array,
13:10
we're gonna want this to be a vine object, so an array of objects, and for those objects, we're gonna have an ID of type vine number,
13:18
and we can even do an is exist check, making sure that the table of our sinists and the column ID contains the value that we're sending up here,
13:28
and then title vine string for the crew members title. Give that a safer formatting, and this should now match the syntax with how we're posting it up.
13:37
So let's jump into our movies controller next and get it added. So we have the additional field within our validator of crew which contains our crew members,
13:46
and what we're gonna wanna do is get back an object where the key is the ID and the object is the ID's title,
13:55
and it also has a sort order for them as well. So we're gonna want this format. So if we have multiple, it would be like ID two,
14:04
so on and so forth, looking like that. So we can use reduce to easily build that object.
14:10
So const crew members equals crew reduce, and that's going to be optional. We'll have our accumulator object, our row,
14:20
and an index so that we can add in the sort. Our accumulator is going to start out as an empty object, and for each time that we're looping over it,
14:28
we're going to take that accumulator and add in the rows ID with an object of title,
14:35
row.title, and sort_order, because remember we need to post this in as is defined in the database
14:42
since we don't have a model defining this pivot table, and the index will serve as our sort order there. Then of course, we'll need to return back
14:50
that accumulator for the next loop. Now we're gonna have some red squigglies because we haven't defined a type for this. So we'll need to type hint this reduce as well,
14:59
and we can type hint this as a record where the key is a number, and the value is an object with title, string,
15:08
sort, order, number, just like so. And we need an extra caret there at the end of that. Give that a save so it should format and get rid of all of our squigglies. It looks good.
15:18
And now all we need to do is await, take the movie, reach for the relationship of our crew members, and we'll want to use sync
15:25
because our crew members here is the source of truth. If we've removed somebody from the crew members and we want to delete them, and anyone that we add in, we want to append in.
15:34
So sync will take care of both of those use cases in one go, and just use our crew members here as the source of truth for who the crew members are.
15:42
And of course, this is optional, so it may not have a value at all. So we can default that to an empty array if we didn't provide anybody in.
15:52
Okay, so this should get us to where we can at least save some people in, and then we'll take a look at the next issue. So let's add some crew members,
16:00
take a look and add Camille in, maybe they got the food, and then we'll do Leela, maybe they did the hair for the movie. We can go ahead and update our movie,
16:10
and now you can see we got redirected right back to our movies pagination page, and we can see for this movie, we have a crew members of two now instead of zero. Go back in the edit,
16:20
we're not gonna see them yet because we haven't added it into the query, so let's do that next. Jump up to our update, we'll worry about creation here after we get all of our issues resolved,
16:29
and we're going to want to query this as mentioned earlier, directly from our pivot table. So const crew members equals await,
16:36
we can reach for the database module to do this, from crew movies, which is the name of the pivot table,
16:43
where that movie ID equals movie.id, and again, know that we're using the table and column names as we define this out of the database
16:51
because we're using the database module, which does not go through our Lucid models. And then lastly, we want to order by the sort order field,
17:00
give that a safer formatting, and edit into our page states, so crew members there. All right, now if we jump back into our browser, we should now see both of our crew members,
17:09
we'll give it a refresh, there they are. So all looks good, right? We can add in an additional crew member and it should work. So let's add Natasha, maybe she did makeup,
17:19
update our movie, and sure enough, added in A-OK, we should now have three, which we do. The issue comes in though, with these indexes
17:27
that we are defining on these elements. So we have crew zero ID, as we remove these items, this index is not going to auto update
17:35
because we're not touching those elements in any way whenever we make that removement. So if we take a look at the first item now, its first index is one. Why is this an issue?
17:45
Well, let's go ahead and try to update our movie and we'll see. So let's take a look at our flash message. The value that we're now posting up is crew with an array,
17:53
and the first index which we removed is now null, because there was no representation for the first index of this array.
18:01
What we could do, so let's go into alpinejs.dev, and I'm just gonna go ahead and copy their script right there from the CDN.
18:10
If you want to, you can go into your script and fully install it, but we're just going to want it on this one individual page. So we'll plop it in just before our script here. Believe it or not,
18:19
this is actually going to drastically simplify what we have. So we can get rid of our template as well as our script here as a whole too. So we go ahead and remove that, scroll on up,
18:29
and rather than doing an ID crew list, what we can do instead is x-data, define the state of say our members as,
18:38
and we can use EdgeJS's interpolation to now stringify our crew members and pass it directly into the members variable
18:47
on the client side via alpinejs. AlpineJS is like a very lightweight version of Vue or React.
18:55
It's going to sprinkle in client side JavaScript DOM manipulation for us so that we can easily perform re-renders
19:02
of our inner child content as it updates. This is going to make swapping out that index as we remove items super easy, and it's going to eliminate the issues
19:12
that we've seen thus far. This stringifies a JavaScript helper. Actually, I believe it's js-stringify. It used to just be stringify in version five and advanced js6,
19:21
they've now namespaced it underneath js. This will take what we pass in and safely stringify it so that we can easily pass it in as a value here in our attribute.
19:29
This x-data is how we define state for Alpine similar to again, Vue or React state. And that's going to make sure that we have access
19:36
to members inside of this div right here, essentially serving as kind of a micro component for AlpineJS. Meaning that we can actually dive back
19:46
into our crew member fields component. And if we wanted to, we could reapply all of this just right back out inside of AlpineJS.
19:55
And since we have all of our members, we can loop over it using Alpine and get rid of our EdgeJS loop there as well. We'll want to add in a template,
20:04
which is what EdgeJS uses for loops, do x for member in members, add in a key of member.id,
20:13
and in that template, we can go ahead and cut that out and paste it down at the bottom of our div here, scroll up a little bit and give ourselves an indent.
20:21
So next what we need to do is move the name from EdgeJS now to our client side via AlpineJS because Alpine now contains who our members are
20:30
rather than our EdgeJS loop. Now, so that we're not spending a ton of time mutating our forms so that they support AlpineJS, that's certainly doable.
20:40
Instead, what we're going to do is just swap these from using EdgeJS to Alpine. So let's go and jump into our form and select our span here
20:48
just to make sure that we're using the same styling. And we have a label class flex flex call,
20:54
and then we have that span with our crew member label. And then we have a select with the name and then our options.
21:04
So we can cut out our EdgeJS loop with those options and paste it in here. The name is now going to be an interpolation from AlpineJS
21:13
and we'll do double curly braces there so that we can inject and we'll want the index out of the loop. And then we can do crew, inject that index
21:21
and say that this is the ID field. So now every time AlpineJS loops over this to update it, it will inject the correct index based off of the loop index,
21:31
fixing our issue that we were running into. Now we are doing our selected check in EdgeJS still, but we need to switch this to Alpine
21:38
because member is now in our client side via Alpine. So we can use interpolated selected here
21:45
to say if member.sinist_id equals, and we can use EdgeJS to interpolate in
21:53
who this current sinist.id is. So a nice blending of client side and server side code here going on. Server side being our EdgeJS interpolations
22:03
and client side being our AlpineJS interpolations here. Scroll down here, we'll take care of our input next. Let's select the label that we have going on,
22:10
paste it in, do input type text, name, and then do our interpolated name here,
22:17
backticks, crew, inject in the index, and then add in that this is the title field. And then for the model, this would be an X model.
22:26
And I think for AlpineJS, you need to do members, reach for the index.title. Similar to how view two was. And end that up.
22:34
We can actually switch our select to do that as well, thinking about it. So we could scroll up, let's end our label before we do that. So end our label there. Now let's scroll up,
22:43
and rather than doing our selected, we can do X model, members, index, and reach for the ID, which means that we can get rid of this
22:51
on our option altogether. Scroll down a little bit further because this is no longer just going to remove the element, but rather remove the item out of our members array.
22:59
And we'll need to switch this to an at click now. And we can do members equals members.filter,
23:06
item or item.id does not equal member.id. And then for our ad crew members,
23:14
what we'll want to do is instead members push, we can do an ID and reach for EdgeJS's
23:22
since the first option.id there to plop that in and title as just an empty string,
23:29
directly mutating the state that we have up here in our x-data. You could also define this method directly in x-data and call it from our button to there as well. So let's give that a save,
23:39
jump back into our browser and okay, it looks like everything's still working. Let's do a sanity check refresh. Cool. Let's try to update our movie real quick just to make sure everything goes.
23:47
And it looks like it did not get our titles. Okay, scroll back down, right click the field, inspect, and it looks like I messed up the index
23:57
there a little bit. Let's hide our browser back away and see if we can fix that. Scroll on down to where we have that title. And sure enough, I put a square bracket in. Get rid of that. Try that one more time.
24:06
So let's jump back up into our browser, refresh for sanity sake, try to update it and cool. Okay, so it went okay, but we only now have one person. That's not right.
24:14
Okay, so I rewrote this whole thing for the crew members here three times just to see that I had a very small issue that was preventing our ad crew members button from working and it caused a few other issues.
24:24
So let's go ahead and hide our browser back away before we test this out. And the issue was that the ad crew members button
24:31
is outside the realms of our x-data. So it doesn't know or have access to that state because it's not encapsulated by it.
24:39
So what we wanna do is cut that state out of this div, wrap it in another div, since this div in particular is for listing out
24:47
each one of our members, it has that flex flex call on it. So we don't want that to be on there, but instead on a parent div,
24:54
and then we can indent everything up through our button and encapsulate our button now in that extra div that we have.
25:03
So now if we collapse down our template, our x-data and the div containing it is now including our button.
25:11
Additionally, I'm not sure if I had this initially or not, but we also wanted the selects X model to reference the members index Cynist ID
25:20
rather than just the ID. That might've been another issue that I had that was causing some strangeness. So with both of those in place, we should now have a working system
25:29
without the oddities that we've seen in the past, where we'll have a differentiation in our post behavior, depending on the number of items that we have being posted.
25:38
So for example, if we post up here with Quincy Abernathy as test two, Jermaine R as test three, and we maybe add in somebody else here as test four,
25:47
Natasha, test four, updates. There we go. We see that we get crew members of three. If we go back in the edit and we remove two,
25:56
which was one of the issues that we had solved because now this would be index one and this would be index two. AlpineJS should fix that for us. Automatically now making this index zero
26:05
since we've removed the original zero and now this index one. So let's try to update and sure enough, it worked. Awesome. So now we have two crew members
26:14
and if we take this now down to one, so let's run through that again. Leave just Natasha, update once more. Awesome. Now we just have one.
26:22
So everything there is now working a-OK.