Skip to content

Commit

Permalink
feat: product context access directive (#605)
Browse files Browse the repository at this point in the history
  • Loading branch information
dhhyi authored Mar 9, 2021
1 parent 57bf890 commit fedbc59
Show file tree
Hide file tree
Showing 17 changed files with 81 additions and 74 deletions.
3 changes: 3 additions & 0 deletions src/app/core/directives.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { NgModule } from '@angular/core';
import { ClickOutsideDirective } from './directives/click-outside.directive';
import { IdentityProviderCapabilityDirective } from './directives/identity-provider-capability.directive';
import { IntersectionObserverDirective } from './directives/intersection-observer.directive';
import { ProductContextAccessDirective } from './directives/product-context-access.directive';
import { ProductContextDirective } from './directives/product-context.directive';
import { ServerHtmlDirective } from './directives/server-html.directive';

Expand All @@ -11,13 +12,15 @@ import { ServerHtmlDirective } from './directives/server-html.directive';
ClickOutsideDirective,
IdentityProviderCapabilityDirective,
IntersectionObserverDirective,
ProductContextAccessDirective,
ProductContextDirective,
ServerHtmlDirective,
],
exports: [
ClickOutsideDirective,
IdentityProviderCapabilityDirective,
IntersectionObserverDirective,
ProductContextAccessDirective,
ProductContextDirective,
ServerHtmlDirective,
],
Expand Down
47 changes: 47 additions & 0 deletions src/app/core/directives/product-context-access.directive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Directive, EmbeddedViewRef, OnDestroy, TemplateRef, ViewContainerRef } from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

import { ProductContext, ProductContextFacade } from 'ish-core/facades/product-context.facade';

type ProductContextAccessContext = ProductContext & { context: ProductContextFacade };

@Directive({
selector: '[ishProductContextAccess]',
})
export class ProductContextAccessDirective implements OnDestroy {
private view: EmbeddedViewRef<ProductContextAccessContext>;
private destroy$ = new Subject();

constructor(
context: ProductContextFacade,
viewContainer: ViewContainerRef,
template: TemplateRef<ProductContextAccessContext>
) {
context
.select()
.pipe(takeUntil(this.destroy$))
.subscribe(ctx => {
if (!this.view && ctx?.product) {
this.view = viewContainer.createEmbeddedView(template, { ...ctx, context });
} else if (this.view && ctx?.product) {
Object.keys(ctx).forEach(key => {
this.view.context[key] = ctx[key];
});
}

if (this.view) {
this.view.markForCheck();
}
});
}

static ngTemplateContextGuard(_: ProductContextAccessDirective, ctx: unknown): ctx is ProductContextAccessContext {
return !!ctx || true;
}

ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}
2 changes: 1 addition & 1 deletion src/app/core/facades/product-context.facade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export const EXTERNAL_DISPLAY_PROPERTY_PROVIDER = new InjectionToken<ExternalDis
'externalDisplayPropertiesProvider'
);

interface ProductContext {
export interface ProductContext {
sku: string;
requiredCompletenessLevel: ProductCompletenessLevel | true;
product: AnyProductViewType;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ <h2 class="modal-title">{{ headerTranslationKey | translate }}</h2>
</ng-container>

<ng-template #showSuccess>
<div class="modal-body">
<div class="modal-body" *ishProductContextAccess="let product = product">
<span
[ishServerHtml]="
successTranslationKey
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { of } from 'rxjs';
import { anything, capture, instance, mock, spy, verify, when } from 'ts-mockito';

import { ServerHtmlDirective } from 'ish-core/directives/server-html.directive';
import { ProductContextFacade } from 'ish-core/facades/product-context.facade';
import { InputComponent } from 'ish-shared/forms/components/input/input.component';

import { OrderTemplatesFacade } from '../../facades/order-templates.facade';
Expand Down Expand Up @@ -36,10 +35,7 @@ describe('Select Order Template Modal Component', () => {
SelectOrderTemplateModalComponent,
],
imports: [NgbModalModule, ReactiveFormsModule, TranslateModule.forRoot()],
providers: [
{ provide: OrderTemplatesFacade, useFactory: () => instance(orderTemplateFacadeMock) },
{ provide: ProductContextFacade, useFactory: () => instance(mock(ProductContextFacade)) },
],
providers: [{ provide: OrderTemplatesFacade, useFactory: () => instance(orderTemplateFacadeMock) }],
}).compileComponents();
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import { TranslateService } from '@ngx-translate/core';
import { Subject } from 'rxjs';
import { take, takeUntil } from 'rxjs/operators';

import { ProductContextFacade } from 'ish-core/facades/product-context.facade';
import { SelectOption } from 'ish-shared/forms/components/select/select.component';
import { markAsDirtyRecursive } from 'ish-shared/forms/utils/form-utils';

Expand Down Expand Up @@ -58,8 +57,7 @@ export class SelectOrderTemplateModalComponent implements OnInit, OnDestroy {
private ngbModal: NgbModal,
private fb: FormBuilder,
private translate: TranslateService,
private orderTemplatesFacade: OrderTemplatesFacade,
private context: ProductContextFacade
private orderTemplatesFacade: OrderTemplatesFacade
) {}

ngOnInit() {
Expand Down Expand Up @@ -220,8 +218,4 @@ export class SelectOrderTemplateModalComponent implements OnInit, OnDestroy {
? 'account.order_template.added.confirmation'
: 'account.order_template.move.added.text';
}

get product() {
return this.context.get('product');
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<ng-container *ngIf="product$ | async as product">
<ng-container *ishProductContextAccess="let product = product">
<div class="row" data-testing-id="wishlist-product">
<div class="col-3 col-md-2 list-item">
<ish-product-image imageType="S" [link]="true"></ish-product-image>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { TranslateModule } from '@ngx-translate/core';
import { MockComponent, MockPipe } from 'ng-mocks';
import { instance, mock } from 'ts-mockito';

import { ProductContextFacade } from 'ish-core/facades/product-context.facade';
import { DatePipe } from 'ish-core/pipes/date.pipe';
import { ProductAddToBasketComponent } from 'ish-shared/components/product/product-add-to-basket/product-add-to-basket.component';
import { ProductBundleDisplayComponent } from 'ish-shared/components/product/product-bundle-display/product-bundle-display.component';
Expand Down Expand Up @@ -43,11 +42,7 @@ describe('Account Wishlist Detail Line Item Component', () => {
],
imports: [TranslateModule.forRoot()],
providers: [{ provide: WishlistsFacade, useFactory: () => instance(mock(WishlistsFacade)) }],
})
.overrideComponent(AccountWishlistDetailLineItemComponent, {
set: { providers: [{ provide: ProductContextFacade, useFactory: () => instance(mock(ProductContextFacade)) }] },
})
.compileComponents();
}).compileComponents();
});

beforeEach(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import { ChangeDetectionStrategy, Component, Input, OnChanges, OnInit } from '@angular/core';
import { Observable } from 'rxjs';

import { ProductContextFacade } from 'ish-core/facades/product-context.facade';
import { ProductView } from 'ish-core/models/product-view/product-view.model';
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';

import { WishlistsFacade } from '../../../facades/wishlists.facade';
import { Wishlist, WishlistItem } from '../../../models/wishlist/wishlist.model';
Expand All @@ -14,24 +10,13 @@ import { Wishlist, WishlistItem } from '../../../models/wishlist/wishlist.model'
selector: 'ish-account-wishlist-detail-line-item',
templateUrl: './account-wishlist-detail-line-item.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [ProductContextFacade],
})
export class AccountWishlistDetailLineItemComponent implements OnChanges, OnInit {
constructor(private wishlistsFacade: WishlistsFacade, private context: ProductContextFacade) {}
export class AccountWishlistDetailLineItemComponent {
constructor(private wishlistsFacade: WishlistsFacade) {}

@Input() wishlistItemData: WishlistItem;
@Input() currentWishlist: Wishlist;

product$: Observable<ProductView>;

ngOnInit() {
this.product$ = this.context.select('product');
}

ngOnChanges() {
this.context.set('sku', () => this.wishlistItemData.sku);
}

moveItemToOtherWishlist(sku: string, wishlistMoveData: { id: string; title: string }) {
if (wishlistMoveData.id) {
this.wishlistsFacade.moveItemToWishlist(this.currentWishlist.id, wishlistMoveData.id, sku);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ <h1>{{ wishlist?.title }}</h1>
<ng-container *ngFor="let item of wishlist.items">
<div class="list-item-row">
<ish-account-wishlist-detail-line-item
ishProductContext
[sku]="item.sku"
[wishlistItemData]="item"
[currentWishlist]="wishlist"
></ish-account-wishlist-detail-line-item>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateModule } from '@ngx-translate/core';
import { MockComponent } from 'ng-mocks';
import { MockComponent, MockDirective } from 'ng-mocks';
import { instance, mock } from 'ts-mockito';

import { ProductContextDirective } from 'ish-core/directives/product-context.directive';
import { ErrorMessageComponent } from 'ish-shared/components/common/error-message/error-message.component';
import { LoadingComponent } from 'ish-shared/components/common/loading/loading.component';

Expand All @@ -29,6 +30,7 @@ describe('Account Wishlist Detail Page Component', () => {
MockComponent(FaIconComponent),
MockComponent(LoadingComponent),
MockComponent(WishlistPreferencesDialogComponent),
MockDirective(ProductContextDirective),
],
providers: [{ provide: WishlistsFacade, useFactory: () => instance(mock(WishlistsFacade)) }],
}).compileComponents();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ <h2 class="modal-title">{{ headerTranslationKey | translate }}</h2>
</ng-container>

<ng-template #showSuccess>
<div class="modal-body">
<div class="modal-body" *ishProductContextAccess="let product = product">
<span
[ishServerHtml]="
successTranslationKey
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { of } from 'rxjs';
import { anything, capture, instance, mock, spy, verify, when } from 'ts-mockito';

import { ServerHtmlDirective } from 'ish-core/directives/server-html.directive';
import { ProductContextFacade } from 'ish-core/facades/product-context.facade';
import { InputComponent } from 'ish-shared/forms/components/input/input.component';

import { WishlistsFacade } from '../../facades/wishlists.facade';
Expand All @@ -34,10 +33,7 @@ describe('Select Wishlist Modal Component', () => {
await TestBed.configureTestingModule({
declarations: [MockComponent(InputComponent), MockDirective(ServerHtmlDirective), SelectWishlistModalComponent],
imports: [NgbModalModule, ReactiveFormsModule, TranslateModule.forRoot()],
providers: [
{ provide: WishlistsFacade, useFactory: () => instance(wishlistFacadeMock) },
{ provide: ProductContextFacade, useFactory: () => instance(mock(ProductContextFacade)) },
],
providers: [{ provide: WishlistsFacade, useFactory: () => instance(wishlistFacadeMock) }],
}).compileComponents();
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import { TranslateService } from '@ngx-translate/core';
import { Subject } from 'rxjs';
import { take, takeUntil } from 'rxjs/operators';

import { ProductContextFacade } from 'ish-core/facades/product-context.facade';
import { SelectOption } from 'ish-shared/forms/components/select/select.component';
import { markAsDirtyRecursive } from 'ish-shared/forms/utils/form-utils';

Expand Down Expand Up @@ -59,8 +58,7 @@ export class SelectWishlistModalComponent implements OnInit, OnDestroy {
private ngbModal: NgbModal,
private fb: FormBuilder,
private translate: TranslateService,
private wishlistsFacade: WishlistsFacade,
private context: ProductContextFacade
private wishlistsFacade: WishlistsFacade
) {}

ngOnInit() {
Expand Down Expand Up @@ -233,8 +231,4 @@ export class SelectWishlistModalComponent implements OnInit, OnDestroy {
? 'account.wishlists.add_to_wishlist.confirmation'
: 'account.wishlists.move_wishlist_item.confirmation';
}

get product() {
return this.context.get('product');
}
}
12 changes: 7 additions & 5 deletions src/app/pages/product/product-brand/product-brand.component.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<div *ngIf="manufacturer$ | async as manufacturer" class="product-brand">
<a [routerLink]="['/search', manufacturer]">
<span itemprop="brand">{{ manufacturer }}</span>
</a>
</div>
<ng-container *ishProductContextAccess="let product = product">
<div *ngIf="product.manufacturer as manufacturer" class="product-brand">
<a [routerLink]="['/search', manufacturer]">
<span itemprop="brand">{{ manufacturer }}</span>
</a>
</div>
</ng-container>
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import { RouterTestingModule } from '@angular/router/testing';
import { of } from 'rxjs';
import { instance, mock, when } from 'ts-mockito';

import { ProductContextFacade } from 'ish-core/facades/product-context.facade';
import { ProductContextAccessDirective } from 'ish-core/directives/product-context-access.directive';
import { ProductContext, ProductContextFacade } from 'ish-core/facades/product-context.facade';
import { ProductView } from 'ish-core/models/product-view/product-view.model';

import { ProductBrandComponent } from './product-brand.component';

Expand All @@ -16,11 +18,11 @@ describe('Product Brand Component', () => {

beforeEach(async () => {
const context = mock(ProductContextFacade);
when(context.select('product', 'manufacturer')).thenReturn(of('Samsung'));
when(context.select()).thenReturn(of({ product: { manufacturer: 'Samsung' } as ProductView } as ProductContext));

await TestBed.configureTestingModule({
imports: [RouterTestingModule.withRoutes([{ path: '**', component: ProductBrandComponent }])],
declarations: [ProductBrandComponent],
declarations: [ProductBrandComponent, ProductContextAccessDirective],
providers: [{ provide: ProductContextFacade, useFactory: () => instance(context) }],
}).compileComponents();
});
Expand Down
15 changes: 2 additions & 13 deletions src/app/pages/product/product-brand/product-brand.component.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,8 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';

import { ProductContextFacade } from 'ish-core/facades/product-context.facade';
import { ChangeDetectionStrategy, Component } from '@angular/core';

@Component({
selector: 'ish-product-brand',
templateUrl: './product-brand.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ProductBrandComponent implements OnInit {
manufacturer$: Observable<string>;

constructor(private context: ProductContextFacade) {}

ngOnInit() {
this.manufacturer$ = this.context.select('product', 'manufacturer');
}
}
export class ProductBrandComponent {}

0 comments on commit fedbc59

Please sign in to comment.