Use Form Actions in Angular with AnalogJS for streamlined form submission, validation and state management.
All the big meta frameworks have Form Actions, and now, so does Angular! Progressive enhancement in Form Actions enables forms to function reliably with basic HTML for all users while adding advanced features like client-side validation and dynamic interactivity for those with modern browsers and JavaScript support.
Form Actions allow you to handle form submissions to the server in a structured way in Angular. Validation and state management are simple, and you only need to add a server file and bind a few events to the client Analog component in Angular. Handling form submission to the server is a breeze.
I translated the Analog Form Server Actions example to use the .analog
or .ag
file extensions. This allows you to create a single file component, similar to Vue or Svelte. It magically compiles to a regular Angular component.
Make sure the experimental flag is enabled.
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => ({
build: {
target: ['es2020'],
},
resolve: {
mainFields: ['module'],
},
plugins: [
analog({
ssr: false,
static: false,
prerender: {
routes: [],
},
// this should be enabled <-----
vite: { experimental: { supportAnalogFormat: true } },
nitro: {
preset: 'vercel-edge'
}
}),
],
}));
The pages
directory functions as the file-based router. It is similar to NextJS’s app
directory or SvelteKit’s routes
directory.
In Analog, you need to have your routes in the pages
directory. If you want to add a server action, create a file with the same name as .server.ts
.
Our newsletter component will be newsletter.server.ts
.
import {
type PageServerAction,
redirect,
json,
fail,
} from '@analogjs/router/server/actions';
import { readFormData } from 'h3';
export async function action({ event }: PageServerAction) {
const body = await readFormData(event);
const email = body.get('email');
if (email instanceof File) {
return fail(422, { email: 'Invalid type' });
}
if (!email) {
return fail(422, { email: 'Email is required' });
}
if (email.length < 10) {
return redirect('/');
}
return json({ type: 'success' });
}
readFormData
– An async function to get the form data from the form action, as type FormData
fail
– Will return an error and call the onError()
function on the frontendredirect
– Will send a redirect request to the frontendjson
– Will return values to the frontend and call the onSuccess()
functionThis is much simpler than Svelte, which it closely mimics.
Analog files can eliminate odd decorators in TypeScript and simplify things.
<script lang="ts">
// the JS code here
</script>
<template>
// the angular template
</template>
<style>
// css if you don't use Tailwind
</style>
Angular has a LOT of boilerplate, and this change makes writing in Angular productive again.
We must name our client file newsletter.page.ag
if we want to use Analog SFC’s with the page router.
<script lang="ts">
import { signal } from '@angular/core';
import { FormAction } from '@analogjs/router';
defineMetadata({
imports: [FormAction]
});
type FormErrors =
| {
email?: string;
}
| undefined;
const signedUp = signal(false);
const errors = signal<FormErrors>(undefined);
function onSuccess() {
signedUp.set(true);
}
function onError(result: FormErrors) {
errors.set(result);
}
</script>
<template>
<section class="flex flex-col gap-5">
<h3 class="text-2xl">Newsletter Signup</h3>
@if (!signedUp()) {
<form method="post" (onSuccess)="onSuccess()" (onError)="onError($event)" (onStateChange)="errors.set(undefined)">
<div class="flex gap-5 items-center">
<label for="email"> Email </label>
<input class="border p-1 rounded" type="email" name="email" />
<button class="border bg-blue-800 text-white p-2 rounded" type="submit">Submit</button>
</div>
</form>
@if( errors()?.email ) {
<p>{{ errors()?.email }}</p>
}
} @else {
<div>Thanks for signing up!</div>
}
</section>
</template>
We can use the defineMetadata
inside Analog files.
import { FormAction } from '@analogjs/router';
defineMetadata({
imports: [FormAction]
});
There are three important events to look out for.
(onSuccess)
– Gets called when there is no error; json()
is used in the form action(onError)
– Gets called when there is an error; fail()
is called in the form action(onStateChange)
– Gets called when the state of the form changes; this is good for resetting the form stateIf we pass values from the server, we can use $event
to get the event data, aka binding the event.
We can’t have an Angular app without using signals. We can define our custom FormErrors
type and set the errors from the event bindings.
type FormErrors =
| {
email?: string;
}
| undefined;
const signedUp = signal(false);
const errors = signal<FormErrors>(undefined);
The error itself can be handled any way we want.
The success message is generated on the client, but we could also pass a message from the server if necessary.
Analog Form Actions are incredibly easy to use and effective. Building the same app in NextJS or SvelteKit would require much more boilerplate. Thank you, Brandon! Support him if you can.
Jonathan Gamble has been an avid web programmer for more than 20 years. He has been building web applications as a hobby since he was 16 years old, and he received a post-bachelor’s in Computer Science from Oregon State. His real passions are language learning and playing rock piano, but he never gets away from coding. Read more from him at https://code.build/.