Telerik blogs

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.

Angular Form Actions Demo - newsletter signup with field for email, submit button, and links to home and newsletter

TL;DR

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.

Single File Component (SFC)

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.

Enable

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'
      }
    }),
  ],
}));

Pages Directory

The pages directory functions as the file-based router. It is similar to NextJS’s app directory or SvelteKit’s routes directory.

Create the Server File

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.

Newsletter

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' });
}

New Functions

  • 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 frontend
  • redirect – Will send a redirect request to the frontend
  • json – Will return values to the frontend and call the onSuccess() function

This is much simpler than Svelte, which it closely mimics.

Analog SFC Makeup

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.

Create the Client Interaction

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>

Import the Form Action

We can use the defineMetadata inside Analog files.

import { FormAction } from '@analogjs/router';

defineMetadata({
  imports: [FormAction]
});

Event Bindings

There are three important events to look out for.

  1. (onSuccess) – Gets called when there is no error; json() is used in the form action
  2. (onError) – Gets called when there is an error; fail() is called in the form action
  3. (onStateChange) – Gets called when the state of the form changes; this is good for resetting the form state

If we pass values from the server, we can use $event to get the event data, aka binding the event.

Signals

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);

Error

The error itself can be handled any way we want.

Error message says Email is required

Success

The success message is generated on the client, but we could also pass a message from the server if necessary.

Success message says Thanks for signing up!

Verdict?

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.


About the Author

Jonathan Gamble

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/.

 

 

Related Posts

Comments

Comments are disabled in preview mode.