Advanced Sortable Drag and Drop with React & TailwindCSS

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
today we're going to build a custom notion inspired conone board with react tail1 CSS and frame R motion no drag and drop libraries required this will allow us to add new tasks to each column drag cards between columns including highlighting the position that the card is going to move to delete cards from columns like this and animate all of the layout using framer motion we'll even take a quick look at how we can persist the cards whenever the page is refreshed all of this code can be found on my website both in JavaScript and in typescript along with a whole bunch of other animated UI components made specifically for react T1 CSS and framer motion anyways let's get started to start I just have this basic notion con Bon component this is just going to add some basic styling specifically I'll give this a height of screen a width of full a background of neutral 900 and a text of neutral 50 to give it a dark mode feel below this I'll add a new component called board the board component will house all of the actual functionality and everything this can return a div with a couple of classes on it so I'll give it a display of flex a height of full a width of full a gap of three an overflow of scroll and padding of 12 I'm giving this a display of flex because it's going to house all of our individual columns and then the Overflow so that we can actually scroll to see all of our columns if they're off the side of the screen or below the bottom of the screen make sure that we go back and we actually instantiate this component and we should now see that we have overflow scroll bars on both the X and Y AIS I'm going to define a piece of State called cards eventually this is going to hold the actual data for each of our individual cards but for now we'll just make it an empty array we'll make one more component called column column is going to house as you would expect each individual column and this is going to take in a couple of props so the first one's going to be title that's just the literal title that's at the top of the column and then heading color will be the color for that text the column prop will be a unique string between each of our columns that's going to help us figure out which column we're dragging to and from and then cards and set cards or just those pieces of state that we talked about a second ago I'm now going to go back up to our board and I'll drop a couple of these columns in so looking at the first one as an example as a title of backlog a column of just lowercase backlog a heading color of text neutral 500 and then I'm also passing in cards and set cards I then have three more columns for to-do in progress and complete the most important thing here really is just that the column props are similar to what I have here so name one backlog like this another Todo another doing and then another done or just make sure that they're unique and you remember them for later we can now add some basic markup for each of these columns so on the wrapping div for the column I'm going to give it a width of 56 and a shrink of zero where using Flex to actually lay these out so we want to make sure that we set shrink to zero so that it doesn't actually try to shrink all these into the size of the viewport and then for the markup for the actual heading of each column we have this wrapping div with a margin bottom of three a display of flex and items of Center and AD justify between the H3 is just going to render our actual title with a font of medium so it's like a font weight of medium and then we're passing in that heading color and then the span is just rendering the number of cards that are in each column so I'm giving that a rounded border which actually I think I refactored it you actually probably don't even need that piece but you do need the text of small and the text of neutral 400 eventually we're going to filter this down to just just the cards that are actually in that column but now we should just see zeros across the board well then add a piece of State called active for now this is just going to kind of be a static value but in a little bit we're going to circle back and whenever you actually hover a card over each column we'll flip this bullly in and that'll help us style our column specifically I'll make one more new div that's going to house all of the actual cards that'll have a height of full a width of full a transition of colors and if that active prop is true then we'll add a background neutral 800 with 50% opacity else the same color with 0% opacity you can actually see what it looks like manually but just toggling on and off that active State and now we should have something that looks like this and now we can move on to actually adding the data for our columns and styling that up so I'm going to come below my components here and add my data which I'm going to call default cards this is just an array of objects where each object has a title that's a string an ID which is also a string you could use a number as something else but it is a little bit easier here if we use strings we'll see why later and a column which should map to the column props that we talked about earlier so you'll notice this has Columns of like backlog and to-do and doing and done I'll come back up to our cards State and pass in our default cards as the default state for this and those are already being passed through to our individual columns we can now filter these cards down to only the ones where the column of each individual card actually matches the column prop that's being passed into this component for my count here I'm going to switch it from cards to filtered cards and we should actually see the counts change so we have four backlog cards three to-do cards two in progress etc etc and now we can actually move on to rendering out our cards so we're going to map over all of our filtered cards and return a new component which we're going to call card remember to pass in the key for this you can just use the normal ID and then just spread in all of the other values for the card below we'll create our new card component make sure to destructure our title our ID and our column this component can return a fragment with a div inside of it we need a fragment because later we're going to have these little indicators which we also need to render inside of this component but that's getting a little bit ahead of ourselves to actually style our div inside of this we'll add a P tag with a text of small a text of neutral 100 and then just make sure to pass in the title now we should see that we're actually rendering the text in each of our cards and then on our wrapping div I'm going to give it a cursor grab to start and then whenever we're actually actively grabbing it a cursor of grabbing I'll give it rounded borders a one pixel border that border should add a color of neutral 700 and a background of neutral 800 I'll also add a padding of three and we should now have our cards actually rendering we should be able to see that this little grab animation happens when ever we actually click on each of our cards what we should notice though is that we can't actually drag our cards yet so if I try to go and drag my cards it it's just going to highlight the text and what I can do to fix this is come back over to my div and give it a draggable equal to true this is just a native HTML property that allows you to actually drag a component now we should be able to see that whenever I grab one of these components I can drag it around the screen now what you might notice is that these cards look a little bit crammed together like there's no space on the Y AIS between each of them and that's because instead of adding margin between them we're going to add indicator components remember whenever we dragged around in the little demo earlier how there was those little kind of blue purple lines we actually need to render a component for this I'm going to call that component indicator and we're going to render it right on top of our main div here in our card component that's going to take in two props one called before ID which is just going to be the ID this is going to help us later cuz we're going to need to figure out where we actually want to drop this card whenever we're dragging it we also just want to make sure that we pass through the column and then it come down and actually Define our component this component is just going to return a div it's going to have a margin on the y- AIS of 0.5 a height of 0.5 a width of full a background of violet 400 and I'm setting the opacity to 100 here for now but I am going to change that in a second and then finally we just need to pass in two data attributes so like I mentioned with Data before this is just going to be an attribute that we're going to be able to use to pull off which card we're actually dragging near and then the data column attribute is going to help us whenever we need to figure out which indicators we actually want to check so if we're hovering over the to-do column we only want to be fetching and check checking for the drop indicators in the to-do column we should now see that these are all rendering over on the right but we do also want one right at the bottom of each column so I'll just add one more in here and I'll give that a before ID of minus one so that we can differentiate it from the other indicators later I'm going to go ahead and change the opacity on these back to zero cuz we don't want them all just highlighted all the time and we'll actually change that later when we're hovering the cards we can move on to actually adding The Styling for that little delete area that I had set up earlier I'm calling this a burn barrel that's just something I came up with but call it whatever you want and we're going to render that right after our columns and just pass in set cards as the only prop that this is going to need I'll Define my new component for that return a div from it and then this is also going to have an active State it's going to perform essentially the same function as the other active state that I explained earlier we can drop in our class names so a fair bit more on this one than some of the other ones it's going to have a margin top of 10 so that we can align it with the top of the actual cards a display of grid a height of 56 and a width of 56 a shrink of zero for the same reason as the other columns had shrinks of zero and then Place content in the center to actually Center everything a one pixel border that is rounded and then a text of 3 XL now if my state is actually active I want this to like show up as red so I'm going to give it a border of red 800 a background of red 800 with 20% opacity and a text of red 500 if it's not active then we can use the same values essentially but with neutral and now we should have something that looks like this so it's just like a red and gray Square now inside of this div I just want to render a couple of icons so if it's active I have one called fire and then if it's not active I have one called trash the fire one has an animate class of animate bounce this is just a normal tail one class it's just going to give it a little basic animation and I'm getting these icons from react icons so just yarn add or npm install react icons if you want the same ones really quick just toggling on and off the active State we should be able to see the difference between these two values I want to be able to have a little form where I can add a new card in each individual column to do so I'm going to instantiate a new component which I'm going to call add card and we can just drop this directly under the last drop indicator inside of the column component this will we will also need to take in the column so that we know which column to add the card to as well as the set cards function we can Define this component down here at the bottom and this should have two pieces of State one for text so we can actually track what we're typing into the input and then one for adding and that's just going to be a Boolean toggle to whether or not we want to show the form or we want to show the little add button we can start by styling the button so if we're not actively adding we want to show a button that button should say add card and then I'm also giving it this little fi+ icon whenever we click this we'll set adding to true and then we'll add some classes to this so I'm giving it a display of flex a width of full item Center gap of 1.5 padding on the x-axis of three padding on the y- axis of 1.5 a text of extra small text neutral 400 transition colors and whenever we hover over this a text of neutral 50 and now we should see that whenever I click on these they go away because we're not rendering anything else when we click on them to fix that I'm going to add a form and inside of that form will render a text area whenever you type into this in the onchange we'll just make sure that we're updating the text I'm also going to add autofocus so that whenever we click on that button it just automatically focuses this text area and then for the classes it's just a width of full a rounded border radius a border of one pixels a border of violet 400 background Violet 420 a padding of three text of small text neutral 50 placeholder Violet 300 and whenever you focus on this an outline of zero pretty basic stuff now below this I want to have a couple of buttons that show either close or add card in the wrapping div we're just giving a little bit of margin Flex item Center and then justifying it to the right for the button a padding on the xaxis of three y AIS of 1.5 making the text extra small as well and then I'm just giving that neutral colors cuz it's kind of a secondary action but then for the actual add button I'm going to give this a type of submit because it's going to be like our main submit button and then just pretty much the exact same Styles as the other button except I'm giving the background the kind of lighter neutral color and then making the text dark now because this is a type of submit I can come up to my form and say onsubmit and we'll call a function that I'm going to call handle submit Define that up at the top and then remember to call event.prevent default so that we don't render the screen whenever we actually submit the form we want to make sure that we're not adding empty cards so I'll just add this little check to say hey if the text with any wh space trimmed off is has some length or doesn't have any length rather will return else we can actually Define our new card with our column and our title and our random ID and just add that into our cards for that column and then set adding back to false we should now be able to see that I can type in some random value and then click add and we've added our new card now that everything is actually laid out we can get to some of the Dragon drop stuff so I'm going to start by coming up to our column component and I'm going to create a new function called handle drag start this is going to take in the event from the drag as well as the card and on the event you can actually store any data you want on these events well so long as it's a string on the data transfer object so in my example I just want to know what card is being transferred from one section to another so I'm going to add e. datat transfer. set data I'm going to call it card ID and then just make sure that I'm passing in that card ID I'm going to take this function and I'm going to pass it through to our card comp component and then on the main div we're going to pass it into the on drag start function and then just make sure that we're passing in that event as well as the title the ID and the column now as a quick example of this working we can come down to the burn barrel component and I'm going to define a function that I'm going to call handle drag over inside of handle drag over we need to call the event.prevent default function after we've done this we should see that dragging over this component gives us this little green plus essentially calling event.prevent default is telling this div like hey this is a place that you can actually drop something now under event to prevent default I'll set active to true so we can change our Styles and make everything red again then under that I'll create another function called drag leave which is going to set active back to false that way it actually toggles on and off unless we actually drop on it in which case we need to call an on drop function I'm going to call it handle drag end and inh handle drag end we can actually delete a card so I'll pull the card ID off of our data transfer so data transfer. getet data card ID and we should actually be able to see this console logging out now so whenever I drag a component or whenever I drag a card over onto our burn barrel we should see the ID logged out to actually delete it I'll call set cards and then I'll just filter out any card whose ID is equal to that ID and finally set active to false and we should now be able to see our cards being deleted to make this delete animation a little bit more satisfying we can start by adding some basic layout animations I'll come up to the top and I'll import motion from frame or motion and then come down to our card component and on the div I'm going to turn it into a motion. div allthough we really need to do to add some basic layout animations to this is to add the layout prop this is automatically going to see when is removed or moved around in the Dom and animate it from one position to another but in the case of these cards specifically we also need to add an ID so layout ID is equal to ID we need this mainly for moving between columns so whenever we move from one column to another it need to understand okay this is the same component it's just being rendered in a different place now we should now see that whenever we delete one of our cards the card that's under it animates to its new position what I don't like though is that the ad card button under it does not animate that just snaps into a new position so we can fix that in the exact same way we just come down to our form form turn that into a motion. form and give that a layout prop these are never going to move between columns so we don't actually need a layout ID and then we'll just do the exact same thing for the button after this we should now see that the button and the form actually animate as well I'd like to Circle back really quick and just make sure that we're actually highlighting The Columns like we talked about whenever we're dragging cards over them and I'll start by coming up to our column and defining a new function called handle drag over this is also going to need to call event. prevent default for the same reason as we talked about a minute ago and then we can just call set active true we'll pass this into the on drag over in our wrapping div and we should now see that whenever we hover over each of our columns it highlights but we don't remove the Highlight which is a very easy fix we just need a handle drag leave function to set active back to false and then we'll pass that into the on drag leave prop on that same diff now we're almost there as we drag it around we see it turns back off but whenever we actually drop the cord it's going to stay highlighted so to fix that we'll create a handle drag end function set active defaults on that as well and then pass that into the on drop prop and that should be it we should now be able to drag a card around and see that it highlights everything as we expected to next I'd like to highlight the indicators that we created earlier so I'm going to create a function called highlight indicator and I'm going to call that from our handle drag over function passing it in the event the first thing we need to do here is actually get the indicators that are in this column so I'll create one more function called get indicators and all that this function is going to do is call document. query selector all and then query for that data column with whatever the specific column is remember that we passed this through this is exactly why we needed to do that because we only want to grab the columns that are in whichever column we're curent currently querying for we should be able to hover over these and see them being highlighted like this and once we've got our indicators we can actually try and find the nearest one I'm going to create one more function for returning this value I'm going to call that get nearest indicator that's going to take the event as well as the indicators that we just fetched we just want to call an array reduce on these indicators instead of accumulator and current child I'm calling them closest and child and then for our default value we'll just set the offset to negative infinity and then the last element in the array for each of these I just want to start by getting the position of the indicator on the page I can do that by calling child.get bounding client wck and then I want to calculate the offset between the mouse and wherever that top is so the easy way to do this would just be event. client y minus box. toop but the problem with this is we don't really have any buffer at the very top of our column so we can't hover anywhere above that so to fix that I'm just going to add a little bit of offset to this I'm going to call that distance offset and for mine I'm just giving at 50 pixels that seemed like plenty we can then just check that the offset is less than zero and that the offset is greater than whatever the current closest offset is if so then we'll return these values as the new closest offset else we'll just keep whatever the current closest is finally we just need to remember to return this element and to wrap it all up we'll actually style that by increasing the opacity from zero back up to one now as I drag this around we should see it highlighting but it's leaving everything highlighted which is not exactly what we want so I'm going to create one more function called clear highlights and pass that in our indicators we're going to need to call this from a couple of places so these elements are actually going to be an optional prop here so if they are passed in we'll just use those else I'll call that get indicators function that we talked about earlier and then just for each of those indicators we're just going to toggle the opacity back to zero now this is almost there but we do also need to remember to clear our highlights whenever we drop our card or whenever we leave one column so I'm just going to come down to our handle drag leave and our handle drag end functions and just call the same function from there notice that I'm not passing in any indicators that's because that function can grab them itself and that should be it we should now be able to drag this around and it will highlight the closest indicator now with all of that done we have the majority of the boiler plate in place for actual setting up our shifting of cards between columns we start by coming down to our handle drag end function and I'll start by pulling our card ID off of that data transfer object I'll then grab our indicators and also get our nearest indicator using the same functions we set up just a minute ago and then remember that on our indicator we set this before data set value and this is actually linked to whatever card is nearest to where we were hovering so this is where that actually comes into play if for whatever reason we don't have one I'll default it to negative 1 all of them should have one but negative one is just going to indicate the very end of the list and then we can can actually do our checks so first if before is equal to whatever the card ID that means you're trying to put it in front of itself that doesn't make any sense so just don't do anything if that's the case if that's not the case I'm going to start by creating a copy of our cards I'll then find whichever card is equal to the card ID that we just grabbed if for whatever reason the card's not there this shouldn't happen but if you're using typescript it's going to yell at you so I just added this check anyways then we'll actually grab that card and just update the actual column so remember that we're potentially dragging it to a new column so we want to update the column on that value then we'll actually filter out that card now that we have a copy of it from the original copy list we know if we want to move it to back again like I mentioned a minute ago if before is equal to -1 so if we do want to move it to the back we can just push it to the end of our copy array else we actually want to splice it into place so first we have to figure out what index to place it at we can do this by doing copy. find index and then figure out which index or which elements ID is equal to that before ID again just kind of for typescript purposes if you happen to be using it if we don't have insert at index then return else we'll just splice it into into that index so copy. splice whatever the index is zero and then the actual card data then finally just update the cards so set cards to that copy array and this should all be working now so with animations and everything we can grab between one column move it to another column move it to the trash bin see all of the highlighting everything you can even create a new card and then drag that between columns and then delete that as well now the last time I did a video like this a whole bunch of people commented asking me to show how you could actually persist this data between page loads I don't want to do a full video on you know setting up a dat datase and everything because there's a million different ways that you could do that so if that's something you want to do I challenge you to set that up yourself if you're using a SQL database if you're going to set it up one way if you're using a nosql database you're going to set it up another way but I will show you super quick how you can set this up and just make it work using local storage if that's good enough for whatever your use case is so first I'm going to remove our default cards from our state and turn that into just a basic array below that I'm going to create another piece of state which I'm going to call has checked we'll default that to false I'll then create a use effect which is listening for the cards state to change and this is where you can actually see that has checked value coming into play so if we have checked then we actually do want to run this next piece of code local storage. set item cards the reason we don't do this initially is we're initializing cards as an empty array and just remove that check if you want you'll see the problem it's just going to circularly kind of set your state to an empty array which is not what we want so anyways all that this is doing is saying anytime that cards change we want to update a local storage value called cards with a stringified version of our list actually get that value on Mount I'll create another use effect with an empty dependency array inside of that I'll grab the cards data from local storage if we actually have any cards data then we'll parse the Json else we'll just pass an empty array and finally set that has checked value to true so that the use effect above it can actually run now we should see this working I already have a couple of cards in here as an example but I'll add a new one really quick click add and we can move all of this around refresh the page and it should all continue to work exactly as we expect I can delete something refresh again and will stay deleted so yeah again if this is good enough for your use case it's as easy as this if you actually want to persist this data but I do encourage you to push forward and figure out what actually will work for your your use case if you have a specific database in mind or whatever it may be
Info
Channel: Tom Is Loading
Views: 22,477
Rating: undefined out of 5
Keywords: algorithms and data structures, programming, web developer, coding
Id: O5lZqqy7VQE
Channel Id: undefined
Length: 21min 12sec (1272 seconds)
Published: Thu Feb 08 2024
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.