Skip to content

Commit

Permalink
feat(Form Component): Restructure to make the form direct redundant &
Browse files Browse the repository at this point in the history
 remove it

feat(Demo): Restructure completely to use the remapping strategy

feat(Readme): Fix the readme to match the new pattern
  • Loading branch information
zak-cloudnc committed Mar 6, 2019
1 parent 5d94e8c commit de4df2b
Show file tree
Hide file tree
Showing 25 changed files with 164 additions and 197 deletions.
157 changes: 91 additions & 66 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,93 +9,69 @@ Works well with polymorphic data structures.

## Install

Install the [npm-package](https://www.npmjs.com/package/ngx-row-accordion):

`yarn add ngx-sub-form`

## Setup

```diff
+ import { NgxSubFormModule } from 'ngx-sub-form';

@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
+ NgxSubFormModule
],
bootstrap: [AppComponent],
})
export class AppModule {}
```
Install the [npm-package](https://www.npmjs.com/package/ngx-sub-form):

## Usage

_Before we get started with how to use the library and give some examples, a complete demo is available on [this repo](https://github.com/cloudnc/ngx-sub-form), within the `src` folder.
Demo is built around a concept of galatic sales. You can sell either Droids (Protocol, Medical, Astromech, Assassin) or Vehicules (Spaceship, Speeder). This will also be used for the following examples_.
Demo is built around a concept of galactic sales. You can sell either Droids (Protocol, Medical, Astromech, Assassin) or Vehicles (Spaceship, Speeder). This will also be used for the following examples_.

### First component level

Within the component where the form will be handled, we have to define the top level structure of the form (as you'd normally do).
Within the component where the form will be handled, we have to define the top level structure of the form, with _each
polymorphic type having it's own form control_

```ts
public sellForm: FormGroup = new FormGroup({
sell: new FormControl(null, { validators: [Validators.required] }),
public listingForm: FormGroup = new FormGroup({
droidListing: new FormControl(null, Validators.required),
vehicleListing: new FormControl(null, Validators.required),
listingType: new FormControl(null, Validators.required)
});
```

Then we need to create a separated `FormControl` to select the sell's type:

```ts
public selectSellType: FormControl = new FormControl();
```

and give access to our `enum` from the component:

```ts
public SellType = SellType;
public ListingType = ListingType;
```

Just as a sidenote here, here's the `SellType` enum:
Just as a sidenote here, here's the `ListingType` enum:

```ts
export enum SellType {
VEHICULE = 'Vehicule',
export enum ListingType {
VEHICLE = 'Vehicle',
DROID = 'Droid',
}
```

Then, within the `.html` we create a `select` tag to choose between the 2 types:
Then, within the `.component.html` we create a `select` tag to choose between the 2 types:

```html
<select [formControl]="selectSellType">
<option *ngFor="let sellType of SellType | keyvalue" [value]="sellType.value">
{{ sellType.value }}
<select formControlName="listingType">
<option *ngFor="let listingType of ListingType | keyvalue" [value]="listingType.value">
{{ listingType.value }}
</option>
</select>
```

Now we need to create, based on the sell's type, either a `DroidSellComponent` or a `VehiculeSellComponent`:
Now we need to create, based on the listing type, either a `DroidListingComponent` or a `VehicleListingComponent`:

```html
<form [formGroup]="sellForm">
<div [ngSwitch]="selectSellType.value" ngxSubFormOptions>
<app-droid-sell *ngSwitchCase="SellType.DROID" ngxSubFormOption formControlName="sell"></app-droid-sell>

<app-vehicule-sell *ngSwitchCase="SellType.VEHICULE" ngxSubFormOption formControlName="sell"></app-vehicule-sell>
<form [formGroup]="listingForm">
<div [ngSwitch]="listingForm.get(formControlNames.listingType).value">
<app-droid-listing *ngSwitchCase="ListingType.DROID" formControlName="droidListing"></app-droid-listing>
<app-vehicle-listing *ngSwitchCase="ListingType.VEHICLE" formControlName="vehicleListing"></app-vehicle-listing>
</div>

<button mat-raised-button (click)="upsertSell(sellForm.get('sell').value)" [disabled]="sellForm.invalid">
<button mat-raised-button (click)="upsertListing(listingForm.value)" [disabled]="listingForm.invalid">
Upsert
</button>
</form>
```

3 things to notice above:
One thing to notice above:

- `ngxSubFormOptions` _(will explain later)_
- `ngxSubFormOption` _(will explain later)_
- `formControlName="sell"` our sub form component **IS** a custom `ControlValueAccessor` and let us bind our component to a `formControlName` as we would with an input.
- `formControlName="droidListing"` our sub form component **IS** a custom `ControlValueAccessor` and let us bind our component to a `formControlName` as we would with an input.

### Second component level

Expand All @@ -107,49 +83,98 @@ Add required providers using the utility function `subformComponentProviders`:
+import { subformComponentProviders } from 'ngx-sub-form';

@Component({
selector: 'app-vehicule-sell',
templateUrl: './vehicule-sell.component.html',
styleUrls: ['./vehicule-sell.component.scss'],
+ providers: subformComponentProviders(VehiculeSellComponent),
selector: 'app-vehicle-listing',
templateUrl: './vehicle-listing.component.html',
styleUrls: ['./vehicle-listing.component.scss'],
+ providers: subformComponentProviders(VehicleListingComponent),
})
export class VehiculeSellComponent {}
export class VehicleListingComponent {}
```

Make your original class extends `NgxSubFormComponent`:
Make your original class extend `NgxSubFormComponent` _or_ `NgxSubFormRemapComponent` if you need to remap the data (see below):

```diff
-import { subformComponentProviders } from 'ngx-sub-form';
+import { subformComponentProviders, NgxSubFormComponent } from 'ngx-sub-form';

-export class VehiculeSellComponent {}
+export class VehiculeSellComponent extends NgxSubFormComponent {}
-export class VehicleListingComponent {}
+export class VehicleListingComponent extends NgxSubFormComponent {}
```

Define the controls of your form:

```ts
private controls: Controls<VehiculeSell> = {
id: new FormControl(this.uuidService.generate(), { validators: [Validators.required] }),
price: new FormControl(null, { validators: [Validators.required] }),
protected formControls: Controls<VehicleListing> = {
id: new FormControl(this.uuidService.generate(), Validators.required),
price: new FormControl(null, Validators.required),
};

public formGroup: FormGroup = new FormGroup(this.controls);

public controlsNames: ControlsNames<VehiculeSell> = getControlsNames(this.controls);
```

_Simplified from the original example into src folder to keep the example as minimal as possible._

As you know, [Angular reactive forms are not strongly typed](https://github.com/angular/angular/issues/13721). We're providing an interface (`Controls<T>`) to at least set the correct names within the form (but it will not help you when using `form.get('...').value`). It is still very useful and when making a refactor if your data structure changes and do not match the form structure Typescript compilation will fail.

We also provide a utility function called `getControlsNames` which you can pass your `controls` to. This will let you define your `formControlName`s in the view in a safe way thanks to AoT. If you update your main interface, your form control but you forget about the view, you'll get an error.
The NgxFormComponent base class automatically extracts your form control names and exposes them as a public member on `formControlNames`

Then within the `.html`:
Then within the `.html`, you can reference them like so:

```html
<fieldset [formGroup]="formGroup" class="container">
<input type="text" placeholder="ID" [formControlName]="controlsNames.id" />
<input type="text" placeholder="ID" [formControlName]="formControlNames.id" />

<input type="number" placeholder="Price" [formControlName]="controlsNames.price" />
<input type="number" placeholder="Price" [formControlName]="formControlNames.price" />
</fieldset>
```

### Remapping Data

It is a frequent pattern to have the data that you're trying to modify in a format that is incovenient to the angular
forms structural constraints. For this reason, ngx-form-component offers a separate extended class `NgxSubFormRemapComponent`
which will require you to define two interfaces - one to model the data going in to the form (which will be applied
internally as `form.setValue()`), and the other to describe the interface of the value that will be set on the form.

**You're always better off making your data structure better suit Angular forms, than abusing forms to fit your data pattern**

For a complete example of this see `https://github.com/cloudnc/ngx-sub-form/blob/master/src/app/main/listing/vehicle-listing/vehicle-product.component.ts` (repeated below):

```ts
interface OneVehicleForm {
speeder: Speeder;
spaceship: Spaceship;
vehicleType: VehicleType;
}

@Component({
selector: 'app-vehicle-product',
templateUrl: './vehicle-product.component.html',
styleUrls: ['./vehicle-product.component.scss'],
providers: subformComponentProviders(VehicleProductComponent),
})
export class VehicleProductComponent extends NgxSubFormRemapComponent<OneVehicle, OneVehicleForm> {
protected formControls: Controls<OneVehicleForm> = {
speeder: new FormControl(null),
spaceship: new FormControl(null),
vehicleType: new FormControl(null, { validators: [Validators.required] }),
};

public VehicleType = VehicleType;

protected transformToFormGroup(obj: OneVehicle): OneVehicleForm {
return {
speeder: obj.vehicleType === VehicleType.SPEEDER ? obj : null,
spaceship: obj.vehicleType === VehicleType.SPACESHIP ? obj : null,
vehicleType: obj.vehicleType,
};
}

protected transformFromFormGroup(formValue: OneVehicleForm): OneVehicle {
switch (formValue.vehicleType) {
case VehicleType.SPEEDER:
return formValue.speeder;
case VehicleType.SPACESHIP:
return formValue.spaceship;
}
}
}
```
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"lib:build:prod": "yarn run ng build --project ngx-sub-form",
"lib:build:watch": "yarn run lib:build:prod --watch",
"------------------ Quick Commands ------------------": "",
"lint:fix": "yarn demo:lint --fix && yarn prettier:write",
"lint:fix": "yarn demo:lint:fix && yarn prettier:write",
"semantic-release": "semantic-release"
},
"private": true,
Expand Down
38 changes: 0 additions & 38 deletions projects/ngx-sub-form/src/lib/ngx-sub-form-options.directive.ts

This file was deleted.

55 changes: 17 additions & 38 deletions projects/ngx-sub-form/src/lib/ngx-sub-form.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { Subscription } from 'rxjs';
import { delay, tap } from 'rxjs/operators';
import { Controls, ControlsNames, getControlsNames } from './ngx-sub-form-utils';

export abstract class NgxSubFormComponent<ControlInterface, FormInterface = ControlInterface> implements ControlValueAccessor, Validator, OnDestroy {
export abstract class NgxSubFormComponent<ControlInterface, FormInterface = ControlInterface>
implements ControlValueAccessor, Validator, OnDestroy {
protected formControls: Controls<FormInterface>;

public get formControlNames(): ControlsNames<FormInterface> {
Expand All @@ -19,10 +20,6 @@ export abstract class NgxSubFormComponent<ControlInterface, FormInterface = Cont
return this.fg;
}

// this should not be handled directly by the developer
// instead, please use the provided directives
public resetValueOnDestroy = true;

protected onChange: Function;
protected onTouched: Function;

Expand All @@ -32,24 +29,12 @@ export abstract class NgxSubFormComponent<ControlInterface, FormInterface = Cont
public formControlName: string;

public validate(ctrl: AbstractControl): ValidationErrors | null {
// @hack see bellow where defining this.formGroup to null
if (!this.formGroup || this.formGroup.valid) {
// @hack see below where defining this.formGroup to null
if (!this.formGroup || this.formGroup.valid || this.formGroup.pristine) {
return null;
}

const errors: ValidationErrors = {};

for (const key in this.formGroup.controls) {
if (this.formGroup.controls.hasOwnProperty(key)) {
const control = this.formGroup.controls[key];

if (!control.valid) {
errors[key] = control.errors;
}
}
}

return errors;
return { [this.formControlName]: true };
}

public ngOnDestroy(): void {
Expand All @@ -62,26 +47,19 @@ export abstract class NgxSubFormComponent<ControlInterface, FormInterface = Cont
this.subscription.unsubscribe();
}

// if we're in the case of a form where we need to choose between different types
// for ex with a switch case, we do not want to reset the value for the following reason
// form is patched at the root level (result of an API call for example)
// sub component handle what to display based on the type
// type1 was displayed before but type2 has just been patched instead
// component of type1 is being destroyed, and removing the new data we just patched into the form
// to avoid that we provide directives which are setting `resetValueOnDestroy` to false when needed
if (this.onChange && this.resetValueOnDestroy) {
if (this.onChange) {
this.onChange(null);
}
}

public writeValue(obj: any): void {
if (obj) {
if (!!this.formGroup) {
this.formGroup.patchValue(this.transformToFormGroup(obj), {
// required to be true otherwise it's not possible
// to be warned when the form is updated
emitEvent: true,
this.formGroup.setValue(this.transformToFormGroup(obj), {
emitEvent: false,
});

this.formGroup.markAsPristine();
}
} else {
// @todo clear form?
Expand All @@ -94,13 +72,13 @@ export abstract class NgxSubFormComponent<ControlInterface, FormInterface = Cont
// that method can be overridden if the
// shape of the form needs to be modified
protected transformToFormGroup(obj: ControlInterface): FormInterface {
return obj as any as FormInterface;
return (obj as any) as FormInterface;
}

// that method can be overriden if the
// that method can be overridden if the
// shape of the form needs to be modified
protected transformFromFormGroup(formValue: FormInterface): ControlInterface {
return formValue as any as ControlInterface;
return (formValue as any) as ControlInterface;
}

public registerOnChange(fn: any): void {
Expand Down Expand Up @@ -128,9 +106,10 @@ export abstract class NgxSubFormComponent<ControlInterface, FormInterface = Cont
}
}

export abstract class NgxSubFormRemapComponent<ControlInterface, FormInterface> extends NgxSubFormComponent<ControlInterface, FormInterface> {
export abstract class NgxSubFormRemapComponent<ControlInterface, FormInterface> extends NgxSubFormComponent<
ControlInterface,
FormInterface
> {
protected abstract transformToFormGroup(obj: ControlInterface): FormInterface;
protected abstract transformFromFormGroup(formValue: FormInterface): ControlInterface;
}


Loading

0 comments on commit de4df2b

Please sign in to comment.