Superforms!
- Andreas Söderlund Is programming art, engineering or both?
I will talk about Superforms, the forms library for SvelteKit that handles validation, error handling, data coercion and so much more, all while staying true to the Svelte+Kit principles.
Transcript
Hello, I'm Andreas, the creator of SuperForms.
I'm going to show you how easy it can be to make a database backend with this form library,
how it simplifies the process to just a few lines of code,
so you can focus on more interesting and productive things than writing validation code, error handling, status messages and so on.
So let's get started right away.
So what we have here is just a freshly installed SvelteKit site, the skeleton app
and I add a checklist here so we can track the progress. What we're going to
build is a so-called CRUD application. CRUD stands for create, read, update, delete
and it's the fundamental data management that you have to do to handle data in
a backend. So let's just start by installing SuperForms. To do this you just install two
libraries with npm or pnpm, SvelteKit SuperForms and zod. zod is the validation library that
SuperForms uses to simplify the validation process. Validation can be very complex so
it's nice to have a dedicated library for that and zod makes a great job. But before
we can validate anything. We need a route so we can actually display something in
the backend and I'm thinking something like users slash and then an ID for the
user which is very common in REST APIs for example. So let's make a route here.
We can just right-click and select SvelteKit create route in the routes
there. As we said users and then we're going to use double brackets for ID so
it's optional so we can also create a user from this URL and we want the
server page and we want a normal page for displaying the data. So when we have
this we can get started in displaying something. Let's just start by going to
users so let's add this checklist as well and why not the title so if we look
at the first step in the checklist here we want to display an empty form and
this is of course because we want to do the first step of the CRUD which is to
create a new entity which is the user and this validation schema in zod is a
a very nice way of expressing this user
entity that we're going to make. We can
just create it directly by importing
"zod" and then we create the schema. We call
it "userSchema" and the syntax as I
said is very simple. We can just make an
object here and in this object we have
all the properties that we expect from
user so let's say that we want this user to have a name, email address but for the
database we should also have an ID so we can refer to the user in some way. So if
we start with ID just let it be a string and we said it has to be at least one
character. We have the name we can should also let it be a string and we can let
it be two characters and finally the email address then we can use a sold
helper for that, email. So we got our user schema and now we want to display a form
based on this schema that we have. Just to make things simple, we can just paste a very
simple form in here. We have all the three fields here that we want from the
schema. We have the ID, the name and the email. So let's check the first one just
to make some progress here. And the next step is to post this form and validate
data. So we added a form action here and this is where we're going to post which
is pretty much automatic with Swellkit and if you want to see it you can just
extract the form data from it and take a look at it. Now I'm posting this form we
got the form fields here all empty but let's see now how we can simplify and
make things much more convenient. So let's start by adding superforms.
So we have added this supervalidate function from the SvelteKit superforms
server. We should call superforms to preload this form that we have created
here with, for example, constraints so we can get the client-side validation. And
to do that we need to call supervalidate in the load function. So we can make a
variable called form and there we can
just await on super validate and we
send it this user schema and this
will create the default values for the
form so it's type safe it will create
constraints and some other things when
we do this we just return it to the
client and then on the client if you look
on this side we should import the
client version of super forms here
which is called super form. So it's a form on the client side and it's a
validation on the server side. So if we want to create the super form now on the
client side we just deconstruct this form, use that the data form that we sent
here. Now I have a form ready to be used on the client and the final step is to
connect this to the form action that we just created here. So instead of starting
to manually extract the data from the form data. Maybe call also saw the user
schema manually to validate it, check for errors, try to map the errors to action
data. We can make things truly simple with super forms. So instead of this now
we can just use this in the log function. We can wait for super validate but now
we send in the request as well as the schema and if we console.log form now
let's see what happens. So instead of having to do things manually here now
and get all kind of useful information we can see that the validation is false
the valid property here which shows that the validation didn't succeed and get
all the errors mapped from zod conveniently to each field and we have
the data here which is valid or not depend on valid here empty will be true
if we didn't send anything to super validate as we did in the load function
but in this case we send post data to it so it will not be empty and the
constraints is the web browser standards for client-side validation so we can
actually spread these properties on the form fields and we'll see that we get an
immediately client-side validation so with that I think the second step is
done! So far quite basic stuff right? Which is nice in a way, SuperForms is
made to be easy to use so you can just post and validate data in a few lines of
code. But let's keep going and see if we can make things just as simple even when
using this form data with a CRUD backend that we're making. So this user schema
can be seen as a representation of a database table entry and if using an ORM
a user should look pretty much like the data type of the schema. To make things
simple in this example we'll create an array of users and pretend that it's a
user table that we can access through a real ORM. So to do that we can just make
it be an array of users and then the type of this array will be the data type of the
schema which can be extracted using solves infer type. So we can infer it as
the type of the user scheme.
And let's add some test users.
Now if we want to display one of these users we can do it very easily with
super validate but to do that first we need to bind all those fields that we
have in the form to the actual form data which is done just as we do it in Svelte.
bind value to form which is a store and then get autocomplete for the fields so
we just put them in like this
no difference yet because it's a form is still empty but it's so simple to
populate the form now that we connected it to super form so if we just want to
for example take the first user in the user database and populate the form we
We can just do this.
UserDB, the first entry, send it in with the user schema.
And now as we save, the form is populated.
As long as this entity that we created matches the schema, only partially is fine too.
Then it will be able to send it to super validate and the form will be populated.
And now let's take advantage of SvelteKit's easy to use root parameters to display any
user in the database.
To do this we can extract the params parameter here and then instead of accessing the user
db directly we can look for a user based on this parameter.
Now we just supply the user instead and hopefully now if we enter a user here, well what do
you know?
Now we can access any user in the database.
And with that, I think we skipped ahead a bit.
We have actually both fetched the entity
and displayed it in a form.
So that was two quick and easy steps.
Now let's go back to this create new entity
and see if we can tackle that too.
It will be very convenient to use the same schema
and load function,
and that is basically what we're going to do.
But there is one difference here.
Now when we're creating a user, we cannot have an ID, because it's not set yet.
To make this possible, we need to extend the user schema, to make it possible to post a
user with no ID, when we want to create one.
Fortunately, SUD makes this quite simple.
Just make a new variable, we can call it "crud_schema", and then we can use "user_schema.extend".
And here we just add the variables that we want to extend.
So in this case, ID, and then we can just specify user schema shape, which is the shape
of the schema.
And then we just use the ID again, but now we append this optional to it, which will
show that we can choose if we want to send an ID or not.
And now we can just replace the user schema that we had here in the load function and
form action with the CRUD schema. And as we made the root parameter id optional, now when we just
go to /users it won't find a user in the database and we will be presented with an empty form.
Though this might not be perfect because if we enter any user id that doesn't exist
we will still get an empty form. In this case we want to send a 404 error. But that's no problem,
we can use a quite simple check to see if we requested a user or if we just want to create a new one.
If params_id exists but still there is no user
we shall throw a 404 error and we see instantly that it works with an incorrect, a non-existing
database id and like this and if we do no user we get an empty form.
Finally, we can create a new entity and to do that we can just for now just entering
it some test information and see what happens.
According to the server post it looks okay, but what if the data is invalid?
Of course we have to check for that before inserting anything in the database.
So we can use this valid property of the object returnFromSuperValidate and just check.
This is how easy it is to return a failed form with superforms.
Just return it as in the load function with the fail function from SvelteKit.
So at this point in the code we know that the data is valid so we can insert it into
the database.
This is where you will usually call your ORM and it will generate an ID for you.
Again we're keeping things simple and we'll just generate a random ID.
And then we create the user based on the form data.
And finally just insert it into the database.
At the end of the form action, since everything is okay here, we just return the form just
as we did in the load function. And this works, but we want to give an indication
to the user that we have created a new entity in the database. And to do that we
can use a helper function called message, which is part of the superforms package.
So instead of returning form we can just return message. The form will be the
first parameter and afterwards we can just send anything we like here. So let's
Let's just put "user created".
And to show this message on the client, now we will start to deconstruct more properties
of this object returned from SuperForm.
So to use message, we can just deconstruct message like this, and then we can display
it anywhere on the page.
Most properties extracted from SuperForm are stores, so they will be reactive.
So let's try to create a user and see what happens.
Now let's do two things to reset the form so we can create another user easily.
We will extract "enhance" from the superform object and also set the "reset form" option
to true.
After applying use and hands to the form like this, everything happens on the client.
Now the form will also reset to its initial empty state.
Let's create the user again.
But what if validation fails?
For example, if we just submit an empty form.
We see the errors on the server, but how to display them on the client?
Well, to do that we keep deconstructing stores from the superform object.
This time it's called errors.
This is a store with the same names as the schema fields, but each property will have
a string array or undefined as values.
This makes it quite easy to display errors for each field.
We can see right away that errors are displayed.
Let's add just a little bit of style.
Before we check the create entity box, it would be nice to list all users so we can
ensure that the date is created.
To do this we can query the database for user names and IDs in a real case, but for now
let's simply send the array of users to the client and display them there.
Now if you click on this, they should be displayed in the form.
Seems to work fine.
Let's create a new user now so we can finally check this create box.
We can just keep going from here and basically post the form and see what happens now.
Now we created a new user.
But if we look at the data we sent to the server,
we see that now we have an ID, which we should use to distinguish between if we want to create or update the user.
So in the form action now, we will branch just after the form is valid.
If an ID exists, we want to update the user.
But if not, we do as before and create the user.
Updating the user will be a job for the ORM usually.
In our simple case, we will find the index of the user
with form date ID and throw a 404 if it doesn't exist.
Now we'll just update the user at that position in the array.
We need to add the ID again because according to the schema the ID is optional so we need
to ensure that the ID exists here.
And finally we should return a message that the user has been updated.
So let's see if everything works.
This important customer is now very important customer.
Submit user updated.
You see that the name is now very important customer.
So we have posted the form, validated the data, updated the entity with this data.
Before we start working on deleting users, let's display a button for creating a user
when we are at an existing one.
To do this easily, we'll use the formId property.
So we can test if an id exists.
If so, display the button.
So we can make an if statement and test if the form isn't empty, then display the button.
And it will just link back to the user's page, with no id.
For this we will also use a form, since the default action of a form is get, and that's
what we want to do with the button.
And the button is also what we will use to delete the user.
Since a button can have a value that will only be posted when that specific button is
clicked, we can use that to determine whether a user should be deleted.
Again we'll use form_id to check if it should be displayed.
That's also a stylus button.
On the server we now need to do a little change, since the delete field shouldn't be a part
of the schema.
So we'll start by getting the raw form data from the request
and pass that to super validate to validate the form.
Then in the form action, we check in the form data if the
delete field exists.
If it does, call the ORM, or in our case, splice the array
based on the posted ID to delete the user.
Finding the index is shared between delete and update.
So now we can just splice the userdb array.
After deleting the user, this userid URL won't exist anymore, so we can't return a message.
So we need to redirect to the users page.
We do that by throwing a redirect.
Should we try to delete the user?
There, the important customer is now gone.
Maybe that was a bit abrupt, so of course we can put in a confirmation dialogue, but I will leave that as an exercise.
One thing I do want to show though, since we have barely scratched the surface of what's possible with superforms,
are timers for easily displaying loading spinners.
You can read more in detail on the website, but there is a timer called "delayed",
which we can use to test whether the form has been loading, and then it will be true after a certain time.
so it will be a perfect way to display a loading spinner. So I will just import an
SVG and if the delay timer is true we'll display it next to the submit button.
We also need to add a delay on the server to see it because it takes 500
milliseconds before the timer is set to true. We'll do that with a simple promise
await. Hopefully it will work.
Not bad for one line of code, eh?
Well, time is running out. There are so many other features we haven't touched
on here. Client signed validation, nested form data, multiple forms on the same
page, single page applications. I'd hope this gave you a decent introduction to SuperForms at least.
So check out the website for more information and I'm available on
Discord if you need help or have any questions. Thank you for watching!