Skip to content

Commit

Permalink
refactor: rework context propagation mechanism
Browse files Browse the repository at this point in the history
  • Loading branch information
dhhyi committed Apr 4, 2022
1 parent 71742dd commit b9e0d4a
Show file tree
Hide file tree
Showing 2 changed files with 108 additions and 77 deletions.
106 changes: 60 additions & 46 deletions src/app/core/directives/product-context.directive.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,70 @@
import { Directive, Input, OnInit, Optional, Output, SkipSelf } from '@angular/core';
import { ReplaySubject, combineLatest, debounceTime, distinctUntilChanged, pairwise, startWith } from 'rxjs';

import {
Directive,
Input,
OnChanges,
OnDestroy,
OnInit,
Optional,
Output,
SimpleChanges,
SkipSelf,
} from '@angular/core';

import { ProductContextDisplayProperties, ProductContextFacade } from 'ish-core/facades/product-context.facade';
ProductContext,
ProductContextDisplayProperties,
ProductContextFacade,
} from 'ish-core/facades/product-context.facade';
import { ProductCompletenessLevel, SkuQuantityType } from 'ish-core/models/product/product.model';

declare type IdType = number | string;

@Directive({
selector: '[ishProductContext]',
providers: [ProductContextFacade],
exportAs: 'ishProductContext',
})
export class ProductContextDirective implements OnInit, OnChanges, OnDestroy {
export class ProductContextDirective implements OnInit {
@Input() completeness: 'List' | 'Detail' = 'List';
@Input() propagateIndex: number | string;

@Output() skuChange = this.context.select('sku');
@Output() quantityChange = this.context.select('quantity');

constructor(
@SkipSelf() @Optional() private parentContext: ProductContextFacade,
private context: ProductContextFacade
) {
this.context.hold(this.context.$, () => this.propagate());
private propIndex = new ReplaySubject<IdType>(1);

constructor(@SkipSelf() @Optional() parentContext: ProductContextFacade, private context: ProductContextFacade) {
if (parentContext) {
function removeFromParent(parent: ProductContext['children'], id: IdType) {
delete parent[id];
}

function addToParent(parent: ProductContext['children'], id: IdType, context: ProductContext) {
parent[id] = context;
}

function isId(id: IdType): boolean {
return id !== undefined;
}

parentContext.connect(
'children',
combineLatest([
this.propIndex.pipe(startWith(undefined), distinctUntilChanged(), pairwise()),
this.context.select().pipe(debounceTime(0)),
]),
(parent, [[prevId, currId], context]) => {
let newChildren: ProductContext['children'];

// remove previous entry if ID changed
if (context.propagateActive && isId(prevId) && prevId !== currId) {
newChildren = { ...parent.children };
removeFromParent(newChildren, prevId);
}

// propagate current entry
if (isId(currId)) {
newChildren ??= { ...parent.children };
if (context.propagateActive) {
addToParent(newChildren, currId, context);
} else {
removeFromParent(newChildren, currId);
}
}

return newChildren ?? parent.children;
}
);
}
}

@Input()
Expand Down Expand Up @@ -62,45 +97,24 @@ export class ProductContextDirective implements OnInit, OnChanges, OnDestroy {
this.context.set('propagateActive', () => propagateActive);
}

@Input()
set propagateIndex(index: number | string) {
this.propIndex.next(index);
}

@Input()
set parts(parts: SkuQuantityType[]) {
this.context.set('parts', () => parts);
this.context.set('displayProperties', () => ({
readOnly: true,
addToBasket: true,
}));
}

@Input()
set configuration(config: Partial<ProductContextDisplayProperties>) {
this.context.config = config;
}

private propagate(remove = false) {
if (this.propagateIndex !== undefined) {
if (!this.parentContext) {
throw new Error('cannot propagate without parent context');
}
this.parentContext.propagate(
this.propagateIndex,
this.context.get('propagateActive') && !remove ? this.context.get() : undefined
);
}
}

ngOnChanges(changes: SimpleChanges): void {
if (changes?.propagateActive) {
this.propagate();
}
}

ngOnInit() {
this.context.set('requiredCompletenessLevel', () =>
this.completeness === 'List' ? ProductCompletenessLevel.List : ProductCompletenessLevel.Detail
);
}

ngOnDestroy(): void {
this.propagate(true);
}
}
79 changes: 48 additions & 31 deletions src/app/core/facades/product-context.facade.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Injectable, InjectionToken, Injector } from '@angular/core';
import { Injectable, InjectionToken, Injector, OnDestroy } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { RxState } from '@rx-angular/state';
import { isEqual } from 'lodash-es';
import { BehaviorSubject, Observable, combineLatest } from 'rxjs';
import { BehaviorSubject, Observable, combineLatest, race } from 'rxjs';
import {
debounceTime,
distinctUntilChanged,
Expand All @@ -13,6 +13,7 @@ import {
skipWhile,
startWith,
switchMap,
take,
} from 'rxjs/operators';

import { AttributeGroupTypes } from 'ish-core/models/attribute-group/attribute-group.types';
Expand Down Expand Up @@ -115,7 +116,7 @@ export interface ProductContext {
}

@Injectable()
export class ProductContextFacade extends RxState<ProductContext> {
export class ProductContextFacade extends RxState<ProductContext> implements OnDestroy {
private privateConfig$ = new BehaviorSubject<Partial<ProductContextDisplayProperties>>({});
private loggingActive: boolean;
private lazyFieldsInitialized: string[] = [];
Expand Down Expand Up @@ -243,10 +244,7 @@ export class ProductContextFacade extends RxState<ProductContext> {
this.select('children').pipe(
map(children => Object.values(children)),
skipWhile(children => !children?.length),
map(
children =>
!children.filter(x => !!x).length || children.filter(x => !!x).some(child => child.hasQuantityError)
),
map(children => !children.length || children.some(child => child.hasQuantityError)),
distinctUntilChanged()
)
);
Expand Down Expand Up @@ -274,10 +272,7 @@ export class ProductContextFacade extends RxState<ProductContext> {
this.select('children').pipe(
map(children => Object.values(children)),
skipWhile(children => !children?.length),
map(
children =>
!children.filter(x => !!x).length || children.filter(x => !!x).some(child => child.hasProductError)
),
map(children => !children.length || children.some(child => child.hasProductError)),
distinctUntilChanged()
)
);
Expand Down Expand Up @@ -321,6 +316,30 @@ export class ProductContextFacade extends RxState<ProductContext> {
map(props => props.reduce((acc, p) => ({ ...acc, ...p }), {}))
)
);

// set display properties for a parent context
this.connect(
'displayProperties',
race(
this.select('children').pipe(
skipWhile(children => !children || !Object.values(children)?.length),
map(() => true)
),
this.select('parts').pipe(
skipWhile(parts => !parts?.length),
map(() => true)
),
this.select('sku').pipe(map(() => false))
).pipe(take(1)),
(state, setStandaloneProperties) =>
setStandaloneProperties
? {
...state.displayProperties,
readOnly: state.displayProperties.readOnly ?? true,
addToBasket: state.displayProperties.addToBasket ?? true,
}
: state.displayProperties
);
}

private get isMaximumLevel(): boolean {
Expand Down Expand Up @@ -407,29 +426,20 @@ export class ProductContextFacade extends RxState<ProductContext> {
}

addToBasket() {
const items: SkuQuantityType[] = Object.values(this.get('children')) || this.get('parts');
if (items && !ProductHelper.isProductBundle(this.get('product'))) {
items
.filter(x => !!x && !!x.quantity)
.forEach(child => {
this.shoppingFacade.addProductToBasket(child.sku, child.quantity);
});
let items: SkuQuantityType[];
if (Object.values(this.get('children'))?.length) {
items = Object.values(this.get('children'));
} else if (this.get('parts')?.length && !ProductHelper.isProductBundle(this.get('product'))) {
items = this.get('parts');
} else {
this.shoppingFacade.addProductToBasket(this.get('sku'), this.get('quantity'));
items = [this.get()];
}
}

propagate(index: number | string, childState: ProductContext) {
this.set('children', state => {
const current = { ...state.children };
current[index] = childState;
return current;
});
this.set('displayProperties', state => ({
...state.displayProperties,
readOnly: true,
addToBasket: true,
}));
items
.filter(x => !!x && !!x.quantity)
.forEach(child => {
this.shoppingFacade.addProductToBasket(child.sku, child.quantity);
});
}

validDebouncedQuantityUpdate$(time = 800) {
Expand All @@ -451,4 +461,11 @@ export class ProductContextFacade extends RxState<ProductContext> {
)
);
}

ngOnDestroy(): void {
if (this.get('propagateActive')) {
this.set('propagateActive', () => false);
}
super.ngOnDestroy();
}
}

1 comment on commit b9e0d4a

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Azure Demo Servers are available:

Please sign in to comment.