SvelteKit Forms: Grammar Check App
Tags: svelte, javascript, webdev
Published: 2023-05-22 15:55:56

---
title: "SvelteKit Forms: Grammar Check App"
published: "true"
description: "SvelteKit forms ❤️ use SvelteKit endpoint APIs to build a spelling, punctuation & grammar check app, with client and server form handling 📝"
tags: "svelte, javascript, webdev"
canonical_url: "https://rodneylab.com/sveltekit-forms/"
cover_image: "https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d14xjw6zzt9cpdqrl7xk.png"
---
## SvelteKit Forms

SvelteKit forms and endpoints work a little differently to other frameworks, like Astro or Remix, so it is worth taking a detailed look at how to pass form data from the client browser to your app&rsquo;s running server instance.  To help here, we look at code for a **spelling, punctuation and grammar checking** tool using LanguageTool.  **LanguageTool** is a service with similar features to Grammarly and offers a free, but limited, API to check text snippets.

Initially, we will look at how you can get data from your server to the client with SvelteKit.  Then, we look at how to run an initial grammar check from the client (before the page renders).  LanguageTool&rsquo;s free API limits requests on an IP address basis. This makes running checks from the browser, rather than the server, a good idea.  That is because, even with a few users running checks simultaneously, limits could quickly be breached, letting no users complete checks.  Running checks from the browser, allows each user&rsquo;s check requests to be sent from that user&rsquo;s own IP address and not the server&rsquo;s.

### Form Handling in Client and on the Server

To end, we will look at client-side and server forms.  We use a **client-side form** to re-run the grammar check on updated (corrected) text.  Then, the **server-side form** is used to submit the final version back to the server for saving to a database.

We will focus on the SvelteKit site, without drilling down into how the LanguageTool API interactions work.  The full code is in the Rodney Lab GitHub repo, though, and you will find a link for this further down.  For now, let&rsquo;s get started on the app!

## 🌱 What are `+page.svelte`, `+page.ts` and `+page.server.ts`?

As you start out in SvelteKit, you might be a little overwhelmed by the different files that you can use for each route.  SvelteKit works a little differently to <a href="https://rodneylab.com/tags/astro/">Astro</a>, <a href="https://rodneylab.com/tags/deno/">Deno Fresh</a> or Remix where typically, you can combine the server and client code into a single file.  So let&rsquo;s start by seeing what each file does and when you might need them.

Each SvelteKit route can have three files: `+page.svelte`, `+page.ts` and `+page.server.ts` or their JavaScript equivalents.

### `+page.svelte`

`+page.svelte` is home to the markup we want rendered for the page.   As well as markup, it can contain **JavaScript logic to run on the client**. This is usually wrapped in a `<script>` tag at the top of the file.  You will omit this file for a resource (or non-HTML route), like an XML sitemap, for example.

### `+page.ts`

`+page.ts` is also code which runs on the client.  There is a difference between code here and code in the `<script>` tag in the `.svelte` counterpart. We use this file to prepare any data we want available **before the initial render**.  In our grammar check example, we query the LanguageTool API before rendering, and that initial API request is run in the `+page.ts` file.

### `+page.server.ts`

`+page.server.ts`,like `+page.ts`, can be used for API fetch requests.  You might use this file instead of `+page.ts` when you do not want to expose a **secret API key** to the client.  Other use cases are to pull data from a **database running on the server** or to handle user **sessions for logged-in users**.  The final common use case is listening for form submissions.  We use that here.

### Do you need all three of these?

No. You will not always need all three, though in our example we see the three working in tandem on the same route.  We mentioned the `+page.svelte` file would be omitted for a non-HTML route.  Similarly, if you have neither server forms nor secret API keys, you might decide to omit the `+page.server.ts` file.  That pattern can work well for a static site, or even, prerendered (static) pages on an otherwise, <abbr>SSR<tool-tip inert role="tooltip">Server-side Rendered</tool-tip></abbr> site.

With the basics covered, let&rsquo;s start looking at the app itself in more detail.

## 🍶 Server Code

Our app only has a single route, and all three files sit right under the `routes folder`.  For a more realistic app, you would create a folder for each route (e.g. `src/routes/contact/`, `src/routes/about/` and so on and so forth).  In each folder, you then place any of the three files needed for that route.

We keep things simple, and instead of pulling the text to check from a database (like we probably would in a real-world app), we have a static text string defined in the server route.

```typescript
import type { Actions, PageServerLoad } from './$types';

export const load: PageServerLoad = function load() {
	// add logic here to pull text from database, using a static string here
	const text = 'Is ChatGPT really neccessary? Lets take a loook.';
	return { text };
};
```
**Avoid over-fetching** here and only return data which the client requires. In the real-world, you might check for a logged-in user then return text relevant to them (a draft post, for example).  Here, we return an object with a single text field.  Next, we see this is available in the `+page.ts` file, though if we omitted that file, we could still access `text` from our `+page.svelte` file.

## 💾 Client Page Load Code


![SvelteKit Form: Initial load - screen capture shows text containing some errors. Status reads 3 checker results, and below, a short description is given for each error.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bb3vzq7lnt8zdvfl1xn8.png)

Remember, the code in `+page.ts` loads before the page first renders.  We can run an initial grammar check here on the data which (would have been stored in the app database). Here is the code for the page load file (`src/routes/+page.ts`):

```typescript
import { chunkText, grammarCheckChunk, mergeChunkResults } from '$lib/utilities/grammar';
import type { PageLoad } from './$types';

export const load: PageLoad = async function load({ data }) {
	// check grammar
	try {
		const { text } = data;
		const chunks = chunkText(text);

		const chunkResults = await Promise.all(
			chunks.map(async ({ text, offsetAdjust }) => {
				const { matches } = await grammarCheckChunk({ text, offsetAdjust });
				return { matches };
			})
		);

		const results = mergeChunkResults(chunkResults);

		return {
			results,
			text
		};
	} catch (error: unknown) {
		console.error(`Error in page load: ${error as string}`);
		throw error;
	}
};
```

Notice, the `data` argument of the `load` function in line `4`. This gives us access to the `text` field returned from the server.  Those results will be ready, ahead of the initial page render.  In lines `19`-`22`, you can see we return the grammar check results, as well as the original text.  These will now be available in the `+page.svelte` file.

## 🖥️ SvelteKitForms: Client Svelte Markup

The final step is the Svelte markup (`src/routes/+page.svelte`):

```svelte
<script lang="ts">
	import SaveIcon from '$lib/components/SaveIcon.svelte';
	import UpdateIcon from '$lib/components/UpdateIcon.svelte';
	import { chunkText, grammarCheckChunk, mergeChunkResults } from '$lib/utilities/grammar';
	import type { ActionData, PageData } from './$types';

	export let data: PageData;

	let {
		text,
		results: { matches }
	} = data;

	$: matchCount = [...matches].length;
</script>
```

Note, again we are pulling in data from the last step, (line `7`). We can destructure the data into the parts we need (lines `9`-`12`).  This app only has a single page.  However, there is one peculiarity of SvelteKit you need to bear in mind.  If you are working on a template `+page.svelte` file, for blog posts, as an example.  The first page visited will work fine and as expected.  If the visitor then navigates to another page (which uses the same template), the destructuring would not work.  SvelteKit would not realize the underlying data change with the page; it would present data for the first page.  There is a workaround, though!

```javascript
	let {
		text,
		results: { matches }
	} = data;
 
	$: ({
		text,
		results: { matches }
	} = data);
```
The first part is as before, and runs when the first page loads.  The second (added block) takes care of subsequent page loads.  Both blocks are required here.  As an alternative, you can avoid destructuring (using `data.text`, for example, instead).

##  😁 Correcting and Rechecking Text

![SvelteKit Form: Corrected text - screen capture shows corrected text with status below reading 0 checker results.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w2uokz4796yurrpzm5nz.png)

All is well and good so far!  You might have noticed the text included one or two mistakes.  We display the original text in a textarea, this is so the user can take on board the LanguageTool suggestions and update it.  Once updated, they will probably want to recheck the text.  If you&rsquo;re thinking let&rsquo;s submit the updated text to the server as a form, hold fire, we will see how to do that later.  For now, we will run the check client side.  This saves us a server trip.

### SvelteKit Forms: Client-side From Submission

Because we want to run a fetch request from the client, but after initial page load, we call the LanguageTool API from the `<script>` block in `src/routes/+page.svelte`:

```svelte
<script lang="ts">
  // ...TRUNCATED

	async function handleRecheck() {
		try {
			const chunks = chunkText(text);
			const chunkResults = await Promise.all(
				chunks.map(async ({ text, offsetAdjust }) => {
					const { matches } = await grammarCheckChunk({ text, offsetAdjust });
					return { matches };
				})
			);

			const { matches: updatedMatches } = mergeChunkResults(chunkResults);
			matches = [...updatedMatches];
		} catch (error: unknown) {
			console.error(`Error in page load: ${error as string}`);
			throw error;
		}
	}

	$: matchCount = [...matches].length;
</script>

<main>
	<h1>SvelteKit Spelling, Punctuation & Grammar Checker with LanguageTool</h1>
	<form on:submit={handleRecheck}>
		<div class="check-form-header">
			<h2>Checked Text</h2>
			<button type="submit">Re-check<UpdateIcon /></button>
		</div>
		<textarea
			bind:value={text}
			name="text"
			placeholder="Enter text"
			rows={3}
		/>
	</form>
  <!-- TRUNCATED...-->
```

Note a few features here:

- We call our `handleRecheck` function from the form `on:submit`.
- We bind the form  textarea value to the `text` variable in line `33` letting us access the updated text in the API call.
- Our server is not involved anywhere here, we are not *using the platform*, but this makes sense here.


## ✅ Server Form Handling: Saving Corrections

Once the user is happy with the text, for sure they will want to save it.  This is where we can **use the platform**!  Here is the same `src/routes/+page.svelte` file updated:


```svelte
<script lang="ts">
	import SaveIcon from '$lib/components/SaveIcon.svelte';
	import UpdateIcon from '$lib/components/UpdateIcon.svelte';
	import '$lib/styles/fonts.css';
	import '$lib/styles/global.css';
	import { chunkText, grammarCheckChunk, mergeChunkResults } from '$lib/utilities/grammar';
	import type { ActionData, PageData } from './$types';

	export let data: PageData;
	export let form: ActionData;

	let {
		text,
		results: { matches }
	} = data;
	let { missing, saved = false, text: submittedText } = form ?? {};
	if (typeof submittedText !== 'undefined') {
		text = submittedText;
	}

	// TRUNCATED...

	$: matchCount = [...matches].length;
</script>

<main>
	<h1>SvelteKit Spelling, Punctuation & Grammar Checker with LanguageTool</h1>
	<form on:submit={handleRecheck}>
		<div class="check-form-header">
			<h2>Checked Text</h2>
			<button type="submit">Re-check<UpdateIcon /></button>
		</div>
		<textarea
			bind:value={text}
			on:blur={() => {
				saved = false;
			}}
			name="text"
			placeholder="Enter text"
			rows={3}
			aria-invalid={missing === 'text'}
			aria-describedby={missing !== 'text' ? undefined : 'text-error'}
		/>
		{#if missing === 'text'}
			<small id={`text-error`} class="error-text">Enter some text before hitting save</small>
		{/if}
	</form>
	<form class="save-form" action="?/save" method="post">
		<span class="status" class:matches={matchCount > 0}> {matches.length} checker results</span>
		<input type="hidden" name="text" value={text} />
		<button type="submit">Save changes<SaveIcon /></button>
	</form>
	{#if saved}
		<div class="saved">Saved</div>
	{:else if missing}
		{JSON.stringify(missing, null, 2)}
	{/if}
</main>
```
Notice the differences in the form element:

- `action="?/save"` and `method="post"` attributes on the form element
- hidden `text` input to pass the updated text value back to the server (alternative is to include text as a query parameter in the `action` value on the form element)
- in line `10`, we pull in the data expected in response to the form submission, and use it to provide user feedback

Of course, this will not work in isolation.  Let&rsquo;s jump back to where we started; the server code for the route.  There we add the form action code, returning the `form` data we see above.

## 🎬 SvelteKit Form Actions: Server Code Take II

```typescript
import { fail } from '@sveltejs/kit';
import type { Actions, PageServerLoad } from './$types';

export const actions: Actions = {
	save: async ({ request }) => {
		try {
			const form = await request.formData();
			const text = form.get('text');
			if (typeof text === 'string') {
				// add logic to save updated text to database
				console.log('Supposed to save here but did nothing 😉');
				return { saved: true };
			}
			return fail(400, { missing: 'text', text: '' });
		} catch (error: unknown) {
			return fail(400, { missing: 'text', text: '' });
		}
	}
};

// TRUNCATED...
```

Much of the form handling code here is using the platform, and you will be familiar with it from JavaScript courses or using Astro, Deno Fresh or Remix.
Note the `save` name for our handler (line `5`) matches the name we used for it in the form DOM element.  This mechanism lets us add several form handlers to the same route.  The `fail` function lets us provide feedback when something is wrong with inputs.  Finally, returning the custom `saved` field in the response object is another way to provide user feedback, this time letting them know it all went well.

## 💯 SvelteKit Forms: Testing

We have run through the main constituent parts for passing data between server and client in SvelteKit.  If you are new to this, you will probably want to clone the full code repo and play around with it.  You do not need an API key to run the LanguageTool checks.  Consider <a href="https://kit.svelte.dev/docs/form-actions#progressive-enhancement">adding progressive enhancements</a> to the server-side form.


## 🙌🏽 SvelteKit Forms: Wrapping Up

In this post, we saw SvelteKit form handling and passing data between server and client.  In particular, we saw:

- why you might use a `+page.server.ts` file
- how you can combine `+page.server.ts` page server load with `+page.ts` page load
- how to handle forms in SvelteKit both server-side and in the client browser

Please <a aria-label="Open the Rodney Lab Git Hub repo" href="https://github.com/rodneylab/sveltekit-grammar-check">see the full repo code on the Rodney Lab GitHub repo</a>. I do hope you have found this post useful and can use the code as a starting point for a tool to help cement your understanding.  I also hope there is something you can take to an app you are currently working on, or one you plan to build in a future project.  Let me know what you decide on!  Also, please let me know about any possible improvements to the content above.

## 🙏🏽 SvelteKit Forms: Feedback

If you have found this post useful, see links below for further related content on this site. I do hope you learned one new thing from the video. Let me know if there are any ways I can improve on it. I hope you will use the code or starter in your own projects. Be sure to share your work on Twitter, giving me a mention, so I can see what you did. Finally, be sure to let me know ideas for other short videos you would like to see. Read on to find ways to get in touch, further below. If you have found this post useful, even though you can only afford even a tiny contribution, please <a aria-label="Support Rodney Lab via Buy me a Coffee" href="https://rodneylab.com/giving/">consider supporting me through Buy me a Coffee</a>.

Finally, feel free to share the post on your social media accounts for all your followers who will find it useful. As well as leaving a comment below, you can get in touch via <a href="https://twitter.com/messages/compose?recipient_id=1323579817258831875">@askRodney</a> on Twitter and also <a aria-label="Contact Rodney Lab via Telegram" href="https://t.me/askRodney">askRodney on Telegram</a>. Also, see <a aria-label="Get in touch with Rodney Lab" href="https://rodneylab.com/contact/">further ways to get in touch with Rodney Lab</a>. I post regularly on <a aria-label="See posts on svelte kit" href="https://rodneylab.com/tags/sveltekit/">SvelteKit</a> as well as <a href="https://rodneylab.com/tags/seo/" aria-label="See posts on search engine optimisation">Search Engine Optimization</a> among other topics. Also, <a aria-label="Subscribe to the   Rodney Lab newsletter" href={`${newsletterUrl}/issue/latest-issue`}>subscribe to the newsletter to keep up-to-date</a> with our latest projects.