Skip to content

Commit

Permalink
Merge pull request #8 from cloudnc/feat/form-mapping-simplification
Browse files Browse the repository at this point in the history
Feat/form mapping simplification
  • Loading branch information
maxime1992 authored Mar 6, 2019
2 parents 92293dc + de4df2b commit b79d457
Show file tree
Hide file tree
Showing 38 changed files with 482 additions and 584 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;
}
}
}
```
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
"demo:lint:fix": "yarn run demo:lint:check --fix",
"------------------ LIB ngx-sub-form ------------------": "",
"lib:build:prod": "yarn run ng build --project ngx-sub-form",
"lib:build:watch": "yarn run lib:ngx-sub-form:build --watch",
"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.

2 changes: 1 addition & 1 deletion projects/ngx-sub-form/src/lib/ngx-sub-form-tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ import { NgxSubFormComponent } from './ngx-sub-form.component';
// see https://github.com/angular/angular/issues/8277#issuecomment-263029485
// this basically allows us to access the host component
// from a directive without knowing the type of the component at run time
export const SUB_FORM_COMPONENT_TOKEN = new InjectionToken<NgxSubFormComponent>('NgxSubFormComponentToken');
export const SUB_FORM_COMPONENT_TOKEN = new InjectionToken<NgxSubFormComponent<any>>('NgxSubFormComponentToken');
Loading

0 comments on commit b79d457

Please sign in to comment.