From 855a1e0a84e088a53994f0f738c1740a4d1a0267 Mon Sep 17 00:00:00 2001 From: Danilo Hoffmann Date: Thu, 18 Jun 2020 16:19:45 +0200 Subject: [PATCH] perf: correctly memoize categories selectors --- src/app/core/facades/shopping.facade.ts | 16 +++- .../category-view/category-view.model.spec.ts | 32 ++----- .../category-view/category-view.model.ts | 10 +-- .../core/routing/category/category.route.ts | 8 +- .../categories/categories.selectors.spec.ts | 86 +++++++++++++++++-- .../categories/categories.selectors.ts | 46 +++++++++- .../store/shopping/shopping-store.spec.ts | 8 -- .../category-categories.component.html | 4 +- .../category-list.component.html | 4 +- .../category-list/category-list.component.ts | 9 +- .../category-navigation.component.html | 16 ++-- .../category-navigation.component.spec.ts | 44 ++++++++-- .../category-navigation.component.ts | 22 +++-- .../category/category-page.component.html | 4 +- .../category-tile.component.html | 6 +- .../category-tile.component.spec.ts | 44 ++++++---- .../category-tile/category-tile.component.ts | 18 +++- .../breadcrumb/breadcrumb.component.spec.ts | 6 +- .../common/breadcrumb/breadcrumb.component.ts | 7 +- .../header-navigation.component.html | 6 +- .../header-navigation.component.spec.ts | 3 + ...category-navigation.component.spec.ts.snap | 21 ----- .../sub-category-navigation.component.html | 16 ++-- .../sub-category-navigation.component.spec.ts | 64 +++++++++++--- .../sub-category-navigation.component.ts | 43 +++++----- 25 files changed, 354 insertions(+), 189 deletions(-) delete mode 100644 src/app/shell/header/sub-category-navigation/__snapshots__/sub-category-navigation.component.spec.ts.snap diff --git a/src/app/core/facades/shopping.facade.ts b/src/app/core/facades/shopping.facade.ts index 7bb5b68c805..ad0ee37e053 100644 --- a/src/app/core/facades/shopping.facade.ts +++ b/src/app/core/facades/shopping.facade.ts @@ -6,7 +6,13 @@ import { debounce, debounceTime, filter, map, switchMap, tap } from 'rxjs/operat import { ProductListingID } from 'ish-core/models/product-listing/product-listing.model'; import { ProductCompletenessLevel, ProductHelper } from 'ish-core/models/product/product.model'; import { addProductToBasket } from 'ish-core/store/customer/basket'; -import { getCategoryLoading, getSelectedCategory, getTopLevelCategories } from 'ish-core/store/shopping/categories'; +import { + getCategory, + getCategoryLoading, + getNavigationCategories, + getSelectedCategory, + getTopLevelCategories, +} from 'ish-core/store/shopping/categories'; import { addToCompare, getCompareProducts, @@ -54,6 +60,14 @@ export class ShoppingFacade { selectedCategory$ = this.store.pipe(select(getSelectedCategory)); selectedCategoryLoading$ = this.store.pipe(select(getCategoryLoading), debounceTime(500)); + category$(uniqueId: string) { + return this.store.pipe(select(getCategory(uniqueId))); + } + + navigationCategories$(uniqueId: string) { + return this.store.pipe(select(getNavigationCategories(uniqueId))); + } + // PRODUCT selectedProduct$ = this.store.pipe(select(getSelectedProduct)); diff --git a/src/app/core/models/category-view/category-view.model.spec.ts b/src/app/core/models/category-view/category-view.model.spec.ts index c5257421d25..7ac7e4402b3 100644 --- a/src/app/core/models/category-view/category-view.model.spec.ts +++ b/src/app/core/models/category-view/category-view.model.spec.ts @@ -39,8 +39,8 @@ describe('Category View Model', () => { ]); const view = createCategoryView(tree, '123'); - expect(view.hasChildren()).toBeFalse(); - expect(view.children()).toBeEmpty(); + expect(view.hasChildren).toBeFalse(); + expect(view.children).toBeEmpty(); }); const cat1 = { @@ -60,34 +60,16 @@ describe('Category View Model', () => { const tree = categoryTree([cat1, cat11]); const view = createCategoryView(tree, '123'); - expect(view.hasChildren()).toBeTrue(); - expect(view.children()).toHaveLength(1); + expect(view.hasChildren).toBeTrue(); + expect(view.children).toHaveLength(1); - expect(view.children()[0].uniqueId).toEqual('123.456'); + expect(view.children[0]).toEqual('123.456'); }); - it('should provide methods to check if a node in a deep complex tree has children', () => { - const tree = categoryTree([cat1, cat11, cat111]); - - const view = createCategoryView(tree, '123'); - expect(view.hasChildren()).toBeTrue(); - expect(view.children()).toHaveLength(1); - - const subCategory = view.children()[0]; - expect(subCategory.uniqueId).toEqual('123.456'); - expect(subCategory.hasChildren()).toBeTrue(); - expect(subCategory.children()).toHaveLength(1); - - const subSubCategory = subCategory.children()[0]; - expect(subSubCategory.uniqueId).toEqual('123.456.789'); - expect(subSubCategory.hasChildren()).toBeFalse(); - expect(subSubCategory.children()).toBeEmpty(); - }); - - it('should provide acces to the category path of a category', () => { + it('should provide access to the category path of a category', () => { const tree = categoryTree([cat1, cat11, cat111]); const view = createCategoryView(tree, '123.456.789'); - expect(view.pathCategories().map(v => v.uniqueId)).toEqual(['123', '123.456', '123.456.789']); + expect(view.categoryPath).toEqual(['123', '123.456', '123.456.789']); }); }); diff --git a/src/app/core/models/category-view/category-view.model.ts b/src/app/core/models/category-view/category-view.model.ts index 77700d423f0..81807271fcd 100644 --- a/src/app/core/models/category-view/category-view.model.ts +++ b/src/app/core/models/category-view/category-view.model.ts @@ -5,9 +5,8 @@ import { Category } from 'ish-core/models/category/category.model'; * View on a {@link Category} with additional methods for navigating to sub categories or category path */ export interface CategoryView extends Category { - children(): CategoryView[]; - hasChildren(): boolean; - pathCategories(): CategoryView[]; + children: string[]; + hasChildren: boolean; } export function createCategoryView(tree: CategoryTree, uniqueId: string): CategoryView { @@ -20,8 +19,7 @@ export function createCategoryView(tree: CategoryTree, uniqueId: string): Catego return { ...tree.nodes[uniqueId], - hasChildren: () => !!tree.edges[uniqueId] && !!tree.edges[uniqueId].length, - children: () => (tree.edges[uniqueId] || []).map(id => createCategoryView(tree, id)), - pathCategories: () => tree.nodes[uniqueId].categoryPath.map(id => createCategoryView(tree, id)), + children: tree.edges[uniqueId] || [], + hasChildren: !!tree.edges[uniqueId] && !!tree.edges[uniqueId].length, }; } diff --git a/src/app/core/routing/category/category.route.ts b/src/app/core/routing/category/category.route.ts index 8d0ee81b8ea..45f0d78cf18 100644 --- a/src/app/core/routing/category/category.route.ts +++ b/src/app/core/routing/category/category.route.ts @@ -2,15 +2,15 @@ import { UrlMatchResult, UrlSegment } from '@angular/router'; import { MonoTypeOperatorFunction } from 'rxjs'; import { filter } from 'rxjs/operators'; -import { CategoryView } from 'ish-core/models/category-view/category-view.model'; +import { Category } from 'ish-core/models/category/category.model'; import { CoreState } from 'ish-core/store/core/core-store'; import { selectRouteParam } from 'ish-core/store/core/router'; -export function generateLocalizedCategorySlug(category: CategoryView) { +export function generateLocalizedCategorySlug(category: Category) { if (!category || !category.categoryPath.length) { return ''; } - const lastCat = category.pathCategories()[category.categoryPath.length - 1].name; + const lastCat = category.name; return lastCat ? lastCat.replace(/ /g, '-') : ''; } @@ -37,7 +37,7 @@ export function matchCategoryRoute(segments: UrlSegment[]): UrlMatchResult { return; } -export function generateCategoryUrl(category: CategoryView): string { +export function generateCategoryUrl(category: Category): string { if (!category) { return '/'; } diff --git a/src/app/core/store/shopping/categories/categories.selectors.spec.ts b/src/app/core/store/shopping/categories/categories.selectors.spec.ts index 39d20f465a9..95b2c8f77cd 100644 --- a/src/app/core/store/shopping/categories/categories.selectors.spec.ts +++ b/src/app/core/store/shopping/categories/categories.selectors.spec.ts @@ -19,8 +19,10 @@ import { loadTopLevelCategoriesSuccess, } from './categories.actions'; import { + getCategory, getCategoryEntities, getCategoryLoading, + getNavigationCategories, getSelectedCategory, getTopLevelCategories, isTopLevelCategoriesLoaded, @@ -35,8 +37,7 @@ describe('Categories Selectors', () => { beforeEach(() => { prod = { sku: 'sku' } as Product; - cat = { uniqueId: 'Aa', categoryPath: ['Aa'] } as Category; - cat.hasOnlineProducts = true; + cat = { uniqueId: 'Aa', categoryPath: ['Aa'], hasOnlineProducts: true } as Category; @Component({ template: 'dummy' }) class DummyComponent {} @@ -63,6 +64,7 @@ describe('Categories Selectors', () => { it('should not select any selected category when used', () => { expect(getSelectedCategory(store$.state)).toBeUndefined(); + expect(getCategory(cat.uniqueId)(store$.state)).toBeUndefined(); }); it('should not select any top level categories when used', () => { @@ -117,6 +119,7 @@ describe('Categories Selectors', () => { it('should not select the irrelevant category when used', () => { expect(getSelectedCategory(store$.state)).toBeUndefined(); + expect(getCategory(cat.uniqueId)(store$.state).uniqueId).toEqual(cat.uniqueId); }); }); @@ -133,18 +136,22 @@ describe('Categories Selectors', () => { it('should select the selected category when used', () => { expect(getSelectedCategory(store$.state).uniqueId).toEqual(cat.uniqueId); + expect(getCategory(cat.uniqueId)(store$.state).uniqueId).toEqual(cat.uniqueId); }); }); }); describe('loading top level categories', () => { - let catA: Category; - let catB: Category; - beforeEach(() => { - catA = { uniqueId: 'A', categoryPath: ['A'] } as Category; - catB = { uniqueId: 'B', categoryPath: ['B'] } as Category; - store$.dispatch(loadTopLevelCategoriesSuccess({ categories: categoryTree([catA, catB]) })); + const catA = { name: 'name_A', uniqueId: 'A', categoryPath: ['A'] } as Category; + const catA1 = { name: 'name_A.1', uniqueId: 'A.1', categoryPath: ['A', 'A.1'] } as Category; + const catA1a = { name: 'name_A.1.a', uniqueId: 'A.1.a', categoryPath: ['A', 'A.1', 'A.1.a'] } as Category; + const catA1b = { name: 'name_A.1.b', uniqueId: 'A.1.b', categoryPath: ['A', 'A.1', 'A.1.b'] } as Category; + const catA2 = { name: 'name_A.2', uniqueId: 'A.2', categoryPath: ['A', 'A.2'] } as Category; + const catB = { name: 'name_B', uniqueId: 'B', categoryPath: ['B'] } as Category; + store$.dispatch( + loadTopLevelCategoriesSuccess({ categories: categoryTree([catA, catA1, catA1a, catA1b, catA2, catB]) }) + ); }); it('should select root categories when used', () => { @@ -154,5 +161,68 @@ describe('Categories Selectors', () => { it('should remember if top level categories are loaded', () => { expect(isTopLevelCategoriesLoaded(store$.state)).toBeTrue(); }); + + describe('selecting navigation categories', () => { + it('should select top level categories when no argument was supplied', () => { + expect(getNavigationCategories(undefined)(store$.state)).toMatchInlineSnapshot(` + Array [ + Object { + "hasChildren": true, + "name": "name_A", + "uniqueId": "A", + "url": "/name_A-catA", + }, + Object { + "hasChildren": false, + "name": "name_B", + "uniqueId": "B", + "url": "/name_B-catB", + }, + ] + `); + }); + + it('should select sub categories when sub category is selected', () => { + expect(getNavigationCategories('A')(store$.state)).toMatchInlineSnapshot(` + Array [ + Object { + "hasChildren": true, + "name": "name_A.1", + "uniqueId": "A.1", + "url": "/name_A.1-catA.1", + }, + Object { + "hasChildren": false, + "name": "name_A.2", + "uniqueId": "A.2", + "url": "/name_A.2-catA.2", + }, + ] + `); + }); + + it('should select deeper sub categories when deeper sub category is selected', () => { + expect(getNavigationCategories('A.1')(store$.state)).toMatchInlineSnapshot(` + Array [ + Object { + "hasChildren": false, + "name": "name_A.1.a", + "uniqueId": "A.1.a", + "url": "/name_A.1.a-catA.1.a", + }, + Object { + "hasChildren": false, + "name": "name_A.1.b", + "uniqueId": "A.1.b", + "url": "/name_A.1.b-catA.1.b", + }, + ] + `); + }); + + it('should be empty when selecting leaves', () => { + expect(getNavigationCategories('A.1.a')(store$.state)).toBeEmpty(); + }); + }); }); }); diff --git a/src/app/core/store/shopping/categories/categories.selectors.ts b/src/app/core/store/shopping/categories/categories.selectors.ts index 001e5d6944f..dff83b39749 100644 --- a/src/app/core/store/shopping/categories/categories.selectors.ts +++ b/src/app/core/store/shopping/categories/categories.selectors.ts @@ -1,6 +1,9 @@ -import { createSelector } from '@ngrx/store'; +import { createSelector, createSelectorFactory, defaultMemoize } from '@ngrx/store'; +import { isEqual } from 'lodash-es'; +import { CategoryTree, CategoryTreeHelper } from 'ish-core/models/category-tree/category-tree.model'; import { createCategoryView } from 'ish-core/models/category-view/category-view.model'; +import { generateCategoryUrl } from 'ish-core/routing/category/category.route'; import { selectRouteParam } from 'ish-core/store/core/router'; import { ShoppingState, getShoppingState } from 'ish-core/store/shopping/shopping-store'; @@ -13,10 +16,21 @@ export const getCategoryTree = createSelector(getCategoriesState, state => state */ export const getCategoryEntities = createSelector(getCategoryTree, tree => tree.nodes); +const getCategorySubTree = (uniqueId: string) => + createSelectorFactory(projector => + defaultMemoize(projector, CategoryTreeHelper.equals, CategoryTreeHelper.equals) + )(getCategoryTree, (tree: CategoryTree) => CategoryTreeHelper.subTree(tree, uniqueId)); + +export const getCategory = (uniqueId: string) => + createSelectorFactory(projector => defaultMemoize(projector, CategoryTreeHelper.equals, isEqual))( + getCategorySubTree(uniqueId), + (tree: CategoryTree) => createCategoryView(tree, uniqueId) + ); + /** * Retrieves the currently resolved selected category. */ -export const getSelectedCategory = createSelector( +export const getSelectedCategory = createSelectorFactory(projector => defaultMemoize(projector, undefined, isEqual))( getCategoryTree, selectRouteParam('categoryUniqueId'), createCategoryView @@ -29,3 +43,31 @@ export const getTopLevelCategories = createSelector(getCategoryTree, tree => ); export const isTopLevelCategoriesLoaded = createSelector(getCategoriesState, state => state.topLevelLoaded); + +export interface NavigationCategory { + uniqueId: string; + name: string; + url: string; + hasChildren: boolean; +} + +function mapNavigationCategoryFromId(uniqueId: string): NavigationCategory { + return { + uniqueId, + name: this.nodes[uniqueId].name, + url: generateCategoryUrl(this.nodes[uniqueId]), + hasChildren: !!this.edges[uniqueId]?.length, + }; +} + +export const getNavigationCategories = (uniqueId: string) => + createSelectorFactory(projector => defaultMemoize(projector, CategoryTreeHelper.equals, isEqual))( + getCategoryTree, + (tree: CategoryTree): NavigationCategory[] => { + if (!uniqueId) { + return tree.rootIds.map(mapNavigationCategoryFromId.bind(tree)); + } + const subTree = CategoryTreeHelper.subTree(tree, uniqueId); + return subTree.edges[uniqueId] ? subTree.edges[uniqueId].map(mapNavigationCategoryFromId.bind(subTree)) : []; + } + ); diff --git a/src/app/core/store/shopping/shopping-store.spec.ts b/src/app/core/store/shopping/shopping-store.spec.ts index 7838f92d855..08047ba99ae 100644 --- a/src/app/core/store/shopping/shopping-store.spec.ts +++ b/src/app/core/store/shopping/shopping-store.spec.ts @@ -480,10 +480,6 @@ describe('Shopping Store', () => { categories: tree(A,A.123) [Shopping] Load Category Success: categories: tree(A.123,A.123.456) - [Shopping] Load Category: - categoryId: "A.123" - [Shopping] Load Category Success: - categories: tree(A.123,A.123.456) @ngrx/router-store/navigated: routerState: {"url":"/category/A.123.456","params":{"categoryUniqueId":"A... event: {"id":1,"url":"/category/A.123.456"} @@ -720,10 +716,6 @@ describe('Shopping Store', () => { categories: tree(A,A.123) [Shopping] Load Category Success: categories: tree(A.123,A.123.456) - [Shopping] Load Category: - categoryId: "A.123" - [Shopping] Load Category Success: - categories: tree(A.123,A.123.456) @ngrx/router-store/navigated: routerState: {"url":"/category/A.123.456/product/P1","params":{"categoryU... event: {"id":1,"url":"/category/A.123.456/product/P1"} diff --git a/src/app/pages/category/category-categories/category-categories.component.html b/src/app/pages/category/category-categories/category-categories.component.html index 8a1a20c4f52..37d0fe60fc6 100644 --- a/src/app/pages/category/category-categories/category-categories.component.html +++ b/src/app/pages/category/category-categories/category-categories.component.html @@ -2,7 +2,7 @@
diff --git a/src/app/pages/category/category-list/category-list.component.html b/src/app/pages/category/category-list/category-list.component.html index e3dd78bda4a..4f76e8d382e 100644 --- a/src/app/pages/category/category-list/category-list.component.html +++ b/src/app/pages/category/category-list/category-list.component.html @@ -1,5 +1,5 @@ diff --git a/src/app/pages/category/category-list/category-list.component.ts b/src/app/pages/category/category-list/category-list.component.ts index 7139f2bb0ad..647e2e3a947 100644 --- a/src/app/pages/category/category-list/category-list.component.ts +++ b/src/app/pages/category/category-list/category-list.component.ts @@ -1,17 +1,10 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; -import { CategoryView } from 'ish-core/models/category-view/category-view.model'; -import { Category } from 'ish-core/models/category/category.model'; - @Component({ selector: 'ish-category-list', templateUrl: './category-list.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) export class CategoryListComponent { - @Input() categories: Category[]; - - trackByFn(_, item: CategoryView) { - return item.uniqueId; - } + @Input() categories: string[]; } diff --git a/src/app/pages/category/category-navigation/category-navigation.component.html b/src/app/pages/category/category-navigation/category-navigation.component.html index 83673a98bf2..f1ec179e843 100644 --- a/src/app/pages/category/category-navigation/category-navigation.component.html +++ b/src/app/pages/category/category-navigation/category-navigation.component.html @@ -1,19 +1,15 @@ -
+
diff --git a/src/app/pages/category/category-navigation/category-navigation.component.spec.ts b/src/app/pages/category/category-navigation/category-navigation.component.spec.ts index 13fff5ab2d2..a3fbae47377 100644 --- a/src/app/pages/category/category-navigation/category-navigation.component.spec.ts +++ b/src/app/pages/category/category-navigation/category-navigation.component.spec.ts @@ -1,11 +1,12 @@ import { ComponentFixture, TestBed, async } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { MockPipe } from 'ng-mocks'; +import { of } from 'rxjs'; +import { instance, mock, when } from 'ts-mockito'; -import { createCategoryView } from 'ish-core/models/category-view/category-view.model'; -import { Category } from 'ish-core/models/category/category.model'; +import { ShoppingFacade } from 'ish-core/facades/shopping.facade'; import { CategoryRoutePipe } from 'ish-core/routing/category/category-route.pipe'; -import { categoryTree } from 'ish-core/utils/dev/test-data-utils'; +import { NavigationCategory } from 'ish-core/store/shopping/categories'; import { CategoryNavigationComponent } from './category-navigation.component'; @@ -15,21 +16,32 @@ describe('Category Navigation Component', () => { let element: HTMLElement; beforeEach(async(() => { + const shoppingFacade = mock(ShoppingFacade); + when(shoppingFacade.selectedCategory$).thenReturn(of({ uniqueId: 'A.1' })); + when(shoppingFacade.navigationCategories$(undefined)).thenReturn( + of([ + { uniqueId: 'A', name: 'nA', url: '/c/A' }, + { uniqueId: 'B', name: 'nB', url: '/c/B' }, + ] as NavigationCategory[]) + ); + when(shoppingFacade.navigationCategories$('A')).thenReturn( + of([ + { uniqueId: 'A.1', name: 'nA.1', url: '/c/A/A.1' }, + { uniqueId: 'A.2', name: 'nA.2', url: '/c/A/A.2' }, + ] as NavigationCategory[]) + ); + when(shoppingFacade.navigationCategories$('B')).thenReturn(of([] as NavigationCategory[])); + TestBed.configureTestingModule({ imports: [RouterTestingModule], declarations: [CategoryNavigationComponent, MockPipe(CategoryRoutePipe)], + providers: [{ provide: ShoppingFacade, useFactory: () => instance(shoppingFacade) }], }) .compileComponents() .then(() => { fixture = TestBed.createComponent(CategoryNavigationComponent); component = fixture.componentInstance; element = fixture.nativeElement; - - const tree = categoryTree([ - { uniqueId: 'A', categoryPath: ['A'] }, - { uniqueId: 'A.1', categoryPath: ['A', 'A.1'] }, - ] as Category[]); - component.category = createCategoryView(tree, 'A'); }); })); @@ -38,4 +50,18 @@ describe('Category Navigation Component', () => { expect(element).toBeTruthy(); expect(() => fixture.detectChanges()).not.toThrow(); }); + + it('should create all links for tree', () => { + fixture.detectChanges(); + expect(element.querySelectorAll('a')).toMatchInlineSnapshot(` + NodeList [ + nA , + + nA.1 + , + nA.2 , + nB , + ] + `); + }); }); diff --git a/src/app/pages/category/category-navigation/category-navigation.component.ts b/src/app/pages/category/category-navigation/category-navigation.component.ts index 2f57f9de151..02e6a69a394 100644 --- a/src/app/pages/category/category-navigation/category-navigation.component.ts +++ b/src/app/pages/category/category-navigation/category-navigation.component.ts @@ -1,16 +1,28 @@ -import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; -import { CategoryView } from 'ish-core/models/category-view/category-view.model'; +import { ShoppingFacade } from 'ish-core/facades/shopping.facade'; import { CategoryHelper } from 'ish-core/models/category/category.model'; +import { NavigationCategory } from 'ish-core/store/shopping/categories'; @Component({ selector: 'ish-category-navigation', templateUrl: './category-navigation.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class CategoryNavigationComponent { - @Input() category: CategoryView; - @Input() categoryNavigationLevel: number; +export class CategoryNavigationComponent implements OnInit { + @Input() uniqueId: string; + + navigationCategories$: Observable; + currentCategoryId$: Observable; categoryEquals = CategoryHelper.equals; + + constructor(private shoppingFacade: ShoppingFacade) {} + + ngOnInit() { + this.navigationCategories$ = this.shoppingFacade.navigationCategories$(this.uniqueId); + this.currentCategoryId$ = this.shoppingFacade.selectedCategory$.pipe(map(c => c?.uniqueId)); + } } diff --git a/src/app/pages/category/category-page.component.html b/src/app/pages/category/category-page.component.html index b6061da61e7..1d94df947e3 100644 --- a/src/app/pages/category/category-page.component.html +++ b/src/app/pages/category/category-page.component.html @@ -1,6 +1,6 @@ - + @@ -12,7 +12,7 @@ - +

{{ category.name }}

diff --git a/src/app/pages/category/category-tile/category-tile.component.html b/src/app/pages/category/category-tile/category-tile.component.html index d5caa19b7cc..657ef39b064 100644 --- a/src/app/pages/category/category-tile/category-tile.component.html +++ b/src/app/pages/category/category-tile/category-tile.component.html @@ -1,4 +1,8 @@ -
+

{{ category.name }}

diff --git a/src/app/pages/category/category-tile/category-tile.component.spec.ts b/src/app/pages/category/category-tile/category-tile.component.spec.ts index 170e778149f..9166b6a7faa 100644 --- a/src/app/pages/category/category-tile/category-tile.component.spec.ts +++ b/src/app/pages/category/category-tile/category-tile.component.spec.ts @@ -1,7 +1,10 @@ import { ComponentFixture, TestBed, async } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { MockComponent, MockPipe } from 'ng-mocks'; +import { of } from 'rxjs'; +import { anything, instance, mock, when } from 'ts-mockito'; +import { ShoppingFacade } from 'ish-core/facades/shopping.facade'; import { createCategoryView } from 'ish-core/models/category-view/category-view.model'; import { Category } from 'ish-core/models/category/category.model'; import { CategoryRoutePipe } from 'ish-core/routing/category/category-route.pipe'; @@ -16,10 +19,33 @@ describe('Category Tile Component', () => { let fixture: ComponentFixture; let element: HTMLElement; + const category = { + uniqueId: 'A', + categoryPath: ['A'], + images: [ + { + name: 'front S', + type: 'Image', + imageActualHeight: 110, + imageActualWidth: 110, + viewID: 'front', + effectiveUrl: '/assets/product_img/a.jpg', + typeID: 'S', + primaryImage: false, + }, + ], + } as Category; + beforeEach(async(() => { + const shoppingFacade = mock(ShoppingFacade); + when(shoppingFacade.category$(anything())).thenReturn( + of(createCategoryView(categoryTree([category]), category.uniqueId)) + ); + TestBed.configureTestingModule({ imports: [RouterTestingModule], declarations: [CategoryTileComponent, MockComponent(CategoryImageComponent), MockPipe(CategoryRoutePipe)], + providers: [{ provide: ShoppingFacade, useFactory: () => instance(shoppingFacade) }], }).compileComponents(); })); @@ -27,24 +53,6 @@ describe('Category Tile Component', () => { fixture = TestBed.createComponent(CategoryTileComponent); component = fixture.componentInstance; element = fixture.nativeElement; - const category = { - uniqueId: 'A', - categoryPath: ['A'], - images: [ - { - name: 'front S', - type: 'Image', - imageActualHeight: 110, - imageActualWidth: 110, - viewID: 'front', - effectiveUrl: '/assets/product_img/a.jpg', - typeID: 'S', - primaryImage: false, - }, - ], - } as Category; - component.category = createCategoryView(categoryTree([category]), category.uniqueId); - fixture.detectChanges(); }); it('should be created', () => { diff --git a/src/app/pages/category/category-tile/category-tile.component.ts b/src/app/pages/category/category-tile/category-tile.component.ts index e686f5d793f..36e292b7c7b 100644 --- a/src/app/pages/category/category-tile/category-tile.component.ts +++ b/src/app/pages/category/category-tile/category-tile.component.ts @@ -1,5 +1,7 @@ -import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; +import { Observable } from 'rxjs'; +import { ShoppingFacade } from 'ish-core/facades/shopping.facade'; import { CategoryView } from 'ish-core/models/category-view/category-view.model'; /** @@ -7,16 +9,24 @@ import { CategoryView } from 'ish-core/models/category-view/category-view.model' * category using {@link CategoryImageComponent}. * * @example - * + * */ @Component({ selector: 'ish-category-tile', templateUrl: './category-tile.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class CategoryTileComponent { +export class CategoryTileComponent implements OnInit { /** * The Category to render a tile for */ - @Input() category: CategoryView; + @Input() categoryUniqueId: string; + + category$: Observable; + + constructor(private shoppingFacade: ShoppingFacade) {} + + ngOnInit() { + this.category$ = this.shoppingFacade.category$(this.categoryUniqueId); + } } diff --git a/src/app/shared/components/common/breadcrumb/breadcrumb.component.spec.ts b/src/app/shared/components/common/breadcrumb/breadcrumb.component.spec.ts index f686888550c..cb5114d929b 100644 --- a/src/app/shared/components/common/breadcrumb/breadcrumb.component.spec.ts +++ b/src/app/shared/components/common/breadcrumb/breadcrumb.component.spec.ts @@ -79,13 +79,13 @@ describe('Breadcrumb Component', () => { it('should render breadcrumbtrail from home and category when available', () => { component.category = view; fixture.detectChanges(); - expect(element.textContent).toMatchInlineSnapshot(`"Home/cat123/cat456"`); + expect(element.textContent).toMatchInlineSnapshot(`"Home/123/123.456"`); }); it('should render breadcrumbtrail from category when available', () => { component.showHome = false; component.category = view; fixture.detectChanges(); - expect(element.textContent).toMatchInlineSnapshot(`"cat123/cat456"`); + expect(element.textContent).toMatchInlineSnapshot(`"123/123.456"`); }); }); @@ -111,7 +111,7 @@ describe('Breadcrumb Component', () => { const view = createProductView(product, tree); component.product = view; fixture.detectChanges(); - expect(element.textContent).toMatchInlineSnapshot(`"Home/n1/n2/FakeProduct"`); + expect(element.textContent).toMatchInlineSnapshot(`"Home/1/1.2/FakeProduct"`); }); }); diff --git a/src/app/shared/components/common/breadcrumb/breadcrumb.component.ts b/src/app/shared/components/common/breadcrumb/breadcrumb.component.ts index aacfd77f952..d1ed65cae17 100644 --- a/src/app/shared/components/common/breadcrumb/breadcrumb.component.ts +++ b/src/app/shared/components/common/breadcrumb/breadcrumb.component.ts @@ -3,7 +3,6 @@ import { ChangeDetectionStrategy, Component, DoCheck, Input } from '@angular/cor import { BreadcrumbItem } from 'ish-core/models/breadcrumb-item/breadcrumb-item.interface'; import { CategoryView } from 'ish-core/models/category-view/category-view.model'; import { ProductView } from 'ish-core/models/product-view/product-view.model'; -import { generateCategoryUrl } from 'ish-core/routing/category/category.route'; @Component({ selector: 'ish-breadcrumb', @@ -26,9 +25,9 @@ export class BreadcrumbComponent implements DoCheck { const usedCategory = category ? category : product ? product.defaultCategory() : undefined; if (usedCategory) { trail.push( - ...usedCategory.pathCategories().map(cat => ({ - text: cat.name, - link: generateCategoryUrl(cat), + ...usedCategory.categoryPath.map(cat => ({ + text: cat, + link: '/category/' + cat, })) ); } diff --git a/src/app/shell/header/header-navigation/header-navigation.component.html b/src/app/shell/header/header-navigation/header-navigation.component.html index b3d56a82f03..0f588a0b37e 100644 --- a/src/app/shell/header/header-navigation/header-navigation.component.html +++ b/src/app/shell/header/header-navigation/header-navigation.component.html @@ -15,16 +15,16 @@
{{ category.name }} - + diff --git a/src/app/shell/header/header-navigation/header-navigation.component.spec.ts b/src/app/shell/header/header-navigation/header-navigation.component.spec.ts index 6b42a08d3a2..582083a27b0 100644 --- a/src/app/shell/header/header-navigation/header-navigation.component.spec.ts +++ b/src/app/shell/header/header-navigation/header-navigation.component.spec.ts @@ -71,6 +71,7 @@ describe('Header Navigation Component', () => { CAT_A @@ -84,6 +85,7 @@ describe('Header Navigation Component', () => { CAT_B @@ -97,6 +99,7 @@ describe('Header Navigation Component', () => { CAT_C diff --git a/src/app/shell/header/sub-category-navigation/__snapshots__/sub-category-navigation.component.spec.ts.snap b/src/app/shell/header/sub-category-navigation/__snapshots__/sub-category-navigation.component.spec.ts.snap deleted file mode 100644 index 0c22c696e27..00000000000 --- a/src/app/shell/header/sub-category-navigation/__snapshots__/sub-category-navigation.component.spec.ts.snap +++ /dev/null @@ -1,21 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Sub Category Navigation Component should be created 1`] = ` - -`; diff --git a/src/app/shell/header/sub-category-navigation/sub-category-navigation.component.html b/src/app/shell/header/sub-category-navigation/sub-category-navigation.component.html index 177146edc9a..64a08df7914 100644 --- a/src/app/shell/header/sub-category-navigation/sub-category-navigation.component.html +++ b/src/app/shell/header/sub-category-navigation/sub-category-navigation.component.html @@ -1,28 +1,28 @@
    diff --git a/src/app/shell/header/sub-category-navigation/sub-category-navigation.component.spec.ts b/src/app/shell/header/sub-category-navigation/sub-category-navigation.component.spec.ts index 613cfe55ca2..7ae44da6f94 100644 --- a/src/app/shell/header/sub-category-navigation/sub-category-navigation.component.spec.ts +++ b/src/app/shell/header/sub-category-navigation/sub-category-navigation.component.spec.ts @@ -2,12 +2,13 @@ import { ComponentFixture, TestBed, async } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { MockComponent } from 'ng-mocks'; +import { of } from 'rxjs'; +import { instance, mock, when } from 'ts-mockito'; import { MAIN_NAVIGATION_MAX_SUB_CATEGORIES_DEPTH } from 'ish-core/configurations/injection-keys'; -import { createCategoryView } from 'ish-core/models/category-view/category-view.model'; -import { Category } from 'ish-core/models/category/category.model'; +import { ShoppingFacade } from 'ish-core/facades/shopping.facade'; import { CategoryRoutePipe } from 'ish-core/routing/category/category-route.pipe'; -import { categoryTree } from 'ish-core/utils/dev/test-data-utils'; +import { NavigationCategory } from 'ish-core/store/shopping/categories'; import { SubCategoryNavigationComponent } from './sub-category-navigation.component'; @@ -17,10 +18,30 @@ describe('Sub Category Navigation Component', () => { let element: HTMLElement; beforeEach(async(() => { + const shoppingFacade = mock(ShoppingFacade); + + when(shoppingFacade.navigationCategories$('A')).thenReturn( + of([ + { uniqueId: 'A.1', name: 'CAT_A1', url: '/CAT_A1-catA.1', hasChildren: true }, + { uniqueId: 'A.2', name: 'CAT_A2', url: '/CAT_A2-catA.2' }, + ] as NavigationCategory[]) + ); + when(shoppingFacade.navigationCategories$('A.1')).thenReturn( + of([{ uniqueId: 'A.1.a', name: 'CAT_A1a', url: '/CAT_A1a-catA.1.a', hasChildren: true }] as NavigationCategory[]) + ); + when(shoppingFacade.navigationCategories$('A.1.a')).thenReturn( + of([ + { uniqueId: 'A.1.a.alpha', name: 'CAT_A1aAlpha', url: '/CAT_A1aAlpha-catA.1.a.alpha' }, + ] as NavigationCategory[]) + ); + TestBed.configureTestingModule({ imports: [RouterTestingModule], declarations: [CategoryRoutePipe, MockComponent(FaIconComponent), SubCategoryNavigationComponent], - providers: [{ provide: MAIN_NAVIGATION_MAX_SUB_CATEGORIES_DEPTH, useValue: 2 }], + providers: [ + { provide: MAIN_NAVIGATION_MAX_SUB_CATEGORIES_DEPTH, useValue: 2 }, + { provide: ShoppingFacade, useFactory: () => instance(shoppingFacade) }, + ], }).compileComponents(); })); @@ -29,15 +50,7 @@ describe('Sub Category Navigation Component', () => { component = fixture.componentInstance; element = fixture.nativeElement; - const tree = categoryTree([ - { uniqueId: 'A', name: 'CAT_A', categoryPath: ['A'] }, - { uniqueId: 'A.1', name: 'CAT_A1', categoryPath: ['A', 'A.1'] }, - { uniqueId: 'A.2', name: 'CAT_A2', categoryPath: ['A', 'A.2'] }, - { uniqueId: 'A.1.a', name: 'CAT_A1a', categoryPath: ['A', 'A.1', 'A.1.a'] }, - { uniqueId: 'A.1.a.alpha', name: 'CAT_A1aAlpha', categoryPath: ['A', 'A.1', 'A.1.a', 'A.1.a.alpha'] }, - ] as Category[]); - - component.category = createCategoryView(tree, 'A'); + component.categoryUniqueId = 'A'; component.subCategoriesDepth = 1; }); @@ -45,6 +58,29 @@ describe('Sub Category Navigation Component', () => { expect(component).toBeTruthy(); expect(element).toBeTruthy(); expect(() => fixture.detectChanges()).not.toThrow(); - expect(element).toMatchSnapshot(); + expect(element).toMatchInlineSnapshot(` + + `); }); }); diff --git a/src/app/shell/header/sub-category-navigation/sub-category-navigation.component.ts b/src/app/shell/header/sub-category-navigation/sub-category-navigation.component.ts index 5611f66effa..73f230b0be8 100644 --- a/src/app/shell/header/sub-category-navigation/sub-category-navigation.component.ts +++ b/src/app/shell/header/sub-category-navigation/sub-category-navigation.component.ts @@ -1,47 +1,48 @@ -import { ChangeDetectionStrategy, Component, Inject, Input } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Inject, Input, OnInit } from '@angular/core'; +import { Observable } from 'rxjs'; import { MAIN_NAVIGATION_MAX_SUB_CATEGORIES_DEPTH } from 'ish-core/configurations/injection-keys'; -import { CategoryView } from 'ish-core/models/category-view/category-view.model'; -import { Category } from 'ish-core/models/category/category.model'; +import { ShoppingFacade } from 'ish-core/facades/shopping.facade'; +import { NavigationCategory } from 'ish-core/store/shopping/categories'; /** * The Sub Category Navigation Component displays second level category navigation. - * - * @example - * - * */ @Component({ selector: 'ish-sub-category-navigation', templateUrl: './sub-category-navigation.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class SubCategoryNavigationComponent { +export class SubCategoryNavigationComponent implements OnInit { @Input() view = 'auto'; - @Input() category: CategoryView; + @Input() categoryUniqueId: string; @Input() subCategoriesDepth: number; - openedCategories = []; + openedCategories: string[] = []; - constructor(@Inject(MAIN_NAVIGATION_MAX_SUB_CATEGORIES_DEPTH) public mainNavigationMaxSubCategoriesDepth: number) {} + navigationCategories$: Observable; + + constructor( + private shoppingFacade: ShoppingFacade, + @Inject(MAIN_NAVIGATION_MAX_SUB_CATEGORIES_DEPTH) public mainNavigationMaxSubCategoriesDepth: number + ) {} + + ngOnInit() { + this.navigationCategories$ = this.shoppingFacade.navigationCategories$(this.categoryUniqueId); + } /** * Indicate if specific category is expanded. - * @param category The category item. */ - isOpened(category: Category): boolean { - return this.openedCategories.includes(category.uniqueId); + isOpened(uniqueId: string): boolean { + return this.openedCategories.includes(uniqueId); } /** * Toggle category open state. - * @param category The category item. */ - toggleOpen(category: Category) { - const index = this.openedCategories.findIndex(id => id === category.uniqueId); - index > -1 ? this.openedCategories.splice(index, 1) : this.openedCategories.push(category.uniqueId); + toggleOpen(uniqueId: string) { + const index = this.openedCategories.findIndex(id => id === uniqueId); + index > -1 ? this.openedCategories.splice(index, 1) : this.openedCategories.push(uniqueId); } }