0.8.1 • Published 11 months ago

@luistabotelho/angular-signal-forms v0.8.1

Weekly downloads
-
License
-
Repository
-
Last release
11 months ago

@luistabotelho/angular-signal-forms

A simple library to manage forms using signals. Use the provided signalForm() function to create a new SignalForm.

!NOTE If you have suggestions on how to improve this documentation, leave your feedback here

!NOTE If you want to report a bug or suggest an improvement create an issue here

Changelogs

GitHub Release Changelogs

Topics

signalForm()

The signalForm<T>(initialValue, options?) function is the basis of the library. It takes in a SignalFormDefinition and an optional SignalFormOptions.

This function returns a new instance of SignalForm\ where T is the generic user data.

Examples:

form1 = signalForm({
  field1: {
    initialValue: ""
  }
})

// Defining <T> manually adds validation to SignalFormDefinition, garanteing that the resulting form is a valid DataType
form2 = signalForm<DataType>({
  someField: {
    initialValue: 0
  }
})

signalFormGroup()

The signalFormGroup<T>(initialValue, options?) function works in a similar way as the signalForm() function, except it generates a SignalFormGroup\, which is a wrapper around an array of SignalForm\, allowing to quickly add and remove a SignalForm from the group, as well as get validation, errors and values from it.

The function accepts the same inputs as signalForm(), and those are passed down to the individual members of the group.

Examples:

formGroup = signalFormGroup<DataType>({
  field1: {
    initialValue: ""
  }
})
formGroup2 = signalFormGroup<DataType>({
  field1: {
    initialValue: ""
  }
}, {
  requireTouched: true,
  defaultState: "success"
})

.data

A WritableSignal<Array<SignalForm<T>>>. This signal can be manipulated manually or with help of the other SignalFormGroup methods.

.addItem()

Allows adding a new item to the group. This accepts an optional object of type Partial\ as an input to pre-fill the form fields. If not passed the SignalForm will be created with the initialValue supplied to the group.

.removeItem()

Allows removing an item from the group by passing it's index in the array.

.value()

Returns a signal with the value as Array<T>. This is the same as applying signalFormValue() to all members of the group and joining the result in a single array.

.valid()

Returns a signal of boolean representing the validity of all members of the group. This is the same as applying signalFormValid() on all members of the group.

.errors()

Returns a signal with an Array<string> containing all the errors of the group members in format: "{fieldKey}: {error}". (If two members have the same error the error will appear dupplicated in the array).

Classes

T

T is the generic type that represents the data the form is handling. This is the DataType created by the user, it can be UserModel, CarModel, or anything else.

!WARNING Currently signalForm doesn't support deeply nested structures. Adding support for that is under investigation and input is welcomed.

Example:

interface DataType {
  field1: string
  field1Child: string
  field2: string
  dateField: string
}

K

K is defined as a keyof T, therefore it represents a property of T.

We will now refer to K as a field(s).

SignalForm<T>

The instance of SignalForm generated by signalForm(). The SignalForm instance has the same fields as T, but gives each field some extra properties as seen below:

[K in keyof T]: {
  initialValue: T[K],
  validators: Array<ValidatorFunction<T, T[K]>>,
  currentValue: WritableSignal<T[K]>,
  touched: WritableSignal<boolean>,
  state: Signal<State>,
  valid: Signal<boolean>
}

initialValue

Is constant and represents the initialValue defined by the user.

validators

Is the array of ValidatorFunction defined by the user.

currentValue

A WriteableSignal representing the current value of the field. This is the property that should be bound to the input fields.

[(ngModel)]="form.field1.currentValue"

touched

A WriteableSignal to represent weather the field was touched. Touched has to be manually handled and the recomendation is to bind it to the blur event of the input.

(blur)="form.field1.touched.set(true)"

state

A Signal containing the current input State

valid

A Signal containing a boolean that represents if the field is valid or not based on the validators

SignalFormGroup<T>

SignalFormDefinition<T>

The initial definition accepted by the signalForm() constructor. Each field has two properties: initialValue and validators.

{
  field1: {
    initialValue: "",
    validators: [
      (val) => !val ? new Error("Required") : null,
      (val) => val && !RegExp(/^[A-Z]{1}/).test(val) ? new Error("First letter must be upper case") : null,
      (val) => val && val.length > 10 ? new Error("Must not exceed 10 characters") : null
    ]
  }
}

initialValue

The initial value of the field.

validators

An array of ValidatorFunction. Keep in mind they are run in sequence, and therefore should be defined in order of priority. Ex.: "Required" will always appear before "First letter must be upper case".

SignalFormOptions

A series of options that optionally can be passed to the signalForm() function. Current options are:

{
  requireTouched: true,
  defaultState: 'default',
  errorState: 'error'
}

requireTouched

If true requires the input to be touched before displaying the error state.

Default: true

defaultState

The default state of the input field if all validators pass.

Default: default

errorState

The error state of the input field if any of the validators returns an Error.

Default: error

State

The value of the state property of each field. The State has two properties:

{
    state: string,
    message: string | null
}

state

The current state of the input field. Either defaultState or errorState.

ValidatorFunction<T, K>

A function which takes in two parameters, PropertyValue and FormValue, and returns an Error or null.

Example:

(propertyValue, formValue) => !propertyValue && formValue.otherField.currentValue() != "Some Value" ? new Error("Required") : null

This function will return an error if this fields currentValue is Falsy and otherFields currentValue != "Some Value".

Helper Functions

resetSignalForm()

Accepts an instance of SignalForm.

Sets the currentValue of all fields to the initialValue and sets touched to false, essentially returning the form to it's initial state.

signalFormValue()

Accepts an instance of SignalForm

Returns a Signal containing the updated instance of T.

Example: { "field1": "Input 1 value", "field1Child": "", "field2": "Input 2 value", "dateField": "2024-11-27T21:54" }

signalFormValid()

Accepts an instance of SignalForm

Returns a Signal containing a boolean representing if all fields in the form are valid.

signalFormErrors()

Accepts an instance of SignalForm

Returns a Signal containing an array of all errors returned by the SignalForm instance. This is usefull if you want to display all errors to the user at once.

This does not take into consideration the touched property and will return all errors regardless.

signalFormSetTouched()

Accepts an instance of SignalForm

Will set all fields in the form to touched, making their state go into error if they are invalid even if the user didn't touch them.

This can be used if you want todisplay all fields with errors when the user attempts to submit the form, even if the user didn't interact with the field.

Be aware that this is not required if the requireTouched option was set to false.

Example Component

Typescript

import { Component } from '@angular/core';
import { signalForm, signalFormValue, signalFormValid, resetSignalForm, signalFormSetTouched } from '../../projects/signal-forms/src/public-api';
import { FormsModule } from '@angular/forms';
import { signalFormErrors } from '../../projects/signal-forms/src/lib/helpers/signal-form-errors.helper';
import { CommonModule } from '@angular/common';

interface DataType {
  field1: string
  field1Child: string
  field2: string
  dateField: string
}

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [FormsModule, CommonModule],
  templateUrl: './app.component.html',
  styleUrl: './app.component.css'
})
export class AppComponent {
  form = signalForm<DataType>({
    field1: {
      initialValue: "",
      validators: [
        (val) => !val ? new Error("Required") : null,
        (val) => val && !RegExp(/^[A-Z]{1}/).test(val) ? new Error("First letter must be upper case") : null,
        (val) => val && val.length > 10 ? new Error("Must not exceed 10 characters") : null
      ]
    },
    field1Child: {
      initialValue: "",
      validators: [
        (val, form) => !val && form.field1.currentValue() ? new Error("Required if Field 1 contains a value") : null,
      ]
    },
    field2: {
      initialValue: ""
    },
    dateField: {
      initialValue: new Date().toISOString().slice(0, 16),
      validators: [
        (val) => !val ? new Error("Required") : null,
        (val) => val.slice(0, 10) < new Date().toISOString().slice(0, 10) ? new Error("Date cannot be in the past") : null
      ]
    }
  })

  $formValue = signalFormValue(this.form)
  $formErrors = signalFormErrors(this.form)
  $formValid = signalFormValid(this.form)

  resetForm = () => resetSignalForm(this.form)
  
  submit() {
    signalFormSetTouched(this.form)
    if (!this.$formValid()) {
      return
    }
    // submit to server
  }
}

HTML

<div>
    <label for="field1">Text Input 1</label>
    <br>
    <input 
    id="field1"
    type="text"
    (blur)="form.field1.touched.set(true)"
    [(ngModel)]="form.field1.currentValue">
    <br>
    Touched: {{form.field1.touched()}}
    <br>
    State: {{form.field1.state().state}} : {{form.field1.state().message}}
</div>
<br><br>
<div>
    <label for="field1Child">Text Input 2 Depends on Text Input 1</label>
    <br>
    <input 
    id="field1Child"
    type="text"
    (blur)="form.field1Child.touched.set(true)"
    [(ngModel)]="form.field1Child.currentValue">
    <br>
    Touched: {{form.field1Child.touched()}}
    <br>
    State: {{form.field1Child.state().state}} : {{form.field1Child.state().message}}
</div>
<br><br>
<div>
    <label for="field2">Text Input with no Validations</label>
    <br>
    <input 
    id="field2"
    type="text"
    (blur)="form.field2.touched.set(true)"
    [(ngModel)]="form.field2.currentValue">
    <br>
    Touched: {{form.field2.touched()}}
    <br>
    State: {{form.field2.state().state}} : {{form.field2.state().message}}
</div>
<br><br>
<div>
    <label for="dateField">Date Input</label>
    <br>
    <input 
    id="dateField"
    type="datetime-local"
    (blur)="form.dateField.touched.set(true)"
    [(ngModel)]="form.dateField.currentValue"
    >
    <br>
    Touched: {{form.dateField.touched()}}
    <br>
    State: {{form.dateField.state().state}} : {{form.dateField.state().message}}
</div>
<br><br>
<div>
    Form Valid: {{$formValid()}}
    <br>
    Current Value: {{$formValue() | json}}
    <br>
    All Errors: {{$formErrors() | json}}
    <br><br>
    <button (click)="resetForm()">Reset Form</button>
    <br>
    <button (click)="submit()">Submit</button>
    <br>
    <button (click)="submit()" [disabled]="!$formValid()">Submit If Valid</button>
</div>
0.8.1

11 months ago

0.8.0

11 months ago

0.7.0

11 months ago

0.6.0

11 months ago

0.5.0

11 months ago

0.1.0

11 months ago