Typescript/Javascript tutorial toggle (#1266)

This commit is contained in:
Mihovil Ilakovac 2023-06-26 14:11:05 +02:00 committed by GitHub
parent 2db1dfad2a
commit 8587e14be5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 954 additions and 143 deletions

View File

@ -4,8 +4,6 @@ slug: /quick-start
next: /tutorials/todo-app
---
import Tabs from '@theme/Tabs'
import TabItem from '@theme/TabItem'
import useBaseUrl from '@docusaurus/useBaseUrl';
## Installation

View File

@ -2,8 +2,6 @@
title: Features
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import SendingEmailsInDevelopment from '../_sendingEmailsInDevelopment.md'
## App

View File

@ -5,10 +5,10 @@ title: Introduction
import useBaseUrl from '@docusaurus/useBaseUrl';
:::info
If you want to run Wasp locally and haven't yet set it up, check out [Getting Started](/docs) first for installation instructions, and then continue with the tutorial.
If you want to run Wasp locally and haven't yet set it up, check out [Quick Start](/docs/quick-start) first for installation instructions, and then continue with the tutorial.
:::
Let's build together a simple Todo App in Wasp!
Let's build a simple Todo App in Wasp!
<img alt="How Todo App will work once it is done"
src={useBaseUrl('img/todo-app-tutorial-intro.gif')}

View File

@ -4,6 +4,7 @@ title: "Creating a new project"
---
import useBaseUrl from '@docusaurus/useBaseUrl';
import { ShowForTs } from '@site/src/components/TsJsHelpers';
Run the following command in your terminal to create a new Wasp project:
```shell
@ -17,7 +18,7 @@ wasp start
You have just run your app in the development mode!
:::note
`wasp start` might take a little bit longer due to the initial setup.
`wasp start` might take a little bit longer to finish when you run it for the first time due to the initial setup.
:::
You will be seeing a lot of different output from the client, server and database setting themselves up.
@ -63,17 +64,12 @@ experience (i.e., autocompletion, intellisense, etc.), so it's best to leave
them alone (for now).
:::note Typescript Support
We've introduced Typescript support in Beta version 0.7.0, but you are free to
use JavaScript (js/jsx) or TypeScript (ts/tsx) as you see fit, on a file-by-file basis.
Wasp supports Typescript out of the box but you are free to use JavaScript (js/jsx)
or TypeScript (ts/tsx) as you see fit. No extra configuration is needed!
For this tutorial, we will simply use vanilla Javascript and JSX syntax 🍦
We'll provide you with both JavaScript and TypeScript snippets for each feature we cover.
If you'd prefer to follow along using TypeScript:
1. Start by changing `MainPage.jsx` to `MainPage.tsx`
2. For the rest of the tutorial, whenever you want to use TypeScript in a file,
just make sure to use the appropriate extensions.
No extra configuration is needed!
Code blocks have a toggle between vanilla 🍦 Javascript/JSX and Typescript/TSX.
To see how to get the most out of Wasp and TypeScript, take a look at [our
TypeScript doc](/docs/typescript). It contains a list of all TypeScript features
@ -85,6 +81,9 @@ Let's start with the `main.wasp` file, which introduces 3 new concepts:
[page](language/features.md#page) and
[route](language/features.md#route).
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
```wasp title="main.wasp"
app TodoApp { // Main declaration, defines a new web app.
wasp: {
@ -93,7 +92,7 @@ app TodoApp { // Main declaration, defines a new web app.
title: "Todo app" // Used as a browser tab title.
}
route RootRoute { path: "/", to: MainPage } // Render page MainPage on url `/` (default url).
route RootRoute { path: "/", to: MainPage } // Render page MainPage on url `/`
page MainPage {
// We specify that ReactJS implementation of our page can be found in
@ -103,7 +102,40 @@ page MainPage {
}
```
</TabItem>
<TabItem value="ts" label="TypeScript">
```wasp title="main.wasp"
app TodoApp { // Main declaration, defines a new web app.
wasp: {
version: "^0.11.0"
},
title: "Todo app" // Used as a browser tab title.
}
route RootRoute { path: "/", to: MainPage } // Render page MainPage on url `/`
page MainPage {
// We specify that ReactJS implementation of our page can be found in
// `src/client/MainPage.tsx` as a default export (uses standard JS import syntax).
// Use '@client' to reference files inside the src/client folder.
component: import Main from "@client/MainPage.tsx"
}
```
</TabItem>
</Tabs>
<ShowForTs>
:::warning Using TypeScript
If you are using Typescript, you will need to rename `MainPage.jsx` to `MainPage.tsx`.
:::
</ShowForTs>
Let's now take a look at that React component we referenced in the `page MainPage { ... }` declaration in `main.wasp`:
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
```jsx title="src/client/MainPage.jsx"
import waspLogo from './waspLogo.png'
import './Main.css'
@ -113,6 +145,22 @@ const MainPage = () => {
}
export default MainPage
```
</TabItem>
<TabItem value="ts" label="TypeScript">
```tsx title="src/client/MainPage.tsx"
import waspLogo from './waspLogo.png'
import './Main.css'
const MainPage = () => {
...
}
export default MainPage
```
</TabItem>
</Tabs>
As we can see, this is simply a functional React component that uses the CSS and Wasp logo files sitting next to it in the `src/client` dir.
This is all the code we need!
@ -133,6 +181,10 @@ IDE support, so it's important to keep them.
Next, let's make the `MainPage` component much simpler:
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
```jsx title="src/client/MainPage.jsx"
const MainPage = () => {
return <div>Hello world!</div>
@ -141,6 +193,20 @@ const MainPage = () => {
export default MainPage
```
</TabItem>
<TabItem value="ts" label="TypeScript">
```tsx title="src/client/MainPage.tsx"
const MainPage = () => {
return <div>Hello world!</div>
}
export default MainPage
```
</TabItem>
</Tabs>
At this point, you should see something like this:
<img alt="Todo App - Hello World"

View File

@ -4,6 +4,7 @@ title: "Listing tasks"
---
import useBaseUrl from '@docusaurus/useBaseUrl';
import { ShowForTs, ShowForJs } from '@site/src/components/TsJsHelpers';
We want to admire our tasks, so let's list them!
@ -18,13 +19,25 @@ To list tasks, we will need two things:
1. A Wasp query that fetches all the tasks from the database.
2. React logic that calls our query and displays its results.
## Wasp query
## Defining the Query
Let's implement `getTasks` [query](language/features.md#query).
<ShowForJs>
It consists of a declaration in Wasp and implementation in JS (in `src/server/` directory).
</ShowForJs>
<ShowForTs>
It consists of a declaration in Wasp and implementation in TS (in `src/server/` directory).
</ShowForTs>
### Wasp declaration
Add the following code to `main.wasp`:
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
```wasp title="main.wasp"
// ...
@ -38,6 +51,24 @@ query getTasks {
entities: [Task]
}
```
</TabItem>
<TabItem value="ts" label="TypeScript">
```wasp title="main.wasp"
// ...
query getTasks {
// We specify that JS implementation of the query (which is an async JS function)
// can be found in `src/server/queries.js` as the named export `getTasks`.
// Use '@server' to reference files inside the src/server folder.
fn: import { getTasks } from "@server/queries.js",
// We tell Wasp that this query is doing something with entity `Task`. With that, Wasp will
// automatically refresh the results of this query when tasks change.
entities: [Task]
}
```
:::caution
<!-- This block is mostly duplicated in typescript.md -->
Even if you use TypeScript and have the file `queries.ts`, you will still need to import it using the `.js` extension. Wasp internally uses `esnext` module resolution, which always requires specifying the extension as `.js` (i.e., the extension used in the emitted JS file). This applies to all `@server` imports (and files on the server in general). It does not apply to client files.
@ -45,8 +76,26 @@ Even if you use TypeScript and have the file `queries.ts`, you will still need t
Read more about ES modules in TypeScript [here](https://www.typescriptlang.org/docs/handbook/esm-node.html). If you're interested in the discussion and the reasoning behind this, read about it [in this GitHub issue](https://github.com/microsoft/TypeScript/issues/33588).
:::
### JS implementation
Next, create a new file `src/server/queries.js` and define the JS we've just used in the `query` declaration above:
</TabItem>
</Tabs>
<ShowForJs>
### JavaScript implementation
Next, create a new file `src/server/queries.ts` and define the JavaScript function we've just imported in our `query` declaration:
</ShowForJs>
<ShowForTs>
### TypeScript implementation
Next, create a new file `src/server/queries.ts` and define the TypeScript function we've just imported in our `query` declaration:
</ShowForTs>
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
```js title="src/server/queries.js"
export const getTasks = async (args, context) => {
@ -54,6 +103,29 @@ export const getTasks = async (args, context) => {
}
```
</TabItem>
<TabItem value="ts" label="TypeScript">
```js title="src/server/queries.ts"
import { Task } from "@wasp/entities"
import { GetTasks } from "@wasp/queries/types"
export const getTasks: GetTasks<void, Task[]> = async (args, context) => {
return context.entities.Task.findMany({})
}
```
Wasp automatically generates the types `GetTasks` and `Task` based the contents of `main.wasp`:
- `Task` is a type corresponding to the `Task` entity we've defined in `main.wasp`.
- `GetTasks` is a generic type Wasp automatically generated based the `getTasks` query we've defined in `main.wasp`.
You can use these types to specify the Query's input and output types. This Query doesn't expect any arguments (meaning that its input type is `void`), but it does return an array of tasks (meaning that its output type is `Task[]`)
Annotating the Queries is optional, but highly recommended because doing so enables **full-stack type safety**. We'll see what this means in the next section.
</TabItem>
</Tabs>
Query function parameters:
- `args`: `object`, arguments the query is invoked with.
- `context`: `object`, additional stuff provided by Wasp.
@ -65,52 +137,125 @@ Since we declared in `main.wasp` that our query uses the `Task` entity, Wasp inj
Queries and actions are NodeJS functions that are executed on the server. Therefore, we put them in the `src/server` folder.
:::
## Invoking the query from React
## Invoking the Query on the frontend
We've just said that the queries we write are executed on the server, but Wasp will generate client-side query functions (taking care of the network and cache invalidation in the background). Let's finally use the query we've just created, `getTasks`, in our React component to list the tasks:
We've just said that the queries we write are executed on the server, but Wasp will generate client-side query functions (taking care of serialization, network calls, and cache invalidation in the background). Let's finally use the query we've just created, `getTasks`, in our React component to list the tasks:
```jsx {1-2,5-14,17-32} title="src/client/MainPage.jsx"
import getTasks from '@wasp/queries/getTasks'
import { useQuery } from '@wasp/queries'
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
```jsx {1-2,5-14,17-36} title="src/client/MainPage.jsx"
import getTasks from "@wasp/queries/getTasks"
import { useQuery } from "@wasp/queries"
const MainPage = () => {
const { data: tasks, isFetching, error } = useQuery(getTasks)
const { data: tasks, isLoading, error } = useQuery(getTasks)
return (
<div>
{tasks && <TasksList tasks={tasks} />}
{isFetching && 'Fetching...'}
{error && 'Error: ' + error}
{isLoading && "Loading..."}
{error && "Error: " + error}
</div>
)
}
const Task = (props) => {
const Task = ({ task }) => {
return (
<div>
<input
type='checkbox' id={props.task.id}
checked={props.task.isDone}
/>
{props.task.description}
<input type="checkbox" id={String(task.id)} checked={task.isDone} />
{task.description}
</div>
)
}
const TasksList = (props) => {
if (!props.tasks?.length) return 'No tasks'
return props.tasks.map((task, idx) => <Task task={task} key={idx} />)
const TasksList = ({ tasks }) => {
if (!tasks?.length) return <div>No tasks</div>
return (
<div>
{tasks.map((task, idx) => (
<Task task={task} key={idx} />
))}
</div>
)
}
export default MainPage
```
Most of this is just regular React, the only exception being two special `@wasp` imports:
</TabItem>
<TabItem value="ts" label="TypeScript">
```tsx {1-3,6-15,18-37} title="src/client/MainPage.tsx"
import getTasks from "@wasp/queries/getTasks"
import { useQuery } from "@wasp/queries"
import { Task } from "@wasp/entities"
const MainPage = () => {
const { data: tasks, isLoading, error } = useQuery(getTasks)
return (
<div>
{tasks && <TasksList tasks={tasks} />}
{isLoading && "Loading..."}
{error && "Error: " + error}
</div>
)
}
const Task = ({ task }: { task: Task }) => {
return (
<div>
<input type="checkbox" id={String(task.id)} checked={task.isDone} />
{task.description}
</div>
)
}
const TasksList = ({ tasks }: { tasks: Task[] }) => {
if (!tasks?.length) return <div>No tasks</div>
return (
<div>
{tasks.map((task, idx) => (
<Task task={task} key={idx} />
))}
</div>
)
}
export default MainPage
```
</TabItem>
</Tabs>
<ShowForJs>
Most of this code is just regular React, the only exception being the two special `@wasp` imports:
- `import getTasks from '@wasp/queries/getTasks'` - Gives us our freshly defined Wasp query.
- `import { useQuery } from '@wasp/queries'` - Gives us Wasp's [useQuery](language/features.md#the-usequery-hook) React hook which is actually just a thin wrapper over [react-query](https://github.com/tannerlinsley/react-query)'s [useQuery](https://react-query.tanstack.com/docs/guides/queries) hook, behaving very similarly while offering some extra integration with Wasp.
While we could directly call the query with `getTasks()`, calling it with `useQuery(getTasks)` gives us reactivity - the React component gets re-rendered if the result of the query changes.
</ShowForJs>
<ShowForTs>
Most of this code is just regular React, the only exception being the three special `@wasp` imports:
- `import getTasks from '@wasp/queries/getTasks'` - Gives us our freshly defined Wasp query.
- `import { useQuery } from '@wasp/queries'` - Gives us Wasp's [useQuery](language/features.md#the-usequery-hook) React hook which is actually just a thin wrapper over [react-query](https://github.com/tannerlinsley/react-query)'s [useQuery](https://react-query.tanstack.com/docs/guides/queries) hook, behaving very similarly while offering some extra integration with Wasp.
- `import { Task } from '@wasp/entities'` - The type for Task entity we've defined in `main.wasp`.
Notice how you didn't need to tell TypeScript anything about the Query's response data - TypeScript inferred it automatically.
Because we've previously annotated the Query's backend implementation with `GetTasks<void, Task[]>`, Wasp knows the response data's correct type (i.e., `Task[]`) on the frontend. We call this feature **full-stack type safety**.
</ShowForTs>
We could have called the Query directly with `getTasks()`, but wrapping it with `useQuery(getTasks)` makes it reactive. More precisely, React will re-render the component every time the Query's result changes.
With these changes, you should be seeing the text "No tasks" on the screen:

View File

@ -4,17 +4,22 @@ title: "Creating tasks"
---
import useBaseUrl from '@docusaurus/useBaseUrl';
import { ShowForTs, ShowForJs } from '@site/src/components/TsJsHelpers';
To enable the creation of new tasks, we will need two things:
1. A Wasp action that creates a new task.
2. A React form that calls that action with the new task's data.
## Action
## Defining the Action
Creating an action is very similar to creating a query.
### Wasp declaration
First, we declare the action in Wasp:
We must first declare the Action in `main.wasp`:
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
```wasp title="main.wasp"
// ...
@ -24,9 +29,39 @@ action createTask {
}
```
### JS implementation
</TabItem>
<TabItem value="ts" label="TypeScript">
```wasp title="main.wasp"
// ...
action createTask {
fn: import { createTask } from "@server/actions.js",
entities: [Task]
}
```
</TabItem>
</Tabs>
<ShowForJs>
### JavaScript implementation
Let's now define a JavaScript function for our action:
</ShowForJs>
<ShowForTs>
### TypeScript implementation
Let's now define a TypeScript function for our action:
</ShowForTs>
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
Next, we define a JS function for that action:
```js title="src/server/actions.js"
export const createTask = async (args, context) => {
return context.entities.Task.create({
@ -35,19 +70,47 @@ export const createTask = async (args, context) => {
}
```
</TabItem>
<TabItem value="ts" label="TypeScript">
```ts title="src/server/actions.ts"
import { Task } from "@wasp/entities"
import { CreateTask } from "@wasp/actions/types"
type CreateTaskPayload = Pick<Task, "description">
export const createTask: CreateTask<CreateTaskPayload, Task> = async (
args,
context
) => {
return context.entities.Task.create({
data: { description: args.description },
})
}
```
Once again, we've annotated the Action with proper types (using the types `Task` and `CreateTask` Wasp generated for us). Annotating the Action makes the type information automatically available the frontend, giving us automatic **full-stack type safety**.
</TabItem>
</Tabs>
:::tip
We put the JS function in a new file `src/server/actions.js`, but we could have put it anywhere we wanted! There are no limitations here, as long as the import statement in the Wasp file is correct and the source file is inside the `src/server` folder.
We put the function in a new file `src/server/actions.{js,ts}`, but we could have put it anywhere we wanted! There are no limitations here, as long as the import statement in the Wasp file is correct and the source file is inside the `src/server` folder.
:::
## React form
## Invoking the Action on the frontend
```jsx {3,10,37-59} title="src/client/MainPage.jsx"
import { useQuery } from '@wasp/queries'
import getTasks from '@wasp/queries/getTasks'
import createTask from '@wasp/actions/createTask'
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
```jsx {3,10,41-60} title="src/client/MainPage.jsx"
import getTasks from "@wasp/queries/getTasks"
import createTask from "@wasp/actions/createTask"
import { useQuery } from "@wasp/queries"
const MainPage = () => {
const { data: tasks, isFetching, error } = useQuery(getTasks)
const { data: tasks, isLoading, error } = useQuery(getTasks)
return (
<div>
@ -55,49 +118,50 @@ const MainPage = () => {
{tasks && <TasksList tasks={tasks} />}
{isFetching && 'Fetching...'}
{error && 'Error: ' + error}
{isLoading && "Loading..."}
{error && "Error: " + error}
</div>
)
}
const Task = (props) => {
const Task = ({ task }) => {
return (
<div>
<input
type='checkbox' id={props.task.id}
checked={props.task.isDone}
/>
{props.task.description}
<input type="checkbox" id={String(task.id)} checked={task.isDone} />
{task.description}
</div>
)
}
const TasksList = (props) => {
if (!props.tasks?.length) return 'No tasks'
return props.tasks.map((task, idx) => <Task task={task} key={idx} />)
const TasksList = ({ tasks }) => {
if (!tasks?.length) return <div>No tasks</div>
return (
<div>
{tasks.map((task, idx) => (
<Task task={task} key={idx} />
))}
</div>
)
}
const NewTaskForm = (props) => {
const NewTaskForm = () => {
const handleSubmit = async (event) => {
event.preventDefault()
try {
const description = event.target.description.value
event.target.reset()
const target = event.target
const description = target.description.value
target.reset()
await createTask({ description })
} catch (err) {
window.alert('Error: ' + err.message)
} catch (err: any) {
window.alert("Error: " + err.message)
}
}
return (
<form onSubmit={handleSubmit}>
<input
name='description'
type='text'
defaultValue=''
/>
<input type='submit' value='Create task' />
<input name="description" type="text" defaultValue="" />
<input type="submit" value="Create task" />
</form>
)
}
@ -105,9 +169,90 @@ const NewTaskForm = (props) => {
export default MainPage
```
Here we call our action directly (no hooks) because we don't need any reactivity. The rest is just regular React code.
</TabItem>
<TabItem value="ts" label="TypeScript">
```tsx {1,3,12,43-62} title="src/client/MainPage.tsx"
import { FormEvent } from "react"
import getTasks from "@wasp/queries/getTasks"
import createTask from "@wasp/actions/createTask"
import { useQuery } from "@wasp/queries"
import { Task } from "@wasp/entities"
const MainPage = () => {
const { data: tasks, isLoading, error } = useQuery(getTasks)
return (
<div>
<NewTaskForm />
{tasks && <TasksList tasks={tasks} />}
{isLoading && "Loading..."}
{error && "Error: " + error}
</div>
)
}
const Task = ({ task }: { task: Task }) => {
return (
<div>
<input type="checkbox" id={String(task.id)} checked={task.isDone} />
{task.description}
</div>
)
}
const TasksList = ({ tasks }: { tasks: Task[] }) => {
if (!tasks?.length) return <div>No tasks</div>
return (
<div>
{tasks.map((task, idx) => (
<Task task={task} key={idx} />
))}
</div>
)
}
const NewTaskForm = () => {
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault()
try {
const target = event.target as HTMLFormElement
const description = target.description.value
target.reset()
await createTask({ description })
} catch (err: any) {
window.alert("Error: " + err.message)
}
}
return (
<form onSubmit={handleSubmit}>
<input name="description" type="text" defaultValue="" />
<input type="submit" value="Create task" />
</form>
)
}
export default MainPage
```
</TabItem>
</Tabs>
We're calling the `createTask` Action directly this time (i.e., without wrapping it with a hook) because we don't need reactivity. The rest is just regular React code.
<ShowForTs>
Finally, because we've previously annotated the Action's backend implementation with the correct type, Wasp knows that the `createTask` action expects a value of type `{ description: string }` (try changing the argument and reading the error message). Wasp also knows that a call to the `createTask` action returns a `Task`, but we don't need it.
</ShowForTs>
That's it!
Try creating a "Build a Todo App in Wasp" task and see it appear in the list below.
The task is created on the server and also saved in the database. Try refreshing the page or opening it in another browser - you'll see the tasks are still here!

View File

@ -4,18 +4,23 @@ title: "Updating tasks"
---
import useBaseUrl from '@docusaurus/useBaseUrl';
import { ShowForTs, ShowForJs } from '@site/src/components/TsJsHelpers';
The Todo app isn't done if you can't mark a task as done!
Our Todo app isn't finished if you we can't mark a task as finished!
For that, we will need to do two things:
1. Implement a Wasp action that updates the task.
2. Modify our React code so it calls that action.
To do that, we'll need to do two things:
1. Implement a Wasp Action for toggling a task's `isDone` state.
2. Call this Action from React whenever the user toggles a checkbox.
## Action
## Defining the Action
### Wasp declaration
We declare a Wasp action:
Let's first define the Action in `main.wasp`:
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
```wasp title="main.wasp"
// ...
@ -25,55 +30,159 @@ action updateTask {
}
```
### JS implementation
</TabItem>
<TabItem value="ts" label="TypeScript">
```wasp title="main.wasp"
// ...
action updateTask {
fn: import { updateTask } from "@server/actions.js",
entities: [Task]
}
```
</TabItem>
</Tabs>
<ShowForJs>
### JavaScript implementation
Let's now define the Action's JavaScript implementation in `src/server/actions.js`:
</ShowForJs>
<ShowForTs>
### TypeScript implementation
Let's now define the Action's JavaScript implementation in `src/server/actions.ts`:
</ShowForTs>
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
We define the JS implementation of the Wasp action in `src/server/actions.js`:
```js title="src/server/actions.js"
// ...
export const updateTask = async (args, context) => {
export const updateTask = async ({ id, isDone }, context) => {
return context.entities.Task.update({
where: { id: args.taskId },
where: { id },
data: {
isDone: args.data.isDone
}
isDone: isDone,
},
})
}
```
## React logic
</TabItem>
<TabItem value="ts" label="TypeScript">
And we update the React component:
```jsx {2,7-16,23} title="src/client/MainPage.jsx"
```ts title="src/server/actions.ts"
// highlight-next-line
import { CreateTask, UpdateTask } from "@wasp/actions/types"
// ...
type UpdateTaskPayload = Pick<Task, "id" | "isDone">
export const updateTask: UpdateTask<UpdateTaskPayload, Task> = async (
{ id, isDone },
context
) => {
return context.entities.Task.update({
where: { id },
data: {
isDone: isDone,
},
})
}
```
</TabItem>
</Tabs>
## Invoking the Action on the frontend
Finally, all that's left to do is call the Action from the React component:
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
```jsx {2,7-16,24} title="src/client/MainPage.jsx"
// ...
import updateTask from '@wasp/actions/updateTask'
// ...
const Task = (props) => {
const Task = ({ task }) => {
const handleIsDoneChange = async (event) => {
try {
await updateTask({
taskId: props.task.id,
data: { isDone: event.target.checked }
id: task.id,
isDone: event.target.checked,
})
} catch (error) {
window.alert('Error while updating task: ' + error.message)
} catch (error: any) {
window.alert("Error while updating task: " + error.message)
}
}
return (
<div>
<input
type='checkbox' id={props.task.id}
checked={props.task.isDone}
type="checkbox"
id={String(task.id)}
checked={task.isDone}
onChange={handleIsDoneChange}
/>
{props.task.description}
{task.description}
</div>
)
}
// ...
```
</TabItem>
<TabItem value="ts" label="TypeScript">
```tsx {2,4,9-18,26} title="src/client/MainPage.tsx"
// ...
import { FormEvent, ChangeEvent } from "react"
// ...
import updateTask from '@wasp/actions/updateTask'
// ...
const Task = ({ task }: { task: Task }) => {
const handleIsDoneChange = async (event: ChangeEvent<HTMLInputElement>) => {
try {
await updateTask({
id: task.id,
isDone: event.target.checked,
})
} catch (error: any) {
window.alert("Error while updating task: " + error.message)
}
}
return (
<div>
<input
type="checkbox"
id={String(task.id)}
checked={task.isDone}
onChange={handleIsDoneChange}
/>
{task.description}
</div>
)
}
// ...
```
</TabItem>
</Tabs>
Awesome! We can now tick this task as done ;).

View File

@ -5,13 +5,13 @@ title: "Authentication"
import useBaseUrl from '@docusaurus/useBaseUrl';
Most of the apps today are multi-user, and Wasp has first-class support for it, so let's see how to add it to our Todo app!
Most of the apps today require some sort of registration and login flows, and Wasp has support for it out of the box, so let's see how to add it to our Todo app!
Let's define a Todo list (luckily we have an app for that now ;)) to get this done:
- [ ] Add Wasp entity `User`.
- [ ] Add `auth` Wasp declaration.
- [ ] Add `Login` and `Signup` pages
- [ ] Modify `src/client/MainPage.jsx` so that it requires authentication.
Let's define a Todo list (luckily we have an app for that now 🙃) to get this done:
- [ ] Add a new entity called `User`.
- [ ] Add `auth` to our `app`.
- [ ] Add `Login` and `Signup` pages.
- [ ] Modify `src/client/MainPage.{jsx,tsx}` so that it requires authentication.
- [ ] Add Prisma relation between `User` and `Task` entities.
- [ ] Modify our queries and actions so that they work only with the tasks belonging to the authenticated user.
- [ ] Add a logout button.
@ -45,10 +45,10 @@ app TodoApp {
title: "Todo app",
auth: {
// Expects entity User to have (username:String) and (password:String) fields.
// Expects entity User to have username and passwords fields.
userEntity: User,
methods: {
// We also support Google and GitHub, with more on the way!
// We also support Google, GitHub and email auth, with more on the way!
usernameAndPassword: {}
},
// We'll see how this is used a bit later
@ -58,9 +58,9 @@ app TodoApp {
```
What this means for us is that Wasp now offers us:
- Login and Signup forms located at `@wasp/auth/forms/Login` and `@wasp/auth/forms/Signup` paths, ready to be used.
- `logout()` action.
- React hook `useAuth()`.
- `context.user` as an argument within query/action.
- a `logout()` action.
- a React hook `useAuth()`.
- `context.user` as an argument within queries/actions.
This is a very high-level API for auth which makes it very easy to get started quickly, but is
not very flexible. If you require more control (e.g. want to execute some custom code on the server
@ -92,6 +92,9 @@ page LoginPage {
Great, Wasp now knows how to route these and where to find the pages. Now to the React code of the pages:
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
```jsx title="src/client/LoginPage.jsx"
import { Link } from 'react-router-dom'
@ -112,8 +115,36 @@ const LoginPage = () => {
export default LoginPage
```
</TabItem>
<TabItem value="ts" label="TypeScript">
```tsx title="src/client/LoginPage.tsx"
import { Link } from 'react-router-dom'
import { LoginForm } from '@wasp/auth/forms/Login'
const LoginPage = () => {
return (
<>
<LoginForm/>
<br/>
<span>
I don't have an account yet (<Link to="/signup">go to signup</Link>).
</span>
</>
)
}
export default LoginPage
```
</TabItem>
</Tabs>
The Signup page is very similar to the login one:
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
```jsx title="src/client/SignupPage.jsx"
import { Link } from 'react-router-dom'
@ -133,6 +164,30 @@ const SignupPage = () => {
export default SignupPage
```
</TabItem>
<TabItem value="ts" label="TypeScript">
```tsx title="src/client/SignupPage.tsx"
import { Link } from 'react-router-dom'
import { SignupForm } from '@wasp/auth/forms/Signup'
const SignupPage = () => {
return (
<>
<SignupForm/>
<br/>
<span>
I already have an account (<Link to="/login">go to login</Link>).
</span>
</>
)
}
export default SignupPage
```
</TabItem>
</Tabs>
## Updating `MainPage` page to check if the user is authenticated
@ -154,11 +209,26 @@ If an unauthenticated user tries to access route `/` where our page `MainPage` i
Also, when `authRequired` is set to `true`, the React component of a page (specified by `component` property within `page`) will be provided `user` object as a prop. It can be accessed like this:
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
```jsx {1} title="src/client/MainPage.jsx"
const MainPage = ({ user }) => {
// Do something with the user
}
```
</TabItem>
<TabItem value="ts" label="TypeScript">
```tsx {3} title="src/client/MainPage.tsx"
import { User } from "@wasp/entities"
const MainPage = ({ user }: { user: User }) => {
// Do something with the user
}
```
</TabItem>
</Tabs>
Ok, time to try out how this works!
@ -221,18 +291,47 @@ However, for this tutorial, for the sake of simplicity, we will stick with this.
## Updating operations to forbid access to non-authenticated users
Next, let's update the queries and actions to forbid access to non-authenticated users and to operate only on the currently logged-in user's tasks:
```js {1,4,6} title="src/server/queries.js"
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
```js {1,4} title="src/server/queries.js"
import HttpError from '@wasp/core/HttpError.js'
export const getTasks = async (args, context) => {
if (!context.user) { throw new HttpError(401) }
if (!context.user) {
throw new HttpError(401)
}
return context.entities.Task.findMany(
{ where: { user: { id: context.user.id } } }
)
}
```
</TabItem>
<TabItem value="ts" label="TypeScript">
```js {1,4,8,14,15,16} title="src/server/actions.js"
```ts {3,6} title="src/server/queries.ts"
import { Task } from "@wasp/entities"
import { GetTasks } from "@wasp/queries/types"
import HttpError from '@wasp/core/HttpError.js'
export const getTasks: GetTasks<void, Task[]> = async (args, context) => {
if (!context.user) {
throw new HttpError(401)
}
return context.entities.Task.findMany(
{ where: { user: { id: context.user.id } } }
)
}
```
</TabItem>
</Tabs>
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
```js {1,4,8,14,17,18} title="src/server/actions.js"
import HttpError from '@wasp/core/HttpError.js'
export const createTask = async (args, context) => {
@ -246,13 +345,57 @@ export const createTask = async (args, context) => {
}
export const updateTask = async (args, context) => {
if (!context.user) { throw new HttpError(401) }
if (!context.user) {
throw new HttpError(401)
}
return context.entities.Task.updateMany({
where: { id: args.taskId, user: { id: context.user.id } },
data: { isDone: args.data.isDone }
})
}
```
</TabItem>
<TabItem value="ts" label="TypeScript">
```ts {3,11,17,28,31,32} title="src/server/actions.ts"
import { Task } from "@wasp/entities"
import { CreateTask, UpdateTask } from "@wasp/actions/types"
import HttpError from '@wasp/core/HttpError.js'
type CreateTaskPayload = Pick<Task, "description">
export const createTask: CreateTask<CreateTaskPayload, Task> = async (
args,
context
) => {
if (!context.user) {
throw new HttpError(401)
}
return context.entities.Task.create({
data: {
description: args.description,
user: { connect: { id: context.user.id } }
}
})
}
type UpdateTaskPayload = Pick<Task, "id" | "isDone">
export const updateTask: UpdateTask<UpdateTaskPayload, { count: number }> = async (
{ id, isDone },
context
) => {
if (!context.user) {
throw new HttpError(401)
}
return context.entities.Task.updateMany({
where: { id: args.taskId, user: { id: context.user.id } },
data: { isDone: args.data.isDone }
})
}
```
</TabItem>
</Tabs>
:::note
Due to how Prisma works, we had to convert `update` to `updateMany` in `updateTask` action to be able to specify the user id in `where`.
@ -280,6 +423,10 @@ You will see that each user has their own tasks, just as we specified in our cod
## Logout button
Last, but not least, let's add the logout functionality:
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
```jsx {2,10} title="src/client/MainPage.jsx"
// ...
import logout from '@wasp/auth/logout'
@ -295,5 +442,25 @@ const MainPage = () => {
)
}
```
</TabItem>
<TabItem value="ts" label="TypeScript">
```tsx {2,10} title="src/client/MainPage.tsx"
// ...
import logout from '@wasp/auth/logout'
//...
const MainPage = () => {
// ...
return (
<div>
// ...
<button onClick={logout}>Logout</button>
</div>
)
}
```
</TabItem>
</Tabs>
This is it, we have a working authentication system, and our Todo app is multi-user!

View File

@ -4,6 +4,7 @@ title: "Dependencies"
---
import useBaseUrl from '@docusaurus/useBaseUrl';
import { ShowForTs } from '@site/src/components/TsJsHelpers';
What is a Todo app without some clocks!? Well, still a Todo app, but certainly not as fun as one with clocks!
@ -15,7 +16,7 @@ app TodoApp {
// ...
dependencies: [
("react-clock", "3.0.0")
("react-clock", "4.2.0")
]
}
```
@ -27,6 +28,9 @@ wasp start
to have Wasp download and install the new dependency. If `wasp start` is already running, Wasp will detect the dependency change, and restart automatically.
Next, let's create a new component `Clocks` where we can play with the clocks.
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
```jsx title="src/client/Clocks.jsx"
import { useEffect, useState } from 'react'
import Clock from 'react-clock'
@ -50,8 +54,40 @@ const Clocks = () => {
export default Clocks
```
</TabItem>
<TabItem value="ts" label="TypeScript">
```tsx title="src/client/Clocks.tsx"
import { useEffect, useState } from 'react'
import Clock from 'react-clock'
import 'react-clock/dist/Clock.css'
const Clocks = () => {
const [time, setTime] = useState(new Date())
useEffect(() => {
const interval = setInterval(() => setTime(new Date()), 1000)
return () => clearInterval(interval)
}, [])
return (
<div style={{ display: 'flex' }}>
<Clock value={time} />
<Clock value={new Date(time.getTime() + 60 * 60000)} />
</div>
)
}
export default Clocks
```
</TabItem>
</Tabs>
And let's import it into our `MainPage.{jsx,tsx}` component.
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
And let's import it into our main React component.
```jsx {2,11} title="src/client/MainPage.jsx"
// ...
import Clocks from './Clocks'
@ -71,6 +107,36 @@ const MainPage = () => {
}
// ...
```
As you can see, importing other files from `src/client` is completely normal, just use the relative path. The same goes for all files under `src/server`. You can't (and shouldn't) import files from `src/client` into `src/server` and vice versa. If you want to share code between the two runtimes, you can use a relative import to import anything from `src/shared` into both the client code and the server code. The `src/shared` is especially handy for full-stack type definitions when using TypeScript.
</TabItem>
<TabItem value="ts" label="TypeScript">
```tsx {2,11} title="src/client/MainPage.tsx"
// ...
import Clocks from './Clocks'
const MainPage = () => {
// ...
return (
<div>
// ...
<div><Clocks /></div>
// ...
</div>
)
}
// ...
```
</TabItem>
</Tabs>
As you can see, importing other files from `src/client` is completely normal, just use the relative path. The same goes for all files under `src/server`. You can't (and shouldn't) import files from `src/client` into `src/server` and vice versa. If you want to share code between the two runtimes, you can use a relative import to import anything from `src/shared` into both the client code and the server code.
<ShowForTs>
The `src/shared` is especially handy for full-stack type definitions when using TypeScript.
</ShowForTs>
That is it! We added a dependency and used it in our project.

View File

@ -18,16 +18,16 @@ If you are interested in what is Wasp actually generating in the background, you
## Tailwind CSS Support
We introduced Tailwind CSS support in our Beta version 0.7.0! 🎨
Wasp supports Tailwind out of the box! 🎨 If you want to style your app with sweet Tailwind CSS classes, all you have to do is add a couple of config files to your app.
If you want to style your app with sweet tailwind CSS classes, all you have to do is add a couple of config files to your app. Check out [this guide](https://www.wasp-lang.dev/blog/2022/11/16/tailwind-feature-announcement) to learn more!
Check out [this guide](/docs/integrations/css-frameworks#tailwind) to learn more!
## Where next?
## What's next?
Well, you could check [the "Language" section](language/overview.md) of the docs for more details on specific parts of Wasp.
Or, you could use Wasp to build something of your own!
Well, you could check the **Guides** on the left of the docs for more details on specific parts of Wasp. Let your imagination run wild and build something of your own! ✨ Maybe something real-time with [WebSockets](/docs/guides/websockets) or with [async jobs](/docs/language/features#jobs)?
If you notice that some of the features you'd like to have are missing, or have any other kind of feedback, please write to us on [Discord](https://discord.gg/rzdnErX) or create an issue on [Github](https://github.com/wasp-lang/wasp), so we can learn which features to add/improve next 🙏
If you notice that some of the features you'd like to have are missing, or have any other kind of feedback, please write to us on [Discord](https://discord.gg/rzdnErX) or create an issue on [Github](https://github.com/wasp-lang/wasp), so we can learn which features to add/improve next.
Even better, if you would like to contribute or help building the feature, let us know!
You can find more details on contributing [here](contributing.md).

View File

@ -1,5 +1,8 @@
const lightCodeTheme = require("prism-react-renderer/themes/github");
const autoImportTabs = require("./src/remark/auto-import-tabs");
const fileExtSwitcher = require("./src/remark/file-ext-switcher");
/** @type {import('@docusaurus/types').DocusaurusConfig} */
module.exports = {
title: "Wasp",
@ -151,6 +154,7 @@ module.exports = {
sidebarCollapsible: true,
// Please change this to your repo.
editUrl: "https://github.com/wasp-lang/wasp/edit/main/web",
remarkPlugins: [autoImportTabs, fileExtSwitcher],
},
blog: {
showReadingTime: true,

View File

@ -4,7 +4,7 @@
"private": true,
"scripts": {
"docusaurus": "docusaurus",
"start": "docusaurus start",
"start": "docusaurus start --port 3002",
"build": "docusaurus build",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",

View File

@ -11,12 +11,7 @@ module.exports = {
},
{
type: "category",
label: "Tutorials",
collapsed: false,
items: [
{
type: "category",
label: "Todo app",
label: "Tutorial",
collapsed: true,
items: [
"tutorials/todo-app",
@ -37,8 +32,6 @@ module.exports = {
"tutorials/todo-app/08-the-end",
],
},
],
},
"examples",
{
type: "category",

View File

@ -0,0 +1,26 @@
// Copied from
// https://github.com/redwoodjs/redwood/blob/bd903c5755925ea7174775a2fdaba371b700c910/docs/src/components/FileExtSwitcher.tsx
import * as React from 'react'
import { useStorageSlot } from '@docusaurus/theme-common'
interface Props {
path: string
}
/**
* Takes a path on the form web/src/layouts/BlogLayout/BlogLayout.{js,tsx} and
* replaces the end part, {js,tsx}, with the correct file extension depending
* on what language the user has selected for the code blocks
*/
export default function FileExtSwitcher({ path }: Props) {
const [jsTs] = useStorageSlot('docusaurus.tab.js-ts')
const extensionStart = path.lastIndexOf('{')
const extensions = path.slice(extensionStart + 1, path.length - 1)
const [jsExt, tsExt] = extensions.split(',')
const pathWithoutExt = path.slice(0, extensionStart)
return <code>{pathWithoutExt + (jsTs === 'js' ? jsExt : tsExt)}</code>
}

View File

@ -0,0 +1,43 @@
// Copied from (and adapted)
// https://github.com/redwoodjs/redwood/blob/bd903c5755925ea7174775a2fdaba371b700c910/docs/src/components/ShowForTs.tsx
import * as React from 'react'
import { useStorageSlot } from '@docusaurus/theme-common'
import MDXContent from '@theme/MDXContent'
interface Props {
children: React.ReactNode
}
/**
* Only renders this block if user has selected TS in the codeblocks
* @Note leave a blank space after opening the tag e.g.
*
* @example
* <ShowForTs>
* // {blank space}
* ### Mdx Formatted content
* </ShowForTs>
* **/
export function ShowForTs({ children }: Props) {
const [jsTs] = useStorageSlot('docusaurus.tab.js-ts')
return jsTs === 'ts' && <MDXContent>{children}</MDXContent>
}
/**
* Only renders this block if user has selected JS in the codeblocks
* @Note leave a blank space after opening the tag e.g.
*
* @example
* <ShowForJs>
* // {blank space}
* ### Mdx Formatted content
* </ShowForJs>
* **/
export function ShowForJs({ children }: Props) {
const [jsTs] = useStorageSlot('docusaurus.tab.js-ts')
return jsTs === 'js' && <MDXContent>{children}</MDXContent>
}

View File

@ -0,0 +1,23 @@
// Copied from
// https://github.com/redwoodjs/redwood/blob/bd903c5755925ea7174775a2fdaba371b700c910/docs/src/remark/auto-import-tabs.js
const needImports = (tree) =>
tree.children.some(
(child) => child.type === 'jsx' && /^<Tabs\b/.test(child.value)
)
const plugin = () => (tree, _file) => {
if (needImports(tree)) {
// Add `import` nodes to the top of the parsed file
tree.children.unshift({
type: 'import',
value: "import Tabs from '@theme/Tabs'",
})
tree.children.unshift({
type: 'import',
value: "import TabItem from '@theme/TabItem'",
})
}
}
module.exports = plugin

View File

@ -0,0 +1,28 @@
// Copied from
// https://github.com/redwoodjs/redwood/blob/bd903c5755925ea7174775a2fdaba371b700c910/docs/src/remark/file-ext-switcher.js
const visit = require('unist-util-visit')
const plugin = () => {
let needImport = false
return (tree, _file) => {
visit(tree, (node) => {
if (node.type === 'inlineCode' && /\w\.\{jsx?,tsx?}$/.test(node.value)) {
needImport = true
node.type = 'jsx'
node.value = `<FileExtSwitcher path="${node.value}" />`
}
})
if (needImport) {
tree.children.unshift({
type: 'import',
value:
"import FileExtSwitcher from '@site/src/components/FileExtSwitcher'",
})
}
}
}
module.exports = plugin