
No Comments Yet
Be the first to share your thoughts and start the conversation.
Be the first to share your thoughts and start the conversation.
Complete source code available till this point of lesson is available at
By logging in, you'll unlock full access to this and other free tutorials on JSM Pro.
Why? Logging in lets us personalize your learning experience, track your progress, and keep you in the loop with new workshops, coding tips, and platform updates.
You'll also be the first to know about upcoming launches, events, and exclusive discounts.
No spam—just helpful content to level up your skills.
If that sounds fair, go ahead and log in to continue →
Enter your name and email to get instant access
In this lesson, we explore the implementation of a custom tags form using React Hookform. This discussion details how to create a user-friendly way to input tags, display them as badges, and manage their state with error handling features.
onKeyDown
to handle user input and update the state accordingly.00:00:02 And the last form we have to implement is the tags form.
00:00:06 Now, why didn't I do it alongside the title as it's quite similar?
00:00:10 Well, actually not.
00:00:12 This tags form will be a tiny bit more complicated.
00:00:16 You see, in this case, we'll implement a quite common, but a bit complicated system where you can input some things into an input.
00:00:25 And then based on that input, you can generate a couple of these different badges or chips, as we can call them.
00:00:33 So we can create multiple tags off of the same input and then display them to the user like this and even add a little X so we can delete them afterward.
00:00:41 So with that in mind, let's head down and let's go back to a question form.
00:00:47 Let's go back to the bottom of the question form.
00:00:51 And right here where we have tags, of course, we want to create a custom tag input.
00:00:57 Or should I say a way to track the array of tags that isn't so straightforward.
00:01:03 So as soon as someone inputs a new tag and hit enter, it should be added to some kind of an array of different tags.
00:01:10 If they provide the same tag, it should say tag where it exists.
00:01:14 And if there are more than three tags, it should provide an error message.
00:01:18 So, it's basically a custom input using React Hookform.
00:01:22 So, let's do that.
00:01:23 We already have some simple input right here, and as you can see, we wrapped it in a div.
00:01:28 That's because a Schatzian form component only allows you to have one child element.
00:01:33 So later on, once we want to display some tags right here below the input, we have to put it in a div.
00:01:38 In this case, our input is already looking pretty good, but I'm going to add a special onKeyDown property that's going to be called whenever a key press
00:01:49 is entered, and I'll call the special handleInputKeyDown function to which I'll pass the event and the field inside of which that event is being triggered.
00:02:00 For now, this function doesn't exist, but we'll create it soon.
00:02:04 And once we do, we'll be able to map over all of these tags.
00:02:08 So let's head over above and let's create this function.
00:02:11 That's going to be the last function needed to then finally create a question.
00:02:15 In this case, I'll call it const handle input key down, which is going to be equal to an arrow function where we accept an event and then a field as well.
00:02:30 First things first, we want to check if the event.key is triple equal to the enter key.
00:02:37 In that case, we want to prevent the default, so E.preventDefault.
00:02:42 Why?
00:02:43 Because the default behavior of the form, once we submit it or press enter, is to just reload the page.
00:02:49 In this case, we're not quite yet ready to reload the page or submit the form, we just want to submit the tag.
00:02:55 So once we tell the browser don't do anything yet, we have to extract the value of the tag input by saying const tag input is equal to E.currentTarget.value.trim.
00:03:11 So we want to trim it because it can only be a single word.
00:03:16 Also, notice how the event and the field are now complaining because of TypeScript.
00:03:20 So let's define event as react.keyboardEvent, and then specifically an HTML input element, keyboardEvent.
00:03:32 and then the field will actually be an object that has a value, which is going to be an array of different strings.
00:03:39 Now that we have the tag input, let's see if it exists by checking if tag input exists, and if tag input.length is lower than 15 characters,
00:03:52 In that case, we can also check if not field.value.includes the tag input, which means that we want to check if the tag doesn't already exist on the tags array.
00:04:04 If that is the case, we want to use the form to set a value on it of tags where we're going to take all of the previous field values.
00:04:14 So we're going to spread the field value and then append this additional tag input to the tags array.
00:04:20 That's going to look something like this.
00:04:22 After that, we want to simply clean up this event by saying E.currentTarget.value is equal to an empty string.
00:04:29 And then if there were any errors with it, we can say form.clearErrors and that's going to be equal to clearing the errors of tags.
00:04:39 Right here, it looks like our TypeScript is complaining a bit that the type string is not assignable to type never.
00:04:46 This is quite an interesting one, so we'll have to debug it soon.
00:04:49 But for now, let's also do some additional error handling by adding an else if, and then checking if tagInput.length is greater than 15, in which case
00:05:00 we can use the form.setError of tags input to type manual and then give it a message of tag should be less than 15 characters.
00:05:16 And we can add one more else if and check if field.value.includes the tag input, in which case we can say tag already exists.
00:05:27 It looks like I didn't close something properly.
00:05:31 There we go.
00:05:32 Now it's better.
00:05:33 And now we can use this field right here or field.tags to render some of the tags.
00:05:39 So let's try doing it right away by heading over to the tags and then trying to render the field.value dot map where we get each individual tag of a type
00:05:54 string and then for each one we can map over and for now let's simply show a string that's going to render the tag.
00:06:03 If we do it like that and try typing something you'll see that we get field.value is not a function.
00:06:09 That's because first I would want to make sure that they even exist.
00:06:12 So let's add a field .value.length is greater than zero, and only if it exists, then render a div And then within that div,
00:06:26 we can render this field value map.
00:06:29 So let me copy it.
00:06:31 And let me put it right here within the div.
00:06:35 Make sure to properly close everything so we're good.
00:06:38 And there we go.
00:06:39 We can also give this div a class name of flex-start, margin top of 2.5, flex-wrap, and a gap of 2.5 as well.
00:06:51 And again, Tailwind CSS IntelliSense saves me right here where I typed flex start.
00:06:56 It's just going to be flex start right here.
00:06:59 And now if I start typing, you can see that we have field.value.map is not a function.
00:07:07 So obviously we're not properly setting the value of the tag's property.
00:07:12 Let's see why this error is happening.
00:07:14 Well, first of all, this TypeScript warning could be pointing us to a solution.
00:07:19 It's saying that type string is not assignable to type never.
00:07:22 So what if we specified its type right here, where we say use form?
00:07:27 We can open up a new pair of code braces and then say z.infer and then type off ask question schema.
00:07:39 And then we can close it properly like so.
00:07:42 This way, this form can infer the value of the tags.
00:07:47 Also, we have some issues right here with the imports, but they got quickly fixed.
00:07:52 So now we have no more top script errors, but of course, just changing the type of something is going to make this error go away.
00:07:58 So we still have to figure out why field.value.map is not a function.
00:08:04 or in other words, why the field.value doesn't exist.
00:08:08 What I'll do first is I'll head into the handle input key down and I will simply console.log the field and the event.
00:08:17 If I do that, open up the terminal right here on the client side and type some kind of a letter here.
00:08:24 You can see that we have some different things.
00:08:27 We first get the field.
00:08:29 And the field actually does contain the value, but for the time being, it is set to an empty string.
00:08:35 And then the second thing is a click event, which actually works properly.
00:08:39 It got a key of T, which is exactly what I typed.
00:08:43 So now we know that a field exists, but it says that it is an empty array.
00:08:47 But why if I run that map?
00:08:50 Why is it throwing an error if this is indeed an empty array?
00:08:54 Maybe at the start it's a string, so at the start it cannot map over it.
00:08:58 And what I can do in that case is maybe add some question marks right here to make sure that it doesn't break if it doesn't need to.
00:09:05 And I will add a new console log right here where I can just console log the field or maybe even field that value so we can see what is inside of it.
00:09:14 I will now type some letter here, go to the terminal, and see that we get back a letter A from question form 137, which is right here.
00:09:25 And that means that we turn the field that value into a string instead of an array.
00:09:31 So that leads me a bit closer to the solution.
00:09:33 I think the fact that we're spreading the field functionalities here make it change the field in the usual way, which is just changing the string.
00:09:41 But in this case, we're doing something special, so I will not be spreading the field, and rather we're defining how we want to handle each input key down
00:09:49 on our own.
00:09:51 So if I save it and reload and type a letter T or test, you can see that works.
00:09:57 If I press enter, it actually adds it right here at the bottom.
00:10:01 And I can remove this comma and you can see that we have just test.
00:10:06 If I enter something like test one, you can see that it adds it as well.
00:10:10 What about test two?
00:10:12 That's good.
00:10:13 What if I type test again?
00:10:14 You can see tag or it exists, which means the error handling works.
00:10:18 And what if I type anything else?
00:10:20 it adds it.
00:10:22 But should we not limit it at three?
00:10:24 Yeah, that's definitely something that we should be doing.
00:10:27 So if I head above, you can see that right now we're allowing them to add as many as possible.
00:10:32 But then later on, once we actually submit the question, we'll ask them to remove some of these tags from the form.
00:10:38 So for the time being, we're okay.
00:10:41 And we can actually properly showcase these tags instead of just rendering plain texts.
00:10:47 And to do that, we will actually reuse the tag card we have used before.
00:10:52 So instead of simply returning a string, we can return a self-closing tag card property coming from tags.
00:11:01 And to it, we have to pass a couple of props.
00:11:04 Let's pass a key equal to tag since we're mapping over it.
00:11:09 Let's pass an underscore ID equal to tag, a name equal to tag.
00:11:14 Let's make it a compact version.
00:11:17 Let's give it a possibility to remove things, meaning we can click on it to remove it.
00:11:22 This is just a prop right now, but later on we will actually create JSX with a button that allows us to remove that tag.
00:11:30 I'll give it the isButton property and I have to give it a special function handleRemove which will be a callback function that calls the handleTagRemove
00:11:42 functionality that passes which tag to remove and from which field.
00:11:46 So let's just create it above so our linter is not complaining.
00:11:51 I'll create it as const handleTagRemove and just leave it as an empty block for now, which should be enough to allow us to head into the tag card and modify
00:12:04 it to accept all of these new props to achieve this new UI of the tag card.
00:12:09 We're going to make it even better than in the design.
00:12:12 So to do it, let me actually type something like React.js.
00:12:17 And I mean, just check this out.
00:12:19 Already by itself, it is looking great because we're reusing the tag card that we have created for the sidebar.
00:12:27 So don't forget about that.
00:12:28 Check it out right here.
00:12:29 React, React.js works like a charm.
00:12:34 But of course, we can make it even better by allowing us to remove it or to modify it somehow.
00:12:39 So let's actually style the tag card further.
00:12:43 I'll make it accept all of these new props, such as remove, which is going to be of a type boolean, isButton, which is going to be of a type boolean,
00:12:55 and handle remove, which is going to be a function that accepts nothing and returns void.
00:13:04 There we go.
00:13:04 And we can now accept all of these things through props.
00:13:07 After compact, we have remove and isButton and handle remove.
00:13:14 Great, and now we can use them right here.
00:13:16 Now, as you can see, currently, each one of these buttons is a link.
00:13:21 But right here, we're creating a question, and we don't really want to navigate off of the form.
00:13:26 I mean, just imagine if somebody typed a long question, and they click on this React.js, and we lead them to a tag details page of React.js.
00:13:35 They would be mad.
00:13:36 So for that reason, we don't want to make these links.
00:13:40 So what I can do is something like this.
00:13:43 I will extract what is inside of this link, like the badge and the showCount and all of those things, and I'll pull it into a new variable right here,
00:13:53 const content is equal to what we just copied.
00:13:57 And now we can render that content right here in between the link.
00:14:01 I'll soon explain why.
00:14:03 And it looks like I have to wrap this into a ReactFragment because we have multiple children elements.
00:14:10 There we go.
00:14:11 So for now, nothing will actually change.
00:14:13 We still see the same React tag.
00:14:16 But now, what this allows us to do is modify it further.
00:14:21 So we can say something along the lines of if.
00:14:25 Compact is true, then we can return, and then if isButton is true, then we'll return a button version of this component,
00:14:36 where we have a class name of flex, justifyBetween, and a gap of 2, and within this button, we render the content.
00:14:45 like this.
00:14:47 But if we don't wanna have it as a button, then we can return this link component right here.
00:14:54 And of course we have to properly close it.
00:14:56 There we go.
00:14:57 So now if I click on it, you'll notice that it's not actually gonna redirect me.
00:15:01 But of course we still have to make it work a bit better.
00:15:04 And what do I mean by a bit better?
00:15:06 Well, I'll go right here below this div inside of the badge and I'll check if remove is true.
00:15:14 In that case, I will render an image, coming from next image, with a source equal to forward slash icons forward slash close.svg with a width of 12, a
00:15:28 height of 12, an alt tag of close icon, a class name of cursor, dash pointer object dash contain invert zero and in dark mode invert and of course here
00:15:47 we can actually pass in the on click functionality of handle remove because on this button click we want to remove the tag for which we haven't yet implemented
00:15:57 the logic but we'll do that soon you can now see it here We can also style it a bit further by giving this badge a property of flex,
00:16:07 flex-row, a gap of two, and save.
00:16:11 That's going to give us some breathing room for the button.
00:16:14 We have this show count right here at the bottom.
00:16:16 And then we have if compact and if button or link.
00:16:20 So now we can see how we're reusing this content we created to show it within a button or within a link.
00:16:26 In this case, it's just going to be a delete button.
00:16:28 So let's go back to question form.
00:16:30 And let's actually implement the handle remove functionality, which I misspelled right here, under handle tag remove.
00:16:37 In this case, we will have to accept a tag of a type string, as well as a field of a type string array.
00:16:46 Within it, we can create a list of new tags, which is going to be equal to field.value.filter, where we get each T or tag,
00:16:57 and we want to keep all of them besides the tag that we just clicked on.
00:17:02 So if t is not equal to tag, that's going to give us all of the new tags without the one that we just clicked to delete it,
00:17:11 which means that we can create a form.setValue of tags and make it equal to new tags right here.
00:17:17 After that, we'll have to check if new tags .length is equal to zero.
00:17:24 In that case, we can form .setError, specifically set an error on the tags field of a type manual, and say tags are required.
00:17:36 We also have an issue right here on this filter saying that property filter does not exist on array iterator of string.
00:17:44 And that's because field within itself is actually an object that contains a value that is then of a type string array.
00:17:51 So it's so nice that it gave us that warning.
00:17:54 Then it gave us another warning because I misspelled value for values.
00:18:00 If I fix it, we're actually good.
00:18:02 And now if I click on React, you can see that it deletes it.
00:18:05 I can give it a shot with something like JavaScript.
00:18:09 Perfect.
00:18:10 Let's try Redux.
00:18:12 Oh my God, it's so nice to see these icons actually working.
00:18:16 And I think you can finally see why I spent so much time to actually get this Dev Icon automatic addition based on user input to work because we don't
00:18:27 have a set of predefined tags.
00:18:29 Actually, users will be able to create them.
00:18:31 So if we go with CPP for C++ or maybe just C, you can see all of those have their own icons.
00:18:39 We can delete them, we can add them, and it's just working amazingly well.
00:18:45 The error handling is working too.
00:18:47 So if I type test, you can see that it gets away.
00:18:50 I just have one issue and that is if I click on a tag, it actually brings me back to the first question for some reason.
00:18:57 So let's try to fix that.
00:18:59 How would I start about fixing that?
00:19:00 Well, if we go into our form fields where we map over them, we actually want to see what happens once we click on a tag card.
00:19:09 Currently, each tag card is a button because in this case we're providing it the isButton property and it is possible that this button tries to submit
00:19:20 the form because you can see if I reload we have no errors.
00:19:24 What happens if I try to click ask a question?
00:19:27 It brings us to the top and says title is required.
00:19:30 The same thing happens when I create a tag and click it.
00:19:34 It tries to submit a form.
00:19:35 Okay.
00:19:36 So how would we make that not happen?
00:19:39 One solution that I can think of is maybe giving this button on on click, which is going to be something like handle click.
00:19:47 And then we can define that handle click right here.
00:19:51 And we can prevent the default.
00:19:53 So we can get the event, the click event, and say E.preventDefault, which would prevent the default behavior of actually trying to submit the form.
00:20:04 And this is going to be a React.mouseEvent.
00:20:08 Now if I reload and try it again, I'll type React and click it.
00:20:13 You can see nothing happens.
00:20:15 And now we can freely click it.
00:20:17 And if we want to delete it, we can.
00:20:19 We can create another one, delete it.
00:20:22 Now the tags are working perfectly.
00:20:24 And with that, we're getting very close to actually test out our form in the handle create question functionality.
00:20:31 But I've noticed some issues right here in mobile.
00:20:34 some horizontal scroll.
00:20:36 That should never be happening.
00:20:39 Even though it looks good right now, but you can see that some elements are floating outside of the view, like these inputs,
00:20:45 text fields, and the editor.
00:20:47 Now, I'm not sure which specific field is pushing it out.
00:20:51 There's one specific exercise that I can do to test that out.
00:20:55 So let me try to open up a new Inspect element.
00:20:59 Let's try to replicate what we had.
00:21:02 They're going out, and now I can try to zap specific elements by just deleting them from the view.
00:21:08 Like if I take the Editor component, for example, this one here, and I press Backspace on it, it's going to delete it.
00:21:16 Once it does, what can you see?
00:21:19 If I collapse it, you can see that they're no longer going out.
00:21:22 So what have we learned from that?
00:21:24 Well, the editor is the culprit.
00:21:27 The editor is actually going outside.
00:21:29 So let's head over in the components editor and let's see if we can maybe modify it so that it doesn't jump out.
00:21:36 I'll just reload the screen so we actually get back the editor.
00:21:40 One thing that I tried out before was changing the display property of the editor to grid.
00:21:47 And if you do that, you can notice that immediately now we have the top part of the grid, which is this header or the toolbar,
00:21:54 which now scrolls horizontally.
00:21:56 And then right here below it, we have the content.
00:21:59 So this is working perfectly and no longer does it jump out.
00:22:04 Great.
00:22:05 Now let's figure out where do we get access to these values from these different inputs and editors.
00:22:10 If I go back to the question form, I can try to console log the data that we're receiving.
00:22:15 So let me go right here.
00:22:18 Let me get access to the data, which is going to be of a type z.infer type of ask question schema.
00:22:26 And right here, let's console log the data.
00:22:31 If I save it, I'm going to enter some kind of a title, like, how do I learn to code?
00:22:38 I'll enter some additional texts, like, how do I learn to code?
00:22:42 Please help me.
00:22:45 AI is coming for me.
00:22:48 And I'll go ahead and add tags.
00:22:50 I'll say AI web dev and JavaScript.
00:22:58 I'll open up Inspect Element, expand my browser so I can better see what's happening, and click Ask a Question.
00:23:05 And would you look at this?
00:23:06 We get the title, we get the content right here, and you can see that all formatting is properly applied.
00:23:12 We can see a couple of backwards slashes and then ends for new lines, and then we can see the double asterisks for bolding.
00:23:20 In the same way, we get the array of three different tags that we are ready to submit.
00:23:24 So do you know what this means?
00:23:27 Well, this means that now we have successfully created all of the frontend parts of this application necessary to be able to communicate over to our database.
00:23:37 We have a form that just waits to be connected to the backend.
00:23:41 And after that, immediately we already have a homepage that is just waiting to pull the new questions once we do create them from the backend and then
00:23:50 display them right here.
00:23:51 Real ones, of course, not these fake ones.
00:23:53 So let's commit this by saying implement tag input, commit and sync, and take a well-deserved break.
00:24:02 Or if you're really, really hyped, you can continue straight away because now we're diving into backend development.
00:24:10 Finally, right?