Start creating extendable reactive forms with any framework today.
Goal of margarita form is to help you to create, extend and validate forms easily. Margarita form provides methods to conditionally enable, transform and observe your form, it's value, state and everything related to it.
npm install @margarita-form/react
→ View React examplenpm install @margarita-form/angular
→ View Angular examplenpm install @margarita-form/core
→ View HTML & JS examplenpm install @margarita-form/core
→ View TypeScript exampleDefine your schema. Schema is the foundation for your form. Schema is an array of fields and with each field you can define controls name, type, state, behavior and validations. From schema / fields margarita form will create controls that then are used in your code to control form's value, state and content.
Connect your ui components to form controls. Margarita form does not define how your form should look like. You easily connect any UI components to the form controls and create dynamic forms without hassle.
Handle form submission. Saving form data is easy. Before user can submit the form, margarita form validates all active controls. If form is valid, a callback you defined get run with a form context and saving data to any target can be handled with native Promise api.
npm install @margarita-form/react
Component / readme-example.tsx
import { useMargaritaForm, MargaritaFormField } from '@margarita-form/react';
// Create value and custom field types
interface MyFormValue {
name?: string;
message?: string;
}
interface MyFormField extends MargaritaFormField {
type: 'text' | 'textarea' | 'number' | 'email' | 'password' | 'tel' | 'url' | 'date';
title: string;
}
type RootField = MargaritaFormField<{ value: MyFormValue; fields: MyFormField }>;
// Create form component
export function CustomForm() {
// Initialize form
const form = useMargaritaForm<RootField>({
name: 'my-form', // Name for each form should be unique
fields: [
{
name: 'name',
title: 'Full name',
type: 'text',
validation: { required: true },
attributes: { placeholder: 'John Doe' }, // Attributes are passed to the input
},
{
name: 'email',
title: 'Email',
type: 'email',
validation: { required: true },
attributes: { placeholder: 'john.doe@email.com' },
},
{
name: 'message',
title: 'Message',
type: 'textarea',
validation: { required: true },
attributes: { placeholder: 'Lorem ipsum' }, // Attributes are passed to the input
},
],
handleSubmit: {
// This required function is called when the form is valid
valid: async ({ value }) => {
console.log('Valid submit', { value });
alert(JSON.stringify(value, null, 2));
},
// This optional function is called when the form is invalid
invalid: async ({ control, value }) => {
const { allErrors } = control.state;
console.log('Form is not valid!', { value, allErrors });
},
},
config: {
handleSuccesfullSubmit: 'reset', // Change what happens when form is successfully submitted
},
});
return (
<div className="form-wrapper">
{/* Render the current value */}
<h2>Current value</h2>
<code>{JSON.stringify(form.value)}</code>
{/* Render the form */}
<h2>Form</h2>
<form ref={form.setRef}>
{/* Map controls and connect to inputs */}
{form.controls.map((control) => {
const uid = control.name + '-' + control.key;
// Use switch to render different types of fields
switch (control.field.type) {
case 'textarea':
return (
<div className="field-wrapper" key={uid}>
<label htmlFor={uid}>{control.field.title}</label>
<textarea id={uid} name={uid} ref={control.setRef} />
</div>
);
default:
return (
<div className="field-wrapper" key={uid}>
<label htmlFor={uid}>{control.field.title}</label>
<input id={uid} name={uid} type={control.field.type} ref={control.setRef} />
</div>
);
}
})}
{/* Submit the form */}
<button type="submit">Submit</button>
</form>
</div>
);
}
npm install @margarita-form/angular
Component / readme-example.component.ts
import { Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MargaritaFormField, MargaritaFormModule, MargaritaFormService } from '@margarita-form/angular';
// Create value and custom field types
interface MyFormValue {
name?: string;
message?: string;
}
interface MyFormField extends MargaritaFormField<{ fields: MyFormField }> {
type: 'text' | 'textarea' | 'number' | 'email' | 'password' | 'tel' | 'url' | 'date';
title: string;
}
type RootField = MargaritaFormField<{ value: MyFormValue; fields: MyFormField }>;
// Create form component
@Component({
selector: 'app-readme-example',
standalone: true,
imports: [CommonModule, MargaritaFormModule],
styleUrl: './readme-example.component.scss',
templateUrl: './readme-example.component.html',
})
export class ReadmeExampleComponent {
private formService = inject(MargaritaFormService);
// Initialize form
public form = this.formService.createForm<RootField>({
name: 'my-form', // Name for each form should be unique
fields: [
{
name: 'name',
title: 'Full name',
type: 'text',
validation: { required: true },
attributes: { placeholder: 'John Doe' }, // Attributes are passed to the input
},
{
name: 'email',
title: 'Email',
type: 'email',
validation: { required: true },
attributes: { placeholder: 'john.doe@email.com' },
},
{
name: 'message',
title: 'Message',
type: 'textarea',
validation: { required: true },
attributes: { placeholder: 'Lorem ipsum' }, // Attributes are passed to the input
},
],
handleSubmit: {
// This required function is called when the form is valid
valid: async ({ value }) => {
console.log('Valid submit', { value });
alert(JSON.stringify(value, null, 2));
},
// This optional function is called when the form is invalid
invalid: async ({ control, value }) => {
const { allErrors } = control.state;
console.log('Form is not valid!', { value, allErrors });
},
},
config: {
handleSuccesfullSubmit: 'reset', // Change what happens when form is successfully submitted
},
});
}
Template / readme-example.component.html
<!-- Render the current value -->
<h2>Current value</h2>
<code>{{ form.value | json }}</code>
<!-- Render the form -->
<h2>Form</h2>
<form *ngIf="form" [mfControl]="form">
<!-- Map controls and connect to inputs -->
<div class="field-wrapper" *ngFor="let control of form.controls">
<label [for]="control.uid">{{ control.field.title }}</label>
<!-- Use switch to render different types of fields -->
<ng-container [ngSwitch]="control.field.type">
<textarea *ngSwitchCase="'textarea'" [id]="control.uid" [name]="control.uid" type="text" [mfControl]="control"></textarea>
<input *ngSwitchDefault [id]="control.uid" [name]="control.uid" [type]="control.field.type" [mfControl]="control" />
</ng-container>
</div>
<!-- Submit the form -->
<button type="submit">Submit</button>
</form>
<script src="https://unpkg.com/@margarita-form/core/margarita-form.min.js"></script>
or for specific version
<script src="https://unpkg.com/@margarita-form/core@VERSION/margarita-form.min.js"></script>
Example file / vanilla-html-js.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Margarita Form / Vanilla HTML anf JS example</title>
<script src="https://unpkg.com/@margarita-form/core/margarita-form.min.js"></script>
<link rel="stylesheet" href="/styles-vanilla.css" />
</head>
<body>
<header>
<h1>Margarita Form / Vanilla HTML anf JS example</h1>
</header>
<main>
<div class="form-wrapper">
<!-- Render the current value -->
<h2>Current value</h2>
<code id="current-value"></code>
<!-- Render the form -->
<h2>Form</h2>
<form *ngIf="form" [mfControl]="form">
<!-- Map controls and connect to inputs -->
<div class="form-fields-container">
<!-- Fields are appended here -->
</div>
<!-- Submit the form -->
<button type="submit">Submit</button>
</form>
</div>
</main>
<script>
const form = MargaritaForm.createMargaritaForm({
name: 'my-form', // Name for each form should be unique
fields: [
{
name: 'name',
title: 'Full name',
type: 'text',
validation: { required: true },
attributes: { placeholder: 'John Doe' }, // Attributes are passed to the input
},
{
name: 'email',
title: 'Email',
type: 'email',
validation: { required: true },
attributes: { placeholder: 'john.doe@email.com' },
},
{
name: 'message',
title: 'Message',
type: 'textarea',
validation: { required: true },
attributes: { placeholder: 'Lorem ipsum' }, // Attributes are passed to the input
},
],
handleSubmit: {
// This required function is called when the form is valid
valid: async ({ value }) => {
console.log('Valid submit', { value });
alert(`Form is valid!\n${JSON.stringify(value, null, 2)}`);
},
// This optional function is called when the form is invalid
invalid: async ({ control, value }) => {
const { allErrors } = control.state;
alert(`Form is not valid!\n${JSON.stringify(allErrors, null, 2)}`);
console.log('Form is not valid!', { value, allErrors });
},
},
config: {
handleSuccesfullSubmit: 'reset', // Change what happens when form is successfully submitted
},
onValueChanges: ({ value }) => {
const currentValueEl = document.querySelector('#current-value');
if (!currentValueEl) throw new Error('Element with id "current-value" was not found');
currentValueEl.innerText = JSON.stringify(value);
},
});
const formEl = document.querySelector('form');
if (!formEl) throw new Error('Element with tag "form" was not found');
form.setRef(formEl);
const getInputEl = (control) => {
switch (control.field.type) {
case 'textarea':
return document.createElement('textarea');
default:
return document.createElement('input');
}
};
const formFieldsContainerEl = formEl.querySelector('.form-fields-container');
if (!formFieldsContainerEl) throw new Error('Element with class "form-fields-container" was not found');
form.controls.forEach((control) => {
const fieldEl = document.createElement('div');
fieldEl.classList.add('field-wrapper');
const labelEl = document.createElement('label');
labelEl.setAttribute('for', control.uid);
labelEl.innerText = `${control.field.title}:`;
const inputEl = getInputEl(control);
inputEl.setAttribute('id', control.uid);
inputEl.setAttribute('name', control.name);
inputEl.setAttribute('type', control.field.type);
formFieldsContainerEl.appendChild(fieldEl);
fieldEl.appendChild(labelEl);
fieldEl.appendChild(inputEl);
control.setRef(inputEl);
});
</script>
</body>
</html>
The plan is to make more framework specific libraries but even now you can use @margarita-form/core to implement the form logic into any project!
npm install @margarita-form/core
Example TypeScript / vanilla-typescript.ts
import { MargaritaFormControl, MargaritaFormField, createMargaritaForm } from '@margarita-form/core';
interface MyFormValue {
name?: string;
message?: string;
}
interface MyFormField extends MargaritaFormField {
type: 'text' | 'textarea' | 'number' | 'email' | 'password' | 'tel' | 'url' | 'date';
title: string;
}
type RootField = MargaritaFormField<{ value: MyFormValue; fields: MyFormField }>;
type MyFormControl = MargaritaFormControl<MyFormField>;
const form = createMargaritaForm<RootField>({
name: 'my-form', // Name for each form should be unique
fields: [
{
name: 'name',
title: 'Full name',
type: 'text',
validation: { required: true },
attributes: { placeholder: 'John Doe' }, // Attributes are passed to the input
},
{
name: 'email',
title: 'Email',
type: 'email',
validation: { required: true },
attributes: { placeholder: 'john.doe@email.com' },
},
{
name: 'message',
title: 'Message',
type: 'textarea',
validation: { required: true },
attributes: { placeholder: 'Lorem ipsum' }, // Attributes are passed to the input
},
],
handleSubmit: {
// This required function is called when the form is valid
valid: async ({ value }) => {
console.log('Valid submit', { value });
alert(`Form is valid!\n${JSON.stringify(value, null, 2)}`);
},
// This optional function is called when the form is invalid
invalid: async ({ control, value }) => {
const { allErrors } = control.state;
alert(`Form is not valid!\n${JSON.stringify(allErrors, null, 2)}`);
console.log('Form is not valid!', { value, allErrors });
},
},
config: {
handleSuccesfullSubmit: 'reset', // Change what happens when form is successfully submitted
},
onValueChanges: ({ value }) => {
const currentValueEl = document.querySelector<HTMLElement>('#current-value');
if (!currentValueEl) throw new Error('Element with id "current-value" was not found');
currentValueEl.innerText = JSON.stringify(value);
},
});
const formEl = document.querySelector('form');
if (!formEl) throw new Error('Element with tag "form" was not found');
form.setRef(formEl);
const getInputEl = (control: MyFormControl) => {
switch (control.field.type) {
case 'textarea':
return document.createElement('textarea');
default:
return document.createElement('input');
}
};
const formFieldsContainerEl = formEl.querySelector('.form-fields-container');
if (!formFieldsContainerEl) throw new Error('Element with class "form-fields-container" was not found');
form.controls.forEach((control) => {
const fieldEl = document.createElement('div');
fieldEl.classList.add('field-wrapper');
const labelEl = document.createElement('label');
labelEl.setAttribute('for', control.uid);
labelEl.innerText = `${control.field.title}:`;
const inputEl = getInputEl(control);
inputEl.setAttribute('id', control.uid);
inputEl.setAttribute('name', control.name);
inputEl.setAttribute('type', control.field.type);
formFieldsContainerEl.appendChild(fieldEl);
fieldEl.appendChild(labelEl);
fieldEl.appendChild(inputEl);
control.setRef(inputEl);
});
Example HTML / vanilla-typescript.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Margarita Form / Vanilla TypeScript example</title>
<link rel="stylesheet" href="/styles-vanilla.css" />
</head>
<body>
<header>
<h1>Margarita Form / Vanilla TypeScript example</h1>
</header>
<main>
<div class="form-wrapper">
<!-- Render the current value -->
<h2>Current value</h2>
<code id="current-value"></code>
<!-- Render the form -->
<h2>Form</h2>
<form *ngIf="form" [mfControl]="form">
<!-- Map controls and connect to inputs -->
<div class="form-fields-container">
<!-- Fields are appended here -->
</div>
<!-- Submit the form -->
<button type="submit">Submit</button>
</form>
</div>
</main>
<script type="module" src="/app/vanilla-typescript/vanilla-typescript.ts"></script>
</body>
</html>
View full example app with Vite server
Proper documentation is coming soon! For now you can check reference documentation and the following examples!