npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@planning-inspectorate/dynamic-forms

v3.8.0

Published

This package is for building GDS forms using a configuration-based approach.

Readme

Dynamics Forms

This package is for building GDS forms using a configuration-based approach. It allows you to define a set of questions, and combine them into journeys. Questions can be configured to be included conditionally, based on the answers to other questions.

The functionality for 'check-your-answers' pages can also be useful for generating pages outside the context of a journey or form, such as for managing data. Each row shown on the page can include a link to edit that data.

Note this is ported from Appeals: dynamic forms, but transformed for ES6 modules and Node Test Runner. Not all functionality has been brought across, such as 'add more'. It is hoped that this version can be developed over time so Appeals can move to this version.

Terminology

  • Component - A "blueprint" of a type of question. i.e. input, radio button, checkbox etc.
  • Question - A specific question within a journey which is made up of one (usually) or many (sometimes) components and their required content.
  • Section - A group of Questions
  • Journey - An entire set of questions required for a completion of a submission
  • Answer - Data input by a user against a specific Question.
  • Response - a collection of answers submitted as part of a Journey.
  • Validation - Verification that an individual Answer meets the criteria of that Question. i.e. String is greater than 3 characters.

Requirements

To use this package nunjucks must be configured so that the component view files are available, and static assets must be made available for certain components and styling. Some nunjucks templates are required to correctly render the question pages and task list views.

Nunjucks

Nunjucks template paths needs configuring with the govuk-frontend and the dynamic forms folder, e.g.

import { createRequire } from 'node:module';
import path from 'node:path';
import nunjucks from 'nunjucks';

export function configureNunjucks() {
	// get the require function, see https://nodejs.org/api/module.html#modulecreaterequirefilename
	const require = createRequire(import.meta.url);
	// path to dynamic forms folder
	const dynamicFormsRoot = path.resolve(require.resolve('@planning-inspectorate/dynamic-forms'), '..');
	// get the path to the govuk-frontend folder, in node_modules, using the node require resolution
	const govukFrontendRoot = path.resolve(require.resolve('govuk-frontend'), '../..');
	const appDir = path.join('some/path/to/the', 'app');

	// configure nunjucks
	return nunjucks.configure(
		// ensure nunjucks templates can use govuk-frontend components, and templates we've defined in `web/src/app`
		[dynamicFormsRoot, govukFrontendRoot, appDir],
		{
			// output with dangerous characters are escaped automatically
			autoescape: true,
			// automatically remove trailing newlines from a block/tag
			trimBlocks: true,
			// automatically remove leading whitespace from a block/tag
			lstripBlocks: true
		}
	);
}

Assets

The select component can use the accessible-autocomplete package. This requires configuring, the component assumes the following paths are available:

  • /assets/css/accessible-autocomplete.min.css
  • /assets/js/accessible-autocomplete.min.js

Layouts

There are two layout files that need to be configured for dynamic-forms: journey and task list.

Journey template

The journey template is used for question pages. It must include a block called dynQuestionContent, and a block called head (which the default gov.uk template has). It should also include the rendering of the errorSummary component. The path to this template is configured via the journeyTemplate property on a Journey.

e.g.

{% extends "govuk/template.njk" %}

{% block beforeContent %}
    {% if errorSummary %}
        <div class="govuk-grid-row">
            <div class="govuk-grid-column-two-thirds">
                {{ govukErrorSummary({
                    titleText: "There is a problem",
                    errorList: errorSummary,
                    attributes: {"data-cy": "error-wrapper"}
                }) }}
            </div>
        </div>
    {% endif %}
{% endblock %}

{% block content %}
    <div class="govuk-grid-row">
        <div class="govuk-grid-column-full">
            <h1 class="govuk-heading-xl">
                {% if pageCaption %}
                    <span class="govuk-caption-xl">{{ pageCaption }}</span>
                {% endif %}
                {{ pageHeading }}
            </h1>
        </div>
    </div>
    
    {% block dynQuestionContent %}{% endblock %}
{% endblock %}

Task list template

The task list template is used for the task list or "check your answers" page. It must include a block called dynTaskList. The path to this template is configured via the taskListTemplate property on a Journey.

e.g.

{% extends "govuk/template.njk" %}

{% block content %}
    <div class="govuk-grid-row">
        <div class="govuk-grid-column-full">
            <h1 class="govuk-heading-xl">
                {% if pageCaption %}
                    <span class="govuk-caption-xl">{{ pageCaption }}</span>
                {% endif %}
                {{ pageHeading }}
            </h1>
        </div>
    </div>
    
    {% block dynTaskList %}{% endblock %}
{% endblock %}

Usage

The test directory includes a very basic journey in test/journey.js. This may be a useful guide for setting up a journey and associated questions. Also in test/questions.test.js there is a function createAppWithQuestions which may be a useful guide for setting up the appropriate controllers and routes for a journey.

Conditions

Conditions are used to show/hide questions based on the answers to other questions. There are fluent methods on a Section to add conditions to questions in multiple ways.

The simplest condition applies to just one question:

import { whenQuestionHasAnswer } from '@planning-inspectorate/dynamic-forms';

new Section('section-key', 'Section title')
        .addQuestion(questions.q1)
        .addQuestion(questions.q2)
        .withCondition(whenQuestionHasAnswer(questions.q1, 'yes'))

In this scenario question two will only be shown if question one has an answer of 'yes'.

Conditions can also be added to an entire section:

new Section('section-key', 'Section title')
        .withSectionCondition(whenQuestionHasAnswer(questions.q1, 'yes'))
        .addQuestion(questions.q3)
        .addQuestion(questions.q4)
        .withCondition(whenQuestionHasAnswer(questions.q3, 'yes'))

In this case q3 and q4 will only be shown if q1 (from a previous section) is answered 'yes'. Q4 will only show if q1 is answered 'yes' and q3 is answered 'yes'.

Finally, conditions can be added to multiple questions - these can overlap:

new Section('section-key', 'Section title')
        .withSectionCondition(whenQuestionHasAnswer(questions.q1, 'yes'))
        .addQuestion(questions.q2)
        .startMultiQuestionCondition('group-1', whenQuestionHasAnswer(questions.q2, 'valid'))
        .addQuestion(questions.q3)
        .addQuestion(questions.q4)
        .withCondition(whenQuestionHasAnswer(questions.q3, 'yes'))
        .endMultiQuestionCondition('group-1')
        .addQuestion(questions.q5)
        .withCondition(whenQuestionHasAnswer(questions.q4, 'yes'))
        .startMultiQuestionCondition('group-2', whenQuestionHasAnswer(questions.q5, 90))
        .addQuestion(questions.q6)
        .addQuestion(questions.q7)
        .endMultiQuestionCondition('group-2')

These conditions combine with individual conditions and section conditions. In the above scenario, the following applies:

| Question | Section Condition | Group Conditions | Question Conditions | |----------|-------------------|------------------|---------------------| | q2 | q1 = 'yes' | N/A | N/A | | q3 | q1 = 'yes' | q2 = 'valid' | N/A | | q4 | q1 = 'yes' | q2 = 'valid' | q3 = 'yes' | | q5 | q1 = 'yes' | N/A | q4 = 'yes' | | q6 | q1 = 'yes' | q5 = 90 | N/A | | q7 | q1 = 'yes' | q5 = 90 | N/A |

Email Validation

Email validation is provided through the EmailValidator class and EmailQuestion component. The validator uses express-validator's robust email validation with configurable options.

Basic Usage

import { 
	COMPONENT_TYPES,
    createQuestions,
    questionClasses,
    EmailValidator
} from '@planning-inspectorate/dynamic-forms';

// Define question configuration
const questionProps = {
    contactEmail: {
        type: COMPONENT_TYPES.EMAIL,
        title: 'Contact Information',
        question: 'What is your email address?',
        fieldName: 'contactEmail',
        url: 'contact-email',
        label: 'Email address',
        validators: [
            new EmailValidator({
                errorMessage: 'Enter an email address in the correct format, like [email protected]'
            })
        ]
    }
};

// Create questions using the factory function
const questions = createQuestions(questionProps, questionClasses, {});
const emailQuestion = questions.contactEmail;

The EmailQuestion uses the following default input attributes:

  • type="email"
  • spellcheck="false"
  • autocomplete="email"

Advanced Validation Options

For stricter email validation requirements:

const businessEmailValidator = new EmailValidator({
    options: {
        allowDisplayName: false,        // Reject "Name <[email protected]>" format
        requireTld: true,              // Require top-level domain (default: true)
        allowUtf8LocalPart: false,     // Only ASCII characters in local part
        allowIpDomain: false           // Don't allow IP addresses as domain
    },
    errorMessage: 'Enter a valid business email address'
});

Alternative: Single Line Input with Email Attributes

You can also configure a single line input with email-specific attributes:

import {
	COMPONENT_TYPES,
	createQuestions,
	questionClasses,
	EmailValidator
} from '@planning-inspectorate/dynamic-forms';

const questionProps = {
    contactEmail: {
        type: COMPONENT_TYPES.SINGLE_LINE_INPUT,
        title: 'Contact Information',
        question: 'What is your email address?',
        fieldName: 'contactEmail',
        url: 'contact-email',
        label: 'Email address',
        inputAttributes: { type: 'email', spellcheck: 'false' },
        autocomplete: 'email',
        validators: [new EmailValidator()]
    }
};

const questions = createQuestions(questionProps, questionClasses, {});

Contributing

When contributing to this package, ensure changes are generic and not service-specific. Speak to the R&D devs if you are not sure. Prefer configuration over hardcoding values, and ensure the code is well documented.

Commits must follow conventional commits, and the commit types will be used by semantic-release to determine the next version number. For example feat commits will result in a minor version bump, while fix commits will result in a patch version bump. Major version bumps should be reserved for breaking changes, and should be discussed with the R&D team before being made.

The package will be released automatically using semantic-release, on merge to main. This will include a git tag for the release, and publishing to NPM.

Type safety

We use JSDocs to describe the types used. This is helpful for the JavaScript developers, and crucial for this project being compatible with TypeScript. Type declarations for TypeScript users are auto-generated by tsc to create type declarations. So please:

  1. Add JSDocs annotations to your code, use @import or an inline import, i.e. @param {import('#section').Section} section. Avoid a top-level import, e.g. @typedef {import('../validator/base-validator.js')} BaseValidator - this seems to create confusion when the compiler auto-generates the type declarations.
  2. Maintain index.js. If you're adding code that users of this module will import, please make it is being exported in index.js.

Thank you for your co-operation and contributions!

Tests

There are some lightweight tests in the test directory which sets up a basic journey and checks the rendering for each question as well as redirect logic.

When adding a new question type, be sure to add an example question into test/questions.js, and mock answers into test/questions.test.js#mockAnswerBody and test/questions.test.js#mockAnswer. Also, the question should be added to the journey in test/journey.js.

To update any snapshots with rendering changes (or new questions), run node --test --test-update-snapshots.

Components

When implement a new component, extend the base Question class. Common methods to override are:

  • formatAnswerForSummary - used for check-your-answers display
  • getDataToSave - answer data to save
  • addCustomDataToViewModel - customise the view model with extra data/configuration
  • answerForViewModel - customise the view model answer value

Where possible addCustomDataToViewModel and answerForViewModel should be overridden instead of prepQuestionForRendering.