Skip to main content

createFormPrimitives()

createFormPrimitives() creates an isolated set of form, field, group, and array factories with shared defaults. Use it to establish field nullability, translated validator messages, and injector inheritance policies once for an application or feature.

The package-level factories infer nullability. Creating a configured set does not change them or any other configured set.

📐 Signature

createFormPrimitives();
createFormPrimitives(options?);

The options object and every property are optional. Omitting nullable uses the same inference as field(): non-nullish defaults do not add null. Set nullable: true to always add null or nullable: false to require nullability in the declared input type. Omitting either injector policy preserves its normal true default:

const defaultForms = createFormPrimitives();
const explicitDefaultForms = createFormPrimitives({});

defaultForms.field(''); // Field<string>
explicitDefaultForms.field(''); // Field<string>

📝 Create non-nullable factories

create-form-primitives.typecheck.ts
import { createFormPrimitives, type FieldNode } from '@ngblocks/form-nodes';

const { form, field, array } = createFormPrimitives({
nullable: false,
validatorMessages: {
required: 'This value is required.',
},
inheritInjector: true,
adoptBindingInjector: true,
});

const profile = form({
username: field(''),
nickname: field.nullable(''),
reference: field.strict('REF-1'),
address: {
city: '',
},
tags: array(field(''), {
initialValue: ['angular'],
}),
}, {});

const username: FieldNode<string> = profile.username;
const nickname: FieldNode<string | null> = profile.nickname;
const reference: FieldNode<string> = profile.reference;

profile();
// Expected output: { username: '', nickname: '', reference: 'REF-1', address: { city: '' }, tags: ['angular'] }

void [username, nickname, reference];

Here, field('') and the city: '' shorthand both produce FieldNode<string>. A local field.nullable('') declaration still produces FieldNode<string | null>.

💬 Configure validator messages

Pass a partial static or reactive catalog to localize built-in validator messages for every node created by the configured factories:

const { form, field } = createFormPrimitives({
validatorMessages: () => ({
required: translate('validation.required'),
minLength: ({ minLength }) => translate('validation.minLength', { minLength }),
}),
});

const profile = form({
username: field('', [required]),
}, {});

A validator's own message has highest priority. An explicit validatorMessages catalog on a form, group, or array overrides the configured default for that subtree. The configured catalog is then considered before Angular provider and process-wide catalogs.

⚙️ Configure injector policies

inheritInjector and adoptBindingInjector can also be defaulted for every created node:

const isolatedForms = createFormPrimitives({
inheritInjector: false,
adoptBindingInjector: false,
});

These options are useful for deliberate ownership boundaries. Their defaults remain true, and a node-level option overrides the configured value. injector is intentionally not a factory default: assigning it to every node would turn inherited ownership into explicit ownership. Pass an injector to the relevant root or boundary node instead.

⚙️ Precedence

An explicit field method takes precedence over the shared default:

const { field } = createFormPrimitives({ nullable: false });

field(''); // Field<string>
field.strict(''); // Field<string>
field.nullable(''); // Field<string | null>

field.nullable() and field.strict() always override the configured default, so local exceptions remain concise in either direction.

Omitting the value or passing null or undefined produces FieldNode<unknown> because there is no concrete initial value from which to infer a future type. An omitted value starts at null, while an explicit undefined is preserved:

const { field } = createFormPrimitives({ nullable: false });

field(); // Field<unknown>; starts at null
field(null); // Field<unknown>; starts at null
field(undefined); // Field<unknown>; starts at undefined

When the future type is known, use field.nullable<T>() to start without a value. A configured field<T>() with nullable: false still requires an initial value:

const nickname = field.nullable<string>();

nickname(); // null

🌳 Shorthands and dynamic nodes

The default applies throughout definitions created by the configured factories. This includes nested object shorthands, children added later with add(), and current or future items created by an array template or factory.

An explicitly created node keeps the policy of the factory that created it:

const nullableForms = createFormPrimitives({ nullable: true });
const nonNullableForms = createFormPrimitives({ nullable: false });
const nullableField = nullableForms.field('');

const profile = nonNullableForms.form({
displayName: nullableField,
}, {});

The configured form() attaches that existing node without changing its value type.

🚀 Application entry point

Applications can expose one configured entry point so declarations share the same policy:

src/app/forms.ts
import { createFormPrimitives } from '@ngblocks/form-nodes';

export const {
form,
field,
group,
array,
} = createFormPrimitives({
nullable: false,
});

Import those factories from the application module when declaring forms.

Empty aggregate declarations

The configured form(), group(), and array() factories can be called without arguments. Forms and groups start as {} and retain defaults for dynamic additions. Arrays start as [] with a configured unknown-valued field template initialized to null, including when the default nullability setting is false. Supply an explicit template for known item types or structure. See empty arrays and empty forms.