Functional Reactive Programing aRCHITECTURE

TECH STACK

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({});
  }
}

FRP

By bretto

FRP

  • 288