angular-blueprint

3 - Unidirectional

issue#24 Have a generic observable store

# Generate a core-domain library
ng g @nrwl/workspace:library core-domain --directory=

libs\core-domain\src\lib\services\store.ts

import { BehaviorSubject, Observable } from 'rxjs';

export class Store<T> {
  private state$ = new BehaviorSubject<T>({ ...this.state });

  constructor(private state: T) {}

  public select(): T {
    return { ...this.state };
  }
  public select$(): Observable<T> {
    return this.state$.asObservable();
  }

  public set(newState: T): void {
    this.state = { ...newState };
    this.state$.next(this.select());
  }
}
# container
ng g c containers/details-page --project=policy --export --inlineStyle --inlineTemplate --changeDetection=OnPush

libs\policy\src\lib\containers\details-page\details-page.component.ts

@Component({
  selector: 'ab-policy-details-page',
  template: `
    <ab-policy-details [policyHolder]="policyHolder"> </ab-policy-details>
  `,
  styles: [],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class DetailsPageComponent implements OnInit {
  public policyHolder: PolicyHolder;

  constructor(policyService: PolicyService) {
    this.policyHolder = policyService.policyHolderConfig;
  }
  ngOnInit() {}
}

apps\spa\src\app\app.routes.ts

export const appRoutes = [
  { path: environment.policyInfo.noticeUrl, component: DetailsPageComponent }
];

libs\policy\src\lib\services\policy.service.ts

export class PolicyService {
  public targetUrlToNavigateForDetails$(): Observable<string> {
    return this.policyDetailsStore.select$().pipe(
      filter(x => x.policyHolder.noticeUrl !== undefined),
      map(x => (x.showingDetails ? x.policyHolder.noticeUrl : ''))
    );
  }
}

apps\spa\src\app\app.component.ts

export class AppComponent {
  public title = 'spa';
  public showPolicyDialog$: Observable<boolean>;

  constructor(private router: Router, private policyService: PolicyService) {
    this.showPolicyDialog$ = this.policyService.haveToShowAccpetationDialog$();
    this.policyService
      .targetUrlToNavigateForDetails$()
      .subscribe({ next: this.navigateTo.bind(this) });
  }
  private navigateTo(targetUrl: string) {
    this.router.navigate([targetUrl]);
  }
}

libs\policy\src\lib\components\dialog\dialog.component.html

<dialog open>
  ...
  <aside *ngIf="!policyDetails.policyHolder.noticeUrl">
    <ab-policy-details *ngIf="policyDetails.showingDetails"
                       [policyHolder]="policyDetails.policyHolder"></ab-policy-details>
  </aside>
</dialog>

issue#20 Have an accept policy button on dialog

libs\policy\src\lib\components\dialog\dialog.component.ts

export class DialogComponent implements OnInit {
  @Output() public acceptPolicy = new EventEmitter<void>();
  ...
}

libs\policy\src\lib\components\dialog\dialog.component.html

<dialog open>
  ...
  <main class="buttons">
    <button (click)="toggleDetails.next()"> details</button>
    <button (click)="acceptPolicy.next()">I accept</button>
  </main>
  ...
</dialog>

libs\policy\src\lib\containers\mandatory-dialog\mandatory-dialog.component.ts

@Component({
  selector: 'ab-policy-mandatory-dialog',
  template: `
    <ab-policy-dialog
      *ngIf="(policyDetails$ | async) as policyDetails"
      [policyDetails]="policyDetails"
      (toggleDetails)="onToggleDetails()"
      (acceptPolicy)="onAcceptPolicy()"
    >
    </ab-policy-dialog>
  `,
  styles: [],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class MandatoryDialogComponent implements OnInit {
  public policyDetails$: Observable<PolicyDetails> = this.policyService.policyDetails$();

  constructor(private policyService: PolicyService) {}

  public ngOnInit(): void {}

  public onToggleDetails(): void {
    this.policyService.toggleMoreDetails();
  }
  public onAcceptPolicy(): void {
    this.policyService.acceptPolicy();
  }
}

libs\policy\src\lib\services\policy.service.ts

export class PolicyService {
  public acceptPolicy() {
    this.policyAcceptationEntity.acceptPolicy();
  }
}

libs\policy-domain\src\lib\services\policy-acceptation.entity.ts

export interface PolicyAcceptation {
  acceptedOn: Date;
}
export class PolicyAcceptationEntity {
  private policyAcceptationStore: Store<PolicyAcceptation>;
  public startPolicyManagement(): void {
    const currentPolicyAcceptation = {};
    this.policyAcceptationStore.set(currentPolicyAcceptation);
  }
  public acceptPolicy(): void {
    const policyAcceptationPayload: PolicyAcceptation = {
      acceptedOn: new Date()
    };
    this.policyAcceptationStore.set(policyAcceptationPayload);
  }
  public isPolicyAccepted$(): Observable<boolean> {
    return this.policyAcceptationStore
      .select$()
      .pipe(map(currentState => currentState['acceptedOn'] !== undefined));
  }
}

issue#21 Have a repository to store the policy acceptation token in the browser Local Store

libs\policy-domain\src\lib\services\policy-acceptation.repository.ts

export class PolicyAcceptationRepository {
  private policyAcceptationKey = 'policyAcceptation';

  constructor() {}

  public getAcceptation(): PolicyAcceptation {
    if (window) {
      const value: string = window.localStorage.getItem(
        this.policyAcceptationKey
      );
      if (value) {
        return JSON.parse(value);
      }
    }
    return undefined;
  }
  public setAcceptation(value: PolicyAcceptation): void {
    if (window) {
      window.localStorage.setItem(
        this.policyAcceptationKey,
        JSON.stringify(value)
      );
    }
  }
}

libs\policy-domain\src\lib\services\policy-acceptation.entity.ts

export class PolicyAcceptationEntity {
  private policyAcceptationStore: Store<PolicyAcceptation>;
  private policyAcceptationRepository: PolicyAcceptationRepository = new PolicyAcceptationRepository();
  constructor() {
    this.policyAcceptationStore = new Store<PolicyAcceptation>({
      acceptedOn: undefined
    });
  }

  public startPolicyManagement(): void {
    const currentPolicyAcceptation = this.policyAcceptationRepository.getAcceptation();
    this.policyAcceptationStore.set(currentPolicyAcceptation);
  }

  public acceptPolicy(): void {
    const policyAcceptationPayload: PolicyAcceptation = {
      acceptedOn: new Date()
    };
    this.policyAcceptationRepository.setAcceptation(policyAcceptationPayload);
    this.policyAcceptationStore.set(currentPolicyAcceptation);
  }
}

issue#22 Show dialog or navigate based on accepted policy state

libs\policy\src\lib\services\policy.service.ts

export class PolicyService {
  private policyAcceptationEntity: PolicyAcceptationEntity = new PolicyAcceptationEntity();
  private policyDetailsStore: Store<PolicyDetails>;

  constructor(@Inject(POLICY_HOLDER_CONFIG) public readonly policyHolderConfig: PolicyHolder) {
    this.policyDetailsStore = new Store<PolicyDetails>({
      showingDetails: false,
      policyHolder: policyHolderConfig
    });
    this.policyAcceptationEntity.startPolicyManagement();
  }

  public haveToShowAcceptationDialog$(): Observable<boolean> {
    return this.policyAcceptationEntity.isPolicyAccepted$().pipe(map(x => !x));
  }
  public targetUrlToNavigateWhenAccepted$(): Observable<string> {
    return this.policyAcceptationEntity.isPolicyAccepted$().pipe(
      filter(x => x),
      map(x => '')
    );
  }
  ...
}

apps\spa\src\app\app.component.ts

export class AppComponent {
  public title = 'spa';
  public showPolicyDialog$: Observable<boolean>;

  constructor(private router: Router, private policyService: PolicyService) {
    this.showPolicyDialog$ = this.policyService.haveToShowAcceptationDialog$();
    this.policyService
      .targetUrlToNavigateForDetails$()
      .subscribe({ next: this.navigateTo.bind(this) });
    this.policyService
      .targetUrlToNavigateWhenAccepted$()
      .subscribe({ next: this.navigateTo.bind(this) });
  }
  private navigateTo(targetUrl: string) {
    this.router.navigate([targetUrl]);
  }
}

Already documented issues:

issue#23 Have an Observable system to control state of cookies policy

issue#26 Have a command and a query for showPolicyDetails

No needed anymore:

issue#28 Simplify No policy container

Diagrams

Projects dependencies

3-projects-dependency 3-class-dependency


Back to index

previous: 2-change


angular-blueprint is a free product created by Angular Builders

Angular.Builders

Resources for software architects and teams of programmers who built great applications with Angular.

– By Alberto Basalo