diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000000..0ce3b5325c --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "wasp", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/web/blog/2024-11-20-building-react-forms-with-ease-using-react-hook-form-and-zod.md b/web/blog/2024-11-20-building-react-forms-with-ease-using-react-hook-form-and-zod.md new file mode 100644 index 0000000000..8ce3810579 --- /dev/null +++ b/web/blog/2024-11-20-building-react-forms-with-ease-using-react-hook-form-and-zod.md @@ -0,0 +1,182 @@ +--- +title: 'Building React Forms with Ease Using React Hook Form, Zod and Shadcn' +authors: [martinovicdev] +image: /img/forms/banner.webp +tags: [webdev, wasp, react, forms] +--- + +import Link from '@docusaurus/Link'; +import useBaseUrl from '@docusaurus/useBaseUrl'; + +import InBlogCta from './components/InBlogCta'; +import WaspIntro from './_wasp-intro.md'; +import ImgWithCaption from './components/ImgWithCaption' + +Forms are something every developer encounters, whether as a user or on the developer side. They’re essential on most websites, but their complexity can vary wildly—from simple 3-field contact forms to giga-monster-t-rex, multi-page forms with 150 fields, dynamic validation, and asynchronous checks. + +In this post, we’ll explore how React Hook Form, Zod, and Shadcn can be used to create an adaptable, developer-friendly solution that handles a wide range of form requirements with ease. + +![david and victoria meme](/img/forms/meme.webp) + +## The form we’ll be building + +Here’s the form we’ll be developing in this post. I plan on writing another post about an advanced use of forms that will have even more complexity as a follow-up, so stay tuned 😃 + +![example form](/img/forms/form.png) + +## Meet the tools + +Let’s look at the stack we’ll use to build and manage our forms. + +### **React and Wasp** + +- Framework: [**Wasp**](https://github.com/wasp-lang/wasp) (full-stack framework for React, Node.js, and Prisma). +- Enables fast, efficient full-stack web development and deployment with React. + +### **React Hook Form** + +- Lightweight library for crafting forms in React, mainly via its `useForm` hook. +- Handles form validation, error management, and offers flexible validation method and integration with various UI component libraries. + +### **Zod** + +- TypeScript-first validation library for creating detailed, reusable validation schemas. +- Integrates with TypeScript types to keep validation unified and avoid duplication. + +### **Shadcn/UI** + +- Collection of reusable UI components which are embedded directly in project, which allows developers to take only what they need and customize those components as well. +- Offers built-in support for React Hook Form and Zod. + +Here’s an example snippet showcasing a form field in Shadcn library: + +```tsx + ( + + Name + + + + + + )} +/> +``` + +Even if you prefer using a different flavor of the stack, as long as you stick with React and RHF, this is still a valid example that will get you going. + +## Let’s build a simple user dashboard + +The application we'll use to demonstrate basic forms is an admin panel with essential CRUD operations. It will include email and password authentication and consist of two pages: a main screen displaying a table of all users, and a user creation page, which will be the star of this article. + +![example data](/img/forms/data.png) + +![example form](/img/forms/form.png) + +Our form will include validation to ensure users cannot submit it (i.e., create a new user) without meeting the specified requirements. The User object is an excellent candidate for validation examples, as it contains a variety of data types suitable for different validations: strings, dates (e.g., date of birth), email strings, and booleans (e.g., premium user status). The complete Prisma schema file is shown below. + +```sql +model Customer { + id Int @id @default(autoincrement()) + name String + surname String + email String + dateOfBirth DateTime + premiumUser Boolean +} +``` + +To jumpstart our project, we’ll use a predefined [Wasp template](https://wasp-lang.dev/docs/project/starter-templates) with TypeScript, called **todo-ts**. This template comes with ready-made components and routing for authentication, including login and signup screens. It also offers a solid example of how CRUD operations work in Wasp, ideal if you’re new to the framework. Additionally, we’ll leverage the new Wasp TypeScript SDK to manage our configuration, as it provides extended flexibility for customization. + +### Finding this article useful? + +[Wasp](https://wasp.sh/) team is working hard to create content like this, not to mention building a modern, open-source React/NodeJS framework. + +The easiest way to show your support is just to star Wasp repo! 🐝 But it would be greatly appreciated if you could take a look at the [repository](https://github.com/wasp-lang/wasp) (for contributions, or to simply test the product). Click on the button below to give Wasp a star and show your support! + +![https://dev-to-uploads.s3.amazonaws.com/uploads/articles/axqiv01tl1pha9ougp21.gif](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/axqiv01tl1pha9ougp21.gif) + +
+ + ⭐️ Thank You For Your Support 💪 + +
+ +## Putting it all together - Zod schema + React Hook Form instance + layout + +To work with forms, we’ll start by defining a Zod validation schema. Our form has three data types: strings, a date, and a boolean. We’ll apply validation to most fields: `name` and `surname` are required, while `email` utilises the built-in e-mail validation. Zod simplifies validating common string types with built-in validations for different types, like emails, URLs, and UUIDs, which is helpful for our email field. + +For additional validations, the date can’t be set to a future date, and the `premiumUser` field simply needs to be a boolean. Zod also provides default validation error messages, but these can be customized. For example, instead of `name: z.string().min(1)`, we could specify `name: z.string().min(1, 'Name is required')`. + + + +```tsx + const formSchema = z.object({ + name: z.string().min(1, { message: 'Name is required' }), + surname: z.string().min(1, { message: 'Surname is required' }), + email: z.string().email({ message: 'Invalid email address' }), + dateOfBirth: z + .date() + .max(new Date(), { message: 'Date cannot be in the future' }), + premiumUser: z.boolean(), + }); +``` + +Our form is managed by the `useForm` hook from [React Hook Form](https://react-hook-form.com/docs/useform), which provides extensive options for handling and validating form values, checking errors, and managing form state. To integrate our Zod validation schema, we’ll use a Zod resolver, allowing React Hook Form to apply the validations we defined earlier. + +The form’s `defaultValues` are derived from the customer object. Since this component is used for both adding new customers and editing existing ones, we’ll pass the necessary data as input. For a new customer, some sensible default values are used; for existing customers, data is retrieved from the database. Apart from setting default values and determining whether to call `createCustomer` or `updateCustomer`, all other aspects of form handling remain the same. + +```tsx +type FormData = z.infer +const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: customer, +}); +``` + +The final step is to create the form itself and assemble it in the TSX file. As shown earlier, this process is straightforward. Whether we’re using text inputs, date pickers, or checkboxes with Shadcn controls, we follow a similar structure: + +- Start by creating the `FormField` element and setting its `control`, `name`, and `render` properties. +- The `render` property is key, as it contains the form element itself. +- Typically, we wrap everything in `FormItem`, add a `FormLabel` for the label, and place the controlled form element inside `FormControl` with the appropriate value and setter method. +- Finally, we include `FormMessage` below, which displays the Zod validation message if validation fails. + +![form with errors](/img/forms/form-error.png) + +```tsx + +// Defining form schema +const formSchema = z.object({ + dateOfBirth: z.date().max(new Date(), { + message: 'Date of birth cannot be today, or in the future', + }), +}); + +// Defining form +const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: defaultValues, +}); + + // Creating form control + ( + + Date of birth + + + + + + )} +/> +``` + +If you’re curious to see the complete application, check out the GitHub repository here: [GitHub Repo](https://github.com/martinovicdev/wasp-form-tutorial). I hope this article has made working with forms easier, and if you're interested in more form-related content, stay tuned for part two! In the next part, we'll dive into advanced patterns and validation techniques to enhance your applications. + +Please consider starring [**Wasp**](https://github.com/wasp-lang/wasp) on GitHub if you liked this post! Your support helps us continue making web development easier and smoother for everyone. 🐝 \ No newline at end of file diff --git a/web/blog/2024-12-11-armadajs-2024-a-conference-that-feels-like-home.md b/web/blog/2024-12-11-armadajs-2024-a-conference-that-feels-like-home.md new file mode 100644 index 0000000000..3a0904fda6 --- /dev/null +++ b/web/blog/2024-12-11-armadajs-2024-a-conference-that-feels-like-home.md @@ -0,0 +1,58 @@ +--- +title: 'ArmadaJS 2024: A Conference That Feels Like Home' +authors: [milica] +image: /img/armadajs-2024/4.webp +tags: [conference, community, wasp, webdev] +--- + +# ArmadaJS 2024: A Conference That Feels Like Home + +It took us a few days to wrap our thoughts after attending [ArmadaJS](https://armada-js.com/), a small but exciting conference in Novi Sad, Serbia. People usually believe that bigger conferences are better, but the fact that this one was on a smaller scale meant that it was easer for us to closely connect and talk with other attendees. + +In short, we had an amazing time, and in this post, we’ll share all the highlights and fun moments from the event! + +![conference booth](/img/armadajs-2024/1.webp) + +## Our general idea - present the "secret" behind Wasp's success + +Wasp’s co-founders are usually the ones who get to give a speech about our open-source and full stack philosophy, but this time our founding engineer [Mihovil](https://github.com/infomiho) had the honor of being the speaker. We wanted to test the audience’s response to his speech and to have a fun booth where people could have a blast by playing our custom made arcade and a game - a Super Mario-style performer where you have to run away from a scary monster. + +![playing the arcade](/img/armadajs-2024/2.webp) + +## The star of the show - Miho! + +[Miho](https://x.com/infomiho) is an experienced senior full-stack engineer with a passion for web, scuba diving, and learning languages. + +We’d like to take this opportunity to give a shoutout to the conference photographer, who took some amazing shots of our team and also fought hard to win a plushie himself. Better luck next time! 😄 + +![Mihovil](/img/armadajs-2024/3.webp) + +The name of Miho’s talk was “Crafting a full-stack framework's DX worth 15,000 GitHub stars”. Miho explained the design principles behind Wasp, which allowed us to create a full-stack framework with a great experience out-of-the-box, but still isn’t limiting and gives developers the freedom of doing this their own way when they want or need so. + +In other words, if you want simplicity, with Wasp you get it out of the box, but if you want to take over the reigns, you can also do that by going one level deeper. The feature he described in depth was our [auth system](https://wasp-lang.dev/docs/auth/overview). + +![Miho at the stage](/img/armadajs-2024/4.webp) + +We won’t spill all the beans in this post, since we hope Miho to attend more conferences with his talk. If you want us to join your JS conference, let us know at [info@wasp-lang.com](mailto:info@wasp-lang.com) 🤓 + +We also had a chance to connect with others in the open source space who keep pushing web forward, and got some nice feedback and questions about Wasp as a framework and our plans for the future. + +## The arcade + +We built this one from ground up ourselves, or to be more precise, [Matija](https://x.com/MatijaSosic) did. His best man created the hardware and Matija developed the original version of the game for his wedding. The idea was to get his best man to run away from the band members. The band members were expecting tips from Matija’s best man, and in order to escape, he had to pick up coffee boosters as he was running away. There were obstacles in the shape of brandy, so you know, the more brandy you have, the slower you are, and the band members take all your money in the end. + +The arcade game is open source and written in Lua, so if you feel like giving it a shot, you can [find it here](https://github.com/matijaSos/wedding-arcade). We took this game and changed the visuals, tweaked it a bit more, and now our Waspy is the star of the game, running away from a …. very unique monster. I won’t spoil all the fun in this post. 😎 + +The ones with the best scores got Waspy plushies, and a ban from playing the game again. That means the next day, a new group of people get to have more fun! + +Winners from day 1 were successful in their mission to get a few versions of Waspy back home, to Croatia, while another one ended up in Serbia. + +![day 1 winners](/img/armadajs-2024/5.webp) + +On day 2, we had 4 winners because the 3rd place players had the exact same score. (We decided not to take the consideration the fact that the difference was 0.5 between them, technically speaking one was a nanometer better than the other one.) We’re nice people, what can I say 😃 + +![day 2 winners](/img/armadajs-2024/6.webp) + +## Thank you + +We are really thankful to the whole ArmadaJS team for their amazing support and all the effort they put into this conference. We also want to thank all the attendees, especially the ones who were constantly keeping us entertained and the booth occupied. We hope you enjoyed our company as much as we enjoyed yours! \ No newline at end of file diff --git a/web/blog/2024-12-24-meet-miho-founding-engineer-wasp.md b/web/blog/2024-12-24-meet-miho-founding-engineer-wasp.md new file mode 100644 index 0000000000..d8d2dbe8fe --- /dev/null +++ b/web/blog/2024-12-24-meet-miho-founding-engineer-wasp.md @@ -0,0 +1,67 @@ +--- +title: 'Meet the team - Mihovil Ilakovac, Founding Engineer' +authors: [milica] +image: /img/miho-interview/miho-cover.png +tags: [meet-the-team, wasp] +--- + +*Wasp team is on a mission to build the world's best full-stack web framework for JS. End-to-end solution and DX of Rails/Laravel, but with JS and your favorite tools. Future-proof and here to stay.* + +It’s been a while since we caught up with the people behind Wasp—the ones turning ideas into code and shaping the future of web development. As [we are actively growing the team](https://wasp-lang.notion.site/Wasp-Careers-59fd1682c80d446f92be5fa65cc17672), we decided to catch up with people who already joined us. In this post, we've interviewed Miho who joined Wasp in 2023. + +### Let’s start with two truths and a lie about yourself. + +1. I recently became a one-star scuba diver. +2. I played the guitar in music school. +3. In one of my previous jobs, I designed the face of Croatia’s COVID chatbot. + +![image.png](/img/miho-interview/image.png) + +### Which statement was the lie above? Any interesting stories to share? + +I mean, it’s nothing too dramatic, but I actually played the piano. I liked playing it, but I wasn’t really that good at it. I liked creating electronic music using Fruity Loops Studio more in those days, [here’s a really old song I made](https://www.youtube.com/watch?v=BqEBbPNDc50). + +Also, I really became a one-star scuba diver recently! + +### Why did you join Wasp? What did you do before? + +In my previous company, Superbet, I worked on the front end in Vue.js in the sports betting team. I started as an individual contributor, became a tech lead, and eventually started a team that worked on cross-platform tooling. That’s one of the things that prompted me to contribute to open source tools like [Directus](https://github.com/directus/directus), and I really enjoyed doing that. + +I realized that I like working on dev tooling and helping other devs to do their job more easily. I liked the open-source community vibe and thought “It would be awesome to be a part of it!”. Being in web development for over 10 years and knowing the complexity behind it made Wasp stand out to me. Working on open source dev tooling that made web dev 10x easier? Nice! This made [Wasp](https://wasp-lang.dev/) a perfect fit for me. Luckily, it turned out that guys at Wasp liked me back 🙂 + +### What is your favorite language? + +These days I’m all about Typescript and Haskell since at Wasp we are trying to create the full-stack framework for JavaScript and our compiler is in Haskell. I’d say I like languages with types because you can model your problem with them and then the compiler will give you errors if you make mistakes. Also, I like immutability in programming languages because again, the compiler helps you to avoid errors. So, I’m all about languages that dig a pit of success for you. + +### What are you most excited about in Wasp? + +I find the concept of full-stack modules quite exciting. It’s like having legos of functionality you plug into your app. Something like Stripe payments support. You’d install something like `wasp/stripe` and get the client UI, server logic and database entities working together. That kind of ability to solve your business problem with a simple module install feels really powerful to me. I’d like to have this as a developer, but I didn’t see that kind of solution in the JS ecosystem. + +### What’s a feature or project you’re most proud of that you worked on in the past three months? + +I like the work that we did on moving away from Wasp’s custom entity syntax to using the Prisma file directly. We used Prisma under the hood the whole time, but users had to use Wasp's special syntax. Now that’s no longer the case and people can just use Prisma as they are used to. I like the concept of enabling devs to use the skills they already have. I worked on the parser for the Prisma file format and it was a fun technical challenge! It enables Wasp to understand your Prisma models and configuration which allows it to generate logic based on your database setup. + +### How did you start coding? + +When I was 11 years old, I started programming in a visual programming language called Logo. You write down commands and a small turtle draws things on the screen. I used to go to competitions in Logo. So you could say I was a competitive turtle user. By the time I was 14, blogs were a thing and I wanted to change some colors of my blog homepage, so I had to learn a bit of CSS. Or should I say, learn how to copy-paste some code and tweak it until it worked. I was always that kind of learner, playing around, messing things up, and then finally figuring it out. + +![Miho at a conference](/img/miho-interview/miho.jpg "Miho at a conference") +*Miho recently gave a talk at [ArmadaJS](https://wasp-lang.dev/blog/2024/12/11/armadajs-2024-a-conference-that-feels-like-home).* + +### Your dev setup? + +I’m used to doing everything on my MacBook Pro M2 and oftentimes without an external screen because I like to travel and work. I’m using VS Code, Warp terminal emulator, and Raycast shortcuts for window management. That’s pretty much it, my setup is as basic as it gets. Before when I was on Linux, I used to play around more, but since I’m on MacOS, I just don’t have the need - maybe it’s the OS, maybe it’s me getting older 😀 + +### What is your current favorite gem, library, tool, or anything else that helps you with your work? Why? + +I love using Zod for env variables validation, it makes me feel nice and cozy knowing they are validated and not just some `string | undefined` Wild West. + +Another thing I love using these days is [bolt.new](http://bolt.new) which gives you superpowers to build a demo app for some idea you have and then you can start playing around with functioning code. It unlocks you as the dev from setting up the boring bits before you can start playing around. + +### One piece of advice you’d give to budding developers? + +Build stuff. But own the whole process, the product, the design, the deployment, and of course the development. I say it in that way because I feel like a lot of devs never get the chance to experience all the other things around their job - but there are so many valuable lessons to learn. You’ll understand your counterparts in other departments, you’ll learn their language, you’ll understand their world and that will make you a 10x developer. Build stuff, and who knows where it might take you. + +### Lastly, where can people find or connect with you online? + +Here’s my pretty slow paced [blog](https://ilakovac.com/) and my more up to date [Twitter](https://x.com/infomiho). \ No newline at end of file diff --git a/web/blog/2025-01-09-wasp-launch-week-8.md b/web/blog/2025-01-09-wasp-launch-week-8.md new file mode 100644 index 0000000000..ded0588678 --- /dev/null +++ b/web/blog/2025-01-09-wasp-launch-week-8.md @@ -0,0 +1,133 @@ +--- +title: "Wasp Launch Week #8 - it's a Fixer Upper 🛠️🏡" +authors: [matijasos] +image: /img/lw8/lw8-banner.png +tags: [launch-week, update] +--- + +import Link from '@docusaurus/Link'; +import useBaseUrl from '@docusaurus/useBaseUrl'; + +import InBlogCta from './components/InBlogCta'; +import WaspIntro from './_wasp-intro.md'; +import ImgWithCaption from './components/ImgWithCaption' + + + + +Hola Waspeteers, + +we're wishing you a happy New Year and hope you had nice and relaxing holidays (and managed to do some coding in between all the food, of course)! In case you haven't heard yet, this year is the year of Wasp 🐝! So we'll immediately kick it off with a bunch of cool new features we've been cooking for the last few months: + + + +As the olden tradition goeth, started by Wasp druids almost one thousand days ago (that's how you make two years sound longer), we'll do a big reveal during our community call that will take place next Monday - January 13th, 10.30 AM EST / 4.30 PM CET! To reserve your spot, [visit the event in our Discord server](https://discord.gg/PQVY9FvG?event=1326930588191559690) and mark yourself as interested. + + + +Be there or be square! Do it now, so we know how many virtual pizzas and drinks we have to order. + +## Why a Fixer Upper? + +In the last seven Launch Weeks we've introduced a lot of new features and concepts - some are things you'd expect a mature web framework to have, and others are things you haven't [ever seen before](/blog/2024/04/25/first-framework-that-lets-you-visualize-react-node-app-code). This has proven to be a winning combination in pushing Wasp forward - it's both about reaching 1.0, but also showing why Wasp's compiler-driven approach is so useful and powerful. + +Having been doing this for a while, almost two years since our first launch week, Wasp has grown a lot and come to that point where you are building and deploying (some even selling) their applications. We've realized we've come to the stage where things work, and they actually work pretty well. + + + +Having also received a ton of feedback from you on different topics, we decided to dedicate a full quarter of building exclusively on that! It's not only about building features, it's also about all the other "little" things which make for an amazing developer experience we're aiming for - documentation, examples, guides & tutorials, integrations with other tools. We went full in on it, and hopefully it will make your life a little bit easier next time you decide to use Wasp to build your app. + +Now, let's take a look together at all the nice things which this latest release of Wasp brings us: + +## #1: Deployment Games 🦑: Docs revamp + easily self-host with Coolify, CapRover, and more! + + + +We all know what choosing a hosting and deployment method can feel like these days - a hundred and one way to go about it, but make a wrong choice and you're in for a trouble. + +This is why more and more developers are looking to learn more about deployment options and use a stack that's flexible enough to be deployed pretty much anywhere. + +It's also how we think about it at Wasp - we want to make it as easy as possible for you to use a service you know and like (e.g. Fly, Railway, or Supabase), but also go the self-hosting route if that's what you prefer (e.g. Coolify or CapRover). + + + +For this launch week, we did a complete revamp of our Deployment docs section - it's much more detailed and contains answers to the common questions we've been getting from the community. Additionaly, we've included step-by-step guides on how to self host your Wasp app with Coolify and CapRover. + +Bon Deploittit! (sorry about this) + +## #2: Validation bonanza 🏴‍☠️: Validate your env vars with Zod! + + + +Environment variables are probably one of the most common causes of making you pull out hair when trying to finally put your app in production. It's one of the facts of life (right up there with paying taxes), so not trying to say we're gonna fix it, but we can make it at least a bit easier (more time for taxes). + +From now, **you can use Zod to validate your env variables, both those that are required by Wasp, and your custom ones**! Determine type, error message and everything else that comes to your mind, so you actually feel (almost) happy when you realize there is a missing env var in your app. + +## #3: Community day: Starter templates, apps made (and sold) with Wasp, and more! + + + +Thanks to you, our dear Waspeteer and community member, we have a lot to write about here, so we decided to dedicate a whole day to you! There have been so many cool apps, milestones and contributions to Wasp that we simply have to show it all off. + +Get ready for the day of showcasing apps made with Wasp, success stories, integrations with AI development tools, community-made starter templates and more! + +## #4: TypeScript all the way: Wasp now respects your own ts.config file! + + + +Wasp is doing a lot of things for you - managing your front-end, back-end, database, deployment and pretty much everything in between. It means it is aware of a lot of things in your codebase, but sometimes it can get a bit too much and make you feel like you're out of control. + +To reduce the sentient-AI vibes ("I'm sorry Dave, I'm afraid I can't do that" - anyone?) we started with making sure that Wasp takes into account whatever you defined in your tsconfig.json and follows it (take that, AI). That means you have an easier way defining your import aliases (e.g. required by Shadcn), better error messages with proper file paths, .... + +All in all, a bit of respect goes a long way! + +## #5: OpenSaaS day: cursorrules, YouTube walkthrough video and Vinny getting back to what he does best 🏗️ + + + +And finally, we'll close it with your favorite SaaS starter - OpenSaaS! Vinny and the team have been casting their magic with no rest, and along with the improved template, also created a full-fledged video tutorial on how to start your SaaS with it! Follow it for step-by-step instructions and enjoy Vinny's smooth espresso-coated voice. + +## Stay in the loop + + + +Every day, we'll update this page with the latest announcement of the day - to stay in the loop, [follow us on Twitter/X](https://twitter.com/WaspLang) and [join our Discord](https://discord.gg/rzdnErX) - see you there! diff --git a/web/blog/2025-01-22-advanced-react-hook-form-zod-shadcn.md b/web/blog/2025-01-22-advanced-react-hook-form-zod-shadcn.md new file mode 100644 index 0000000000..b756e014f5 --- /dev/null +++ b/web/blog/2025-01-22-advanced-react-hook-form-zod-shadcn.md @@ -0,0 +1,285 @@ +--- +title: "Building Advanced React Forms Using React Hook Form, Zod and Shadcn" +authors: [martinovicdev] +image: /img/forms/advanced-react.jpeg +tags: [webdev, wasp, react, forms] +--- + +import Link from '@docusaurus/Link'; +import useBaseUrl from '@docusaurus/useBaseUrl'; + +import InBlogCta from './components/InBlogCta'; +import WaspIntro from './_wasp-intro.md'; + +Developers usually say, "Once you've seen one form, you've seen them all," but that's not always true. Forms can range from basic to highly complex, depending on your project's needs. + +If you're working on a simple contact form (e.g., email, subject, message), [check out part one](https://wasp-lang.dev/blog/2024/11/20/building-react-forms-with-ease-using-react-hook-form-and-zod). It covers the basics of React forms using React Hook Form, Shadcn UI, and Zod. + +But what if your forms need advanced behaviors and validations? That's where this guide comes in. Whether you need conditional fields, custom validation logic, or dynamic form generation, this article will help you take your React forms to the next level. + +![image](https://media4.giphy.com/media/xXXhLy1M4RML6/giphy.gif?cid=7941fdc6u3noalhw3o2rc6z04i9g7a16dk21oh3wkpbiqxdk&ep=v1_gifs_search&rid=giphy.gif&ct=g) + +## What else is there to validate? + +💡 Starting Code: The code we'll use here builds on the final version from part one. You can [find it here on GitHub](https://github.com/martinovicdev/wasp-form-tutorial). + +In [part one](https://wasp-lang.dev/blog/2024/11/20/building-react-forms-with-ease-using-react-hook-form-and-zod), we explored the basics of form validation, including: + +- **Type validation:** Ensuring input types match form fields. +- **Length validation:** Checking input length. +- **Maximum value validation:** Setting value limits. + +Now, let's tackle **advanced validation scenarios**. These include: + +- Custom validation logic. +- Validation tied to other fields (e.g., comparing values). +- Validation based on external data, like database values. + +To demonstrate these, we'll expand the form with new fields: **username, address, postal code, city, and country**. We'll start by applying basic validations to these fields. + +Here's your initial Zod schema: + +```tsx +const formSchema = z + .object({ + name: z.string().min(1, { message: 'Name is required' }), + surname: z.string().min(1, { message: 'Surname is required' }), + email: z + .string() + .email({ message: 'Invalid email address' }), + dateOfBirth: z.date().max(new Date(), { + message: 'Date of birth cannot be in the future', + }), + premiumUser: z.boolean(), + username: z + .string() + .min(1, { message: 'Username is required' }), + address: z.string().min(1, { message: 'Address is required' }), + postalCode: z.string().min(1, { message: 'Postal code is required' }), + city: z.string().min(1, { message: 'City is required' }), + country: z.string().min(1, { message: 'Country is required' }), + }); +``` + +Expanding the form is straightforward since we're working with standard input fields. However, remember to update your **Prisma schema**, queries, and actions to include the new values. + +The final form should look similar to this: + +![image.png](/img/forms/form-2.png) + +The most important part of the equation will be Zod's `refine` and `superRefine` refinement functions. We use them to implement custom validation logic, which cannot be represented with simple types. + +### Refine + +**Refine** is a straightforward method for custom validation. It takes two arguments: + +1. A **validation function**. +2. Optional **additional options** (e.g., custom error messages). + +If the validation function returns `true`, the validation passes; otherwise, it fails. + +Refine also supports **asynchronous functions**, which will play an important role in this article. + +### SuperRefine + +A more advanced alternative to `refine` is `superRefine`, which provides greater flexibility for customizing validations. + +The key difference with `superRefine` is that it allows you to add multiple validation issues by manually throwing errors using `ctx.addIssue`. If you don't explicitly add an issue, the error won't be triggered. + +Another significant distinction is that `superRefine` operates on the **entire schema** rather than a single field, making it ideal for scenarios where validation depends on relationships between multiple fields. + +![doggo](https://media1.giphy.com/media/1PstC8Jk5enGx3Gufw/giphy.gif?cid=7941fdc6xukyg2lvj7uap5dtac4x9w2bg0nvlbe1oyylfg28&ep=v1_gifs_search&rid=giphy.gif&ct=g) + +### Finding this article useful? + +[Wasp](https://wasp.sh/) team is working hard to create content like this, not to mention building a modern, open-source React/NodeJS framework. + +The easiest way to show your support is just to star Wasp repo! 🐝 Click on the button below to give Wasp a star and show your support! + +![https://dev-to-uploads.s3.amazonaws.com/uploads/articles/axqiv01tl1pha9ougp21.gif](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/axqiv01tl1pha9ougp21.gif) + + + +## Custom validation + +A great starting point for **advanced form validation** is implementing **custom validation**. This comes into play when requirements fall outside the “usual” set of rules. + +For example, let's validate a **username** to ensure it **doesn't contain spaces**. Spaces in usernames can cause UI issues or user confusion, so we'll disallow them. + +While `refine` is a common choice for custom validations, we'll use `superRefine` instead. This allows us to perform **multiple checks** simultaneously, making it ideal for more complex validation scenarios. With this in mind, we'll add a **`superRefine`** function at the end of our validation schema to include two checks: one for the **username** and another for the **city**. + +Believe it or not, some cities in the world have numbers in their names (as noted in this [Wikipedia article](https://en.wikipedia.org/wiki/List_of_places_with_numeric_names)). While the city field should initially validate as a string, we also want to ensure it doesn't consist exclusively of numbers. If it does, we'll throw an error. + +To throw the error, we'll use `ctx.addIssue`, specifying the **error code**, **message**, and **path**. + +Similarly, we'll validate the username to ensure it doesn't contain any space characters, applying the same structured error handling as for the city. + +```tsx +//this comes after our validation schema +.superRefine((data, ctx) => { + if (data.username.includes(' ')) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Username cannot contain spaces', + path: ['username'], + }); + } + + if (/^\d+$/.test(data.city)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'City name cannot be purely numeric', + path: ['city'], + }); + } + }); +``` + +## Conditional validation + +Conditional validation is useful when the validity of one field depends on the value or validity of another. It's a common and practical type of advanced validation. + +For this example, we'll implement a simple condition: if the customer's country is the United Kingdom, their postal code must follow the UK-specific format. + +To achieve this, we'll first check if the country field is set to "UK." If it is, we'll validate the postal code against a regex pattern. Using `superRefine` at the end of the validation schema is particularly convenient here, as it provides easy access to the values of the entire form, allowing us to implement this conditional logic seamlessly. + +```tsx +//this comes after our validation schema +.superRefine((data, ctx) => { + if (data.username.includes(' ')) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Username cannot contain spaces', + path: ['username'], + }); + } + + if (/^\d+$/.test(data.city)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'City name cannot be purely numeric', + path: ['city'], + }); + } + + + if (!isValidUKPostcode(data.postalCode, data.country)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Invalid UK postal code format (e.g., SW1A 1AA)', + path: ['postalCode'], + }); + } + }); +``` + +## Asynchronous validation + +Async validation might sound complex, but with React Hook Form, it's surprisingly straightforward and builds on what we've already covered. The only additional requirement is a function to check whether the username or email already exists. + +For this example, we'll focus on validating the username, but the same approach can be applied to email or any other field. I've created two functions that return a `Promise` to make their usage within the Zod validation schema as simple as possible. + +As shown in the code examples, the schema uses these functions to return `true` or `false` based on whether a customer with the given username or email already exists. + +```tsx + const checkUsername = async (value: string): Promise => { + return getCustomersWithUsername({ username: value }).then((data) => { + return !!data; + }); + }; +``` + +In the Zod schema, we'll use the `refine` function to validate the field. However, we won't always perform a database check. + +For example: + +- If the username hasn't changed (i.e., it's the same as the current customer's username) or is still empty, we'll skip the database query. +- Otherwise, we'll check the database for an existing customer with the same username. If a match is found, it will trigger a validation error. + +This approach ensures efficient validation by minimizing unnecessary database queries. + +```tsx +username: z + .string() + .min(1, { message: 'Username is required' }) + .refine( + async (username) => { + if (username === customer.username && customer.username !== '') + return true; + return !(await checkUsername(username)); + }, + { message: 'Username already exists' } + ), +``` + +Finally, the entire Zod schema should look like this: + +```tsx + const formSchema = z + .object({ + name: z.string().min(1, { message: 'Name is required' }), + surname: z.string().min(1, { message: 'Surname is required' }), + email: z + .string() + .email({ message: 'Invalid email address' }) + .refine( + async (email) => { + if (email === customer.email && customer.email !== '') return true; + return !(await checkEmail(email)); + }, + { message: 'Email already exists' } + ), + dateOfBirth: z.date().max(new Date(), { + message: 'Date of birth cannot be in the future', + }), + premiumUser: z.boolean(), + username: z + .string() + .min(1, { message: 'Username is required' }) + .refine( + async (username) => { + if (username === customer.username && customer.username !== '') + return true; + return !(await checkUsername(username)); + }, + { message: 'Username already exists' } + ), + address: z.string().min(1, { message: 'Address is required' }), + postalCode: z.string().min(1, { message: 'Postal code is required' }), + city: z.string().min(1, { message: 'City is required' }), + country: z.string().min(1, { message: 'Country is required' }), + }) + .superRefine((data, ctx) => { + if (!isValidUKPostcode(data.postalCode, data.country)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Invalid UK postal code format (e.g., SW1A 1AA)', + path: ['postalCode'], + }); + } + + if (data.username.includes(' ')) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Username cannot contain spaces', + path: ['username'], + }); + } + + if (/^\d+$/.test(data.city)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'City name cannot be purely numeric', + path: ['city'], + }); + } + }); +``` + +I hope this article has made working with forms easier and more understandable! If you want to see to see the complete source code of the application, check out the [GitHub repo here](https://github.com/martinovicdev/wasp-advanced-form-tutorial). + +Please consider starring [**Wasp**](https://github.com/wasp-lang/wasp) on GitHub if you liked this post! Your support helps us continue making web development easier and smoother for everyone. 🐝 \ No newline at end of file diff --git a/web/docs/advanced/middleware-config.md b/web/docs/advanced/middleware-config.md index 1afe755823..8be240ba70 100644 --- a/web/docs/advanced/middleware-config.md +++ b/web/docs/advanced/middleware-config.md @@ -88,7 +88,6 @@ app todoApp { // ... server: { - setupFn: import setup from "@src/serverSetup", middlewareConfigFn: import { serverMiddlewareFn } from "@src/serverSetup" }, } @@ -113,7 +112,6 @@ app todoApp { // ... server: { - setupFn: import setup from "@src/serverSetup", middlewareConfigFn: import { serverMiddlewareFn } from "@src/serverSetup" }, } diff --git a/web/src/components/Footer.js b/web/src/components/Footer.js index fd9358624d..2e81530829 100644 --- a/web/src/components/Footer.js +++ b/web/src/components/Footer.js @@ -43,7 +43,7 @@ const company = [ }, { text: 'Careers', - url: 'https://www.notion.so/wasp-lang/Founding-Engineer-at-Wasp-88a73838f7f04ab3aee1f8e1c1bee6dd', + url: 'https://wasp-lang.notion.site/Wasp-Careers-59fd1682c80d446f92be5fa65cc17672', }, { text: 'Company', diff --git a/web/static/img/armadajs-2024/1.webp b/web/static/img/armadajs-2024/1.webp new file mode 100644 index 0000000000..5f09cb2a48 Binary files /dev/null and b/web/static/img/armadajs-2024/1.webp differ diff --git a/web/static/img/armadajs-2024/2.webp b/web/static/img/armadajs-2024/2.webp new file mode 100644 index 0000000000..15f9609948 Binary files /dev/null and b/web/static/img/armadajs-2024/2.webp differ diff --git a/web/static/img/armadajs-2024/3.webp b/web/static/img/armadajs-2024/3.webp new file mode 100644 index 0000000000..3f03946b7c Binary files /dev/null and b/web/static/img/armadajs-2024/3.webp differ diff --git a/web/static/img/armadajs-2024/4.webp b/web/static/img/armadajs-2024/4.webp new file mode 100644 index 0000000000..0b51690ed3 Binary files /dev/null and b/web/static/img/armadajs-2024/4.webp differ diff --git a/web/static/img/armadajs-2024/5.webp b/web/static/img/armadajs-2024/5.webp new file mode 100644 index 0000000000..2c2b9d0818 Binary files /dev/null and b/web/static/img/armadajs-2024/5.webp differ diff --git a/web/static/img/armadajs-2024/6.webp b/web/static/img/armadajs-2024/6.webp new file mode 100644 index 0000000000..ca12a348c1 Binary files /dev/null and b/web/static/img/armadajs-2024/6.webp differ diff --git a/web/static/img/forms/advanced-react.jpeg b/web/static/img/forms/advanced-react.jpeg new file mode 100644 index 0000000000..97fdd5f214 Binary files /dev/null and b/web/static/img/forms/advanced-react.jpeg differ diff --git a/web/static/img/forms/banner.webp b/web/static/img/forms/banner.webp new file mode 100644 index 0000000000..9df3037a94 Binary files /dev/null and b/web/static/img/forms/banner.webp differ diff --git a/web/static/img/forms/data.png b/web/static/img/forms/data.png new file mode 100644 index 0000000000..199896526c Binary files /dev/null and b/web/static/img/forms/data.png differ diff --git a/web/static/img/forms/form-2.png b/web/static/img/forms/form-2.png new file mode 100644 index 0000000000..c3bae32a07 Binary files /dev/null and b/web/static/img/forms/form-2.png differ diff --git a/web/static/img/forms/form-error.png b/web/static/img/forms/form-error.png new file mode 100644 index 0000000000..90a3216c38 Binary files /dev/null and b/web/static/img/forms/form-error.png differ diff --git a/web/static/img/forms/form.png b/web/static/img/forms/form.png new file mode 100644 index 0000000000..1ba3f31934 Binary files /dev/null and b/web/static/img/forms/form.png differ diff --git a/web/static/img/forms/meme.webp b/web/static/img/forms/meme.webp new file mode 100644 index 0000000000..ebe4818bbb Binary files /dev/null and b/web/static/img/forms/meme.webp differ diff --git a/web/static/img/lw8/buckle-up.webp b/web/static/img/lw8/buckle-up.webp new file mode 100644 index 0000000000..e1c98982d2 Binary files /dev/null and b/web/static/img/lw8/buckle-up.webp differ diff --git a/web/static/img/lw8/deployment-games.png b/web/static/img/lw8/deployment-games.png new file mode 100644 index 0000000000..377205be71 Binary files /dev/null and b/web/static/img/lw8/deployment-games.png differ diff --git a/web/static/img/lw8/i-love-wasp.png b/web/static/img/lw8/i-love-wasp.png new file mode 100644 index 0000000000..0a47838e97 Binary files /dev/null and b/web/static/img/lw8/i-love-wasp.png differ diff --git a/web/static/img/lw8/join-event-instructions.png b/web/static/img/lw8/join-event-instructions.png new file mode 100644 index 0000000000..ea91a70d1a Binary files /dev/null and b/web/static/img/lw8/join-event-instructions.png differ diff --git a/web/static/img/lw8/lw8-banner.png b/web/static/img/lw8/lw8-banner.png new file mode 100644 index 0000000000..ef9291c87f Binary files /dev/null and b/web/static/img/lw8/lw8-banner.png differ diff --git a/web/static/img/lw8/opensaas-video.png b/web/static/img/lw8/opensaas-video.png new file mode 100644 index 0000000000..15aee8363e Binary files /dev/null and b/web/static/img/lw8/opensaas-video.png differ diff --git a/web/static/img/lw8/tsconfig-wasp.png b/web/static/img/lw8/tsconfig-wasp.png new file mode 100644 index 0000000000..d29e3e2b52 Binary files /dev/null and b/web/static/img/lw8/tsconfig-wasp.png differ diff --git a/web/static/img/lw8/turboreel.webp b/web/static/img/lw8/turboreel.webp new file mode 100644 index 0000000000..7e27710783 Binary files /dev/null and b/web/static/img/lw8/turboreel.webp differ diff --git a/web/static/img/lw8/wasp-app-flow.gif b/web/static/img/lw8/wasp-app-flow.gif new file mode 100644 index 0000000000..4d27433dfd Binary files /dev/null and b/web/static/img/lw8/wasp-app-flow.gif differ diff --git a/web/static/img/lw8/zod-env-validation.png b/web/static/img/lw8/zod-env-validation.png new file mode 100644 index 0000000000..360a0665d1 Binary files /dev/null and b/web/static/img/lw8/zod-env-validation.png differ diff --git a/web/static/img/miho-interview/image.png b/web/static/img/miho-interview/image.png new file mode 100644 index 0000000000..3007fb36ec Binary files /dev/null and b/web/static/img/miho-interview/image.png differ diff --git a/web/static/img/miho-interview/miho-cover.png b/web/static/img/miho-interview/miho-cover.png new file mode 100644 index 0000000000..21c981a9d0 Binary files /dev/null and b/web/static/img/miho-interview/miho-cover.png differ diff --git a/web/static/img/miho-interview/miho.jpg b/web/static/img/miho-interview/miho.jpg new file mode 100644 index 0000000000..22a1e91d71 Binary files /dev/null and b/web/static/img/miho-interview/miho.jpg differ diff --git a/web/static/scripts/posthog.js b/web/static/scripts/posthog.js index d32d6b45a9..72835dc5ed 100644 --- a/web/static/scripts/posthog.js +++ b/web/static/scripts/posthog.js @@ -1,2 +1,8 @@ -!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.async=!0,p.src=s.api_host+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags".split(" "),n=0;n