Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/emit initial value on init #296

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost:4200",
"webRoot": "${workspaceFolder}"
}
]
}
19 changes: 10 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,16 @@ This function takes as parameter a configuration object and returns an object re

<!-- ❌✅ -->

| Key | Type | Optional or required | Root form | Sub form | What is it for? |
| ----------------------- | --------------------------------------------------------------------------- | -------------------- | --------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `formType` | `FormType` | Required | ✅ | ✅ | Defines the type of the form. Can either be `FormType.ROOT` or `FormType.SUB` |
| `disabled$` | `Observable<boolean>` | Required | ✅ | ❌ | When this observable emits `true`, the whole form (including the root form and all the sub forms) will be disabled |
| `input$` | `Observable<ControlInterface \| undefined>` | Required | ✅ | ❌ | A root form is a component in between the parent passing raw data and the form itself. This property is an observable that you must provide which will be used behind the scenes to update for you the form values |
| `output$` | `Subject<ControlInterface>` | Required | ✅ | ❌ | A root form is a component in between the parent passing raw data and the form itself. This property is an observable that you must provide which will be used behind the scenes to broadcast the form value to the parent when it changes |
| `manualSave$` | `Observable<void>` | Optional | ✅ | ❌ | By default a root form will automatically broadcast all the form updates (through the `output$`) as soon as there's a change. If you wish to "save" the form only when you click on a save button for example, you can create a subject on your side and pass it here. Whenever you call `next` on your subject, assuming the form is valid, it'll broadcast te form value to the parent (through the `output$`) |
| `outputFilterPredicate` | `(currentInputValue: FormInterface, outputValue: FormInterface) => boolean` | Optional | ✅ | ❌ | The default behaviour is to compare the current transformed value of `input$` with the current value of the form _(deep check)_, and if these are equal, the value won't be passed to `output$` in order to prevent the broadcast |
| `handleEmissionRate` | `(obs$: Observable<FormInterface>) => Observable<FormInterface>` | Optional | ✅ | ❌ | If you want to control how frequently the form emits on the `output$`, you can customise the emission rate with this. Example: `handleEmissionRate: formValue$ => formValue$.pipe(debounceTime(300))` |
| Key | Type | Optional or required | Root form | Sub form | What is it for? |
| ------------------------ | --------------------------------------------------------------------------- | -------------------- | --------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `formType` | `FormType` | Required | ✅ | ✅ | Defines the type of the form. Can either be `FormType.ROOT` or `FormType.SUB` |
| `disabled$` | `Observable<boolean>` | Required | ✅ | ❌ | When this observable emits `true`, the whole form (including the root form and all the sub forms) will be disabled |
| `input$` | `Observable<ControlInterface \| undefined>` | Required | ✅ | ❌ | A root form is a component in between the parent passing raw data and the form itself. This property is an observable that you must provide which will be used behind the scenes to update for you the form values |
| `output$` | `Subject<ControlInterface>` | Required | ✅ | ❌ | A root form is a component in between the parent passing raw data and the form itself. This property is an observable that you must provide which will be used behind the scenes to broadcast the form value to the parent when it changes |
| `manualSave$` | `Observable<void>` | Optional | ✅ | ❌ | By default a root form will automatically broadcast all the form updates (through the `output$`) as soon as there's a change. If you wish to "save" the form only when you click on a save button for example, you can create a subject on your side and pass it here. Whenever you call `next` on your subject, assuming the form is valid, it'll broadcast te form value to the parent (through the `output$`) |
| `outputFilterPredicate` | `(currentInputValue: FormInterface, outputValue: FormInterface) => boolean` | Optional | ✅ | ❌ | The default behaviour is to compare the current transformed value of `input$` with the current value of the form _(deep check)_, and if these are equal, the value won't be passed to `output$` in order to prevent the broadcast |
| `handleEmissionRate` | `(obs$: Observable<FormInterface>) => Observable<FormInterface>` | Optional | ✅ | ❌ | If you want to control how frequently the form emits on the `output$`, you can customise the emission rate with this. Example: `handleEmissionRate: formValue$ => formValue$.pipe(debounceTime(300))` |
| `emitInitialValueOnInit` | `boolean` | Optional | ✅ | ✅ | Controls whether the form (root or sub) will emit the default values provided with the `FormControls` if they are valid. The default behavior is not to emit the default values |

# Principles

Expand Down
56 changes: 53 additions & 3 deletions projects/ngx-sub-form/src/lib/create-form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { combineLatest, concat, EMPTY, identity, merge, Observable, of, timer }
import {
delay,
filter,
first,
map,
mapTo,
shareReplay,
Expand Down Expand Up @@ -150,17 +151,62 @@ export function createForm<ControlInterface, FormInterface extends {}>(
shareReplay({ refCount: true, bufferSize: 1 }),
);

const broadcastDefaultValueToParent$: Observable<ControlInterface> = transformedValue$.pipe(
first(),
filter(() => options.emitDefaultValue ?? false),
switchMap(transformedValue => {
const defaultValue$ = of(transformedValue);
if (!isRoot<ControlInterface, FormInterface>(options)) {
return defaultValue$.pipe(delay(0));
} else {
// it might be surprising to see formGroup validity being checked twice
// here, however this is intentional. The delay(0) allows any sub form
// components to populate values into the form, and it is possible for
// the form to be invalid after this process. In which case we suppress
// outputting an invalid value, and wait for the user to make the value
// become valid.
return defaultValue$.pipe(
filter(() => formGroup.valid),
delay(0),
filter(formValue => {
if (formGroup.invalid) {
return false;
}

if (options.outputFilterPredicate) {
return options.outputFilterPredicate(transformedValue, formValue);
}

return true;
}),
options.handleEmissionRate ?? identity,
);
}
}),
map(value =>
options.fromFormGroup
? options.fromFormGroup(value)
: // if it's not a remap component, the ControlInterface === the FormInterface
(value as any as ControlInterface),
),
);

const broadcastValueToParent$: Observable<ControlInterface> = transformedValue$.pipe(
switchMap(transformedValue => {
const valueChanges = formGroup.valueChanges;
if (!isRoot<ControlInterface, FormInterface>(options)) {
return formGroup.valueChanges.pipe(delay(0));
return valueChanges.pipe(
delay(0),
tap(v => console.log('not root', v)),
);
} else {
const formValues$ = options.manualSave$
? options.manualSave$.pipe(
withLatestFrom(formGroup.valueChanges),
withLatestFrom(valueChanges),
tap(console.log),
map(([_, formValue]) => formValue),
)
: formGroup.valueChanges;
: valueChanges.pipe(tap(console.log));

// it might be surprising to see formGroup validity being checked twice
// here, however this is intentional. The delay(0) allows any sub form
Expand All @@ -169,6 +215,7 @@ export function createForm<ControlInterface, FormInterface extends {}>(
// outputting an invalid value, and wait for the user to make the value
// become valid.
return formValues$.pipe(
tap(v => console.log('formValues$', v)),
filter(() => formGroup.valid),
delay(0),
filter(formValue => {
Expand Down Expand Up @@ -217,6 +264,9 @@ export function createForm<ControlInterface, FormInterface extends {}>(
broadcastValueToParent$: registerOnChange$.pipe(
switchMap(onChange => broadcastValueToParent$.pipe(tap(value => onChange(value)))),
),
broadcastDefaultValueToParent$: registerOnChange$.pipe(
switchMap(onChange => broadcastDefaultValueToParent$.pipe(tap(value => onChange(value)))),
),
applyUpstreamUpdateOnLocalForm$: transformedValue$.pipe(
tap(value => {
handleFormArrays<FormInterface>(formArrays, value, createFormArrayControl);
Expand Down
1 change: 1 addition & 0 deletions projects/ngx-sub-form/src/lib/ngx-sub-form.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export type NgxSubFormOptions<
formControls: Controls<FormInterface>;
formGroupOptions?: FormGroupOptions<FormInterface>;
emitNullOnDestroy?: boolean;
emitDefaultValue?: boolean;
componentHooks?: ComponentHooks;
// emit on this observable to mark the control as touched
touched$?: Observable<void>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,12 @@ export class AssassinDroidComponent {

public form = createForm<AssassinDroid>(this, {
formType: FormType.SUB,
emitDefaultValue: true,
formControls: {
color: new UntypedFormControl(null, { validators: [Validators.required] }),
name: new UntypedFormControl(null, { validators: [Validators.required] }),
color: new UntypedFormControl('#111111', { validators: [Validators.required] }),
name: new UntypedFormControl('hello', { validators: [Validators.required] }),
droidType: new UntypedFormControl(DroidType.ASSASSIN, { validators: [Validators.required] }),
weapons: new UntypedFormControl([], { validators: [Validators.required] }),
weapons: new UntypedFormControl(['Axe'], { validators: [Validators.required] }),
},
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export class ListingFormComponent {
manualSave$: this.manualSave$$,
formControls: {
vehicleProduct: new UntypedFormControl(null),
droidProduct: new UntypedFormControl(null),
droidProduct: new UntypedFormControl(null, Validators.required),
listingType: new UntypedFormControl(null, Validators.required),
id: new UntypedFormControl(null, Validators.required),
title: new UntypedFormControl(null, Validators.required),
Expand Down
1 change: 1 addition & 0 deletions src/app/main/listing/listing.component.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<mat-slide-toggle class="readonly" data-readonly [formControl]="readonlyFormControl">Readonly</mat-slide-toggle>

{{ listingOutput | json }}
<app-listing-form
[listing]="listing$ | async"
[disabled]="readonlyFormControl.value"
Expand Down
3 changes: 3 additions & 0 deletions src/app/main/listing/listing.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,10 @@ export class ListingComponent {
};
}

public listingOutput?: OneListing;
public upsertListing(listing: OneListing): void {
this.listingOutput = listing;
console.log('upsertListing', listing);
this.listingService.upsertListing(listing);
}
}