FRP
DI
Decorator
Typescript
Immutability
Reducer
Store
Action (SFA)
Observable
<form [formGroup]="form">
<mat-form-field>
<mat-label>Name:</mat-label>
<input matInput formControlName="name">
</mat-form-field>
</form>
<button mat-raised-button (click)="onSave()" [disabled]="state.disable">Save</button>
<button mat-raised-button (click)="onDelete()" [disabled]="state.disable">Delete</button>
interface ZoneEditState {
disable: boolean;
src: Zone;
}
@Component({
selector: 'app-zone-edit-page',
templateUrl: './zone-edit-page.component.html',
styleUrls: ['./zone-edit-page.component.scss']
})
export class ZoneEditPageComponent extends RxState<ZoneEditState> implements OnInit {
form: FormGroup;
value: undefined;
initState: ZoneEditState = {
disable: undefined,
src: undefined,
};
appLoading$ = this.store.pipe(select(isLoading$));
loading$ = this.zoneService.loading$;
invalid$ = new Subject<boolean>();
destroy$ = new Subject();
disable$ = combineLatest([this.loading$, this.invalid$, this.appLoading$]).pipe(
map(([loading, invalid, appLoading]) => loading || invalid || appLoading),
this.setState$('disable'),
);
src$ = this.store.pipe(
select(currentZone$),
this.setState$('src'),
tap((src: Zone) => {
if (src) {
this.patchForm();
} else {
this.form.reset();
}
}));
constructor(private fb: FormBuilder, private zoneService: ZoneService, private store: Store<StateRoot>,
private route: ActivatedRoute, private router: Router) {
super();
this.createForm();
this.connect(this.initState);
this.connectEffect(this.disable$);
this.connectEffect(this.src$);
}
createForm() {
this.form = this.fb.group({
name: ['', [Validators.required]],
});
const status$ = this.form.statusChanges.pipe(
map(v => v === 'INVALID'),
tap(v => this.invalid$.next(v)),
);
const values$ = this.form.valueChanges.pipe(
tap(value => {
this.value = value;
}),
);
merge(status$, values$).pipe(takeUntil(this.destroy$)).subscribe();
}
ngOnInit(): void {
}
applyValueToSrc() {
const value = this.form.getRawValue();
const clone = {...this.zone, ...value};
this.zone = clone;
}
patchForm() {
this.form.reset();
this.form.patchValue(this.zone);
}
onSave() {
this.applyValueToSrc();
this.zoneService.update(this.zone);
}
onDelete() {
this.zoneService.delete(this.zone.id);
this.form.reset();
}
get zone(): Zone {
return this.state.src;
}
ngOnDestroy(): void {
super.ngOnDestroy();
this.destroy$.next({});
}
}
interface LoaderInterceptor {
pending: boolean;
}
@Injectable({
providedIn: 'root'
})
export class LoaderInterceptorService extends RxState<LoaderInterceptor> {
initState: LoaderInterceptor = {
pending: false,
};
pendingRequests$ = new Subject();
pendingRequestsCount$ = this.pendingRequests$
.pipe(scan((acc, curr: number) => acc + curr, 0));
pending$ = this.pendingRequestsCount$.pipe(
map((cnt: number) => cnt !== 0),
this.setState$('pending'),
tap((pending: boolean) => this.store.dispatch(UiActions.isLoading({payload: pending}))
)
);
constructor(
private store: Store<StateRoot>
) {
super();
this.connect(this.initState);
this.connectEffect(this.pending$);
}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
this.pendingRequests$.next(1);
return next.handle(req).pipe(
finalize(() => this.pendingRequests$.next(-1))
);
}
}
import {Injectable, OnDestroy} from '@angular/core';
import {merge, Subject} from 'rxjs';
import {publishReplay, scan, takeUntil, tap} from 'rxjs/operators';
@Injectable()
export class RxState<T> implements OnDestroy {
state: T;
destroy$ = new Subject();
command$ = new Subject<any>();
commands$ = merge(this.command$.asObservable());
setState$ = (key) => tap((v) => this.command$.next({[key]: v}));
state$: any = this.commands$
.pipe(
// tap(cmd => console.log('cmd', cmd)),
scan((state: any, command): any => ({...state, ...command})),
tap(state => this.state = state),
publishReplay(1)
);
constructor() {
this.state$.connect();
}
connect(initState: T, debug?: boolean) {
if (debug) {
this.state$.pipe(
tap((state) => console.log(`[${this.constructor.name}]`, state)),
takeUntil(this.destroy$)
).subscribe();
} else {
this.state$.pipe(
takeUntil(this.destroy$)
).subscribe();
}
this.command$.next(initState);
}
connectEffect(effect$) {
effect$.pipe(
takeUntil(this.destroy$)
).subscribe();
}
setState(cmd) {
this.command$.next(cmd);
}
ngOnDestroy(): void {
this.destroy$.next({});
}
}