npm package: @marcura/dadesk-ui-form
Description: Library which helps manage forms in an organized manner in DA-Desk projects.
Latest version: 2.0.2

# @marcura/dadesk-ui-form

Library which helps manage forms in an organized manner in DA-Desk projects.

This version is expected to work with Angular 15.x.

## FormTools

A utility class for working with Angular forms. Provides static methods to handle form control states,
values, and validators.

## Form

The `Form` class is a comprehensive utility for managing form controls and their states.
It provides methods to add, remove, attach, and detach controls, as well as to enable or disable them.
The class also offers observables to track the state of the form and its controls.

In general, we have two main states of Control:

- `registered`: control is known to the form, but may be not attached to the Angular FormGroup
- `attached`: control is attached to the Angular FormGroup

So this way we have a Control's lifecycle like below:

- creation
- registration
- attachment

With optional phases of:

- detachment
- unregistration

Once control is detached it can be attached again without knowing its' name, because it's still registered.

We have shortcuts performing more than one operation, which will be mostly used in common scenarios i.e.:

`createControl()` = creation + register() + attach():

```ts
public readonly name: Forms.Control<string> = this.createControl('name', '');`
```

- `addControl()` = register() + attach()

```ts
public readonly age: Forms.Control<number | null> = this.addControl('age', new FormControl());`
```

But, if needed, we can perform each operation separately. I.e. we can register a control without attaching it to the form:

```ts
public readonly document: Forms.Control<string> = this.registerControl('document', nonNullControl(''));`
```

For adding subforms and form arrays, we have dedicated methods:

- `addForm()`
- `addArray()`

This class provides also `setChildEnabled()` and `setChildDisabled()` methods,
which should be used to enable/disable controls with attaching/detaching
them at the same time without a risk of accidental enabling of them while
enabling the whole form.

```ts
export class PersonForm extends Form {
  public readonly name: FormControl<string> = this.createControl('name', '', Validators.required);
  public readonly age: FormControl<number> = this.createControl('age', 0, Validators.min(0));
  public readonly document: FormControl<string> = this.createControl('document', '');
  public readonly address: AddressForm = this.addForm('address', new AddressForm());
  public readonly books: FormArray<BookForm> = this.addArray('books', new FormArray());

  public readonly saveWorker: Worker = this.observer.add(new Worker());
  public readonly schema$: State<PersonSchema | null> = this.observer.add(new State<PersonSchema | null>(null));
  public readonly isSubmitDisabled$: Snapshot<boolean> = this.observer.add(
    Snapshot.from(false, Rx.atLeastOneIsTrue([this.isPending$, this.isDisabled$]))
  );

  public constructor() {
    super();
    this.listen(this.saveWorker.isWorking$, this.setDisabled);

    this.listen(FormTools.getValue$(this.age).pipe(map((age) => age >= 18)), (hasDocument) => {
      this.setChildEnabled(this.document)(hasDocument);
      FormTools.setValidators(this.document, hasDocument ? [Validators.required, Validators.minLength(3)] : null);
    });

    this.listen(this.schema$, (schema) => {
      this.name.setValue(schema?.name ?? '');
      this.age.setValue(schema?.age ?? 0);
      this.document.setValue(schema?.document ?? '');
      this.address.schema.set(schema?.address ?? null);
      this.books.updateItemsByValues({
        values: schema?.books ?? [],
        hasValue: (form, value) => form.schema.get()?.id === value.id,
        create: (value) => new BookForm(value),
        update: (form, value) => form.schema.set(value),
        destroy: (form) => form.destroy(),
      });
    });
  }

  public toSchema(): PersonSchema {
    return {
      id: this.schema$.get()?.id,
      name: this.name.value,
      age: this.age.value,
      document: this.document.value,
      address: this.address.toSchema(),
      books: this.books.items.map((form) => form.toSchema()),
    };
  }

  public addBook(): void {
    this.books.push(new BookForm());
  }

  public removeBook(book: BookForm): void {
    this.books.removeItem(book);
  }
}
```

## FormControlArray

A specialized form array class that extends `AbstractFormArray`.
This class is designed to handle an array of form controls of type `TControl`.

```ts
import {FormControlArray} from '@marcura/dadesk-ui-form';

const formArray: FormControlArray<FormControl<number>> = new FormControlArray<FormControl<number>>();
formArray.items$.subscribe((items) => console.log(items));
formArray.push(new FormControl(123));
formArray.removeAt(0);
```

## FormArray

A specialized form array class that extends `AbstractFormArray`.
This class is designed to handle an array of form groups of type `TForm`.

```ts
import {Form, FormArray} from '@marcura/dadesk-ui-form';

class MyForm extends Form {
  public readonly name: FormControl<string> = this.createControl('');
}

const formArray: FormArray<MyForm> = new FormArray<MyForm>();
formArray.items$.subscribe((items) => console.log(items));
formArray.push(new MyForm());
formArray.removeAt(0);
```

## SubmittableForm

Represents a form that can be submitted and includes a worker to handle asynchronous tasks.
Extends the base `Form` class.

## FormService

Service to handle form-related operations such as validation and focus management.

#### `FormService.validate<TForm extends Form>(form: TForm, scrollToInvalid: boolean = true): Promise<boolean>`

Validates the form and returns whether it is valid. If any invalid control found, it automatically scrolls to it.

```ts
import {Form} from '@marcura/dadesk-ui-form';

const form = new Form();
form.addControl('name', new FormControl('', Validators.required));
form.addControl('email', new FormControl('', Validators.email));

const isValid: boolean = await formService.validate(form);
if (isValid) {
  console.log('Form is valid');
} else {
  console.log('Form is invalid');
}
```

#### `FormService.requestFocus(control: NgAbstractControl)`

Requests to focus on the specified control.

#### `FormService.getInvalidControl(formGroup: NgFormGroup): AbstractControl | null`

Returns the first invalid control in the form group.
If no invalid control found, it returns `null`.

```ts
const form = new FormGroup({
  name: new FormControl('', Validators.required),
  email: new FormControl('', Validators.email),
});

const invalidControl: AbstractControl | null = formService.getInvalidControl(form);
if (invalidControl) {
  console.log('Invalid control found:', invalidControl);
} else {
  console.log('No invalid control found');
}
```