From c78209e679c0e71f7226ed7310d9c894ff0695a8 Mon Sep 17 00:00:00 2001 From: Danilo Hoffmann Date: Wed, 17 Jun 2020 12:47:22 +0200 Subject: [PATCH] perf: correctly memoize category selectors --- src/app/core/facades/shopping.facade.ts | 16 +- .../category-tree.helper.spec.ts | 185 ++++++++++++++++++ .../category-tree/category-tree.helper.ts | 49 +++++ .../category-view/category-view.model.spec.ts | 32 +-- .../category-view/category-view.model.ts | 10 +- .../models/category/category.helper.spec.ts | 32 --- .../core/models/category/category.helper.ts | 10 - .../navigation-category.model.ts | 6 + .../categories/categories.selectors.spec.ts | 91 +++++++-- .../categories/categories.selectors.ts | 40 +++- .../store/shopping/shopping-store.spec.ts | 8 - .../category-categories.component.html | 4 +- .../category-list.component.html | 4 +- .../category-list.component.spec.ts | 15 +- .../category-list/category-list.component.ts | 9 +- .../category-navigation.component.html | 15 +- .../category-navigation.component.spec.ts | 62 +++++- .../category-navigation.component.ts | 26 ++- .../category/category-page.component.html | 4 +- .../category-tile.component.html | 6 +- .../category-tile.component.spec.ts | 44 +++-- .../category-tile/category-tile.component.ts | 18 +- .../header-navigation.component.html | 26 +-- .../header-navigation.component.spec.ts | 61 ++---- .../header-navigation.component.ts | 17 +- ...category-navigation.component.spec.ts.snap | 21 -- .../sub-category-navigation.component.html | 18 +- .../sub-category-navigation.component.spec.ts | 60 ++++-- .../sub-category-navigation.component.ts | 43 ++-- 29 files changed, 629 insertions(+), 303 deletions(-) create mode 100644 src/app/core/models/navigation-category/navigation-category.model.ts 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 7bb5b68c80..5b332393e8 100644 --- a/src/app/core/facades/shopping.facade.ts +++ b/src/app/core/facades/shopping.facade.ts @@ -6,7 +6,12 @@ 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, +} from 'ish-core/store/shopping/categories'; import { addToCompare, getCompareProducts, @@ -50,10 +55,17 @@ export class ShoppingFacade { // CATEGORY - topLevelCategories$ = this.store.pipe(select(getTopLevelCategories)); 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-tree/category-tree.helper.spec.ts b/src/app/core/models/category-tree/category-tree.helper.spec.ts index fff792d292..05c1560cab 100644 --- a/src/app/core/models/category-tree/category-tree.helper.spec.ts +++ b/src/app/core/models/category-tree/category-tree.helper.spec.ts @@ -5,6 +5,7 @@ import * as using from 'jasmine-data-provider'; import { CategoryData } from 'ish-core/models/category/category.interface'; import { CategoryMapper } from 'ish-core/models/category/category.mapper'; import { Category } from 'ish-core/models/category/category.model'; +import { categoryTree } from 'ish-core/utils/dev/test-data-utils'; import { CategoryTreeHelper } from './category-tree.helper'; import { CategoryTree } from './category-tree.model'; @@ -458,4 +459,188 @@ describe('Category Tree Helper', () => { } ); }); + + describe('subTree', () => { + let combined: CategoryTree; + + beforeEach(() => { + combined = categoryTree([ + { uniqueId: 'A', categoryPath: ['A'] }, + { uniqueId: 'A.1', categoryPath: ['A', 'A.1'] }, + { uniqueId: 'A.1.a', categoryPath: ['A', 'A.1', 'A.1.a'] }, + { uniqueId: 'A.2', categoryPath: ['A', 'A.2'] }, + { uniqueId: 'A.1.b', categoryPath: ['A', 'A.1', 'A.1.b'] }, + { uniqueId: 'B', categoryPath: ['B'] }, + { uniqueId: 'B.1', categoryPath: ['B', 'B.1'] }, + { uniqueId: 'B.1.a', categoryPath: ['B', 'B.1', 'B.1.a'] }, + { uniqueId: 'B.2', categoryPath: ['B', 'B.2'] }, + ] as Category[]); + }); + + it('should be created', () => { + expect(combined).toMatchInlineSnapshot(` + ├─ A + │ ├─ A.1 + │ │ ├─ A.1.a + │ │ └─ A.1.b + │ └─ A.2 + └─ B + ├─ B.1 + │ └─ B.1.a + └─ B.2 + + `); + + expect(combined.rootIds).toMatchInlineSnapshot(` + Array [ + "A", + "B", + ] + `); + expect(Object.keys(combined.nodes)).toMatchInlineSnapshot(` + Array [ + "A", + "A.1", + "A.1.a", + "A.2", + "A.1.b", + "B", + "B.1", + "B.1.a", + "B.2", + ] + `); + expect(Object.keys(combined.edges)).toMatchInlineSnapshot(` + Array [ + "A", + "A.1", + "B", + "B.1", + ] + `); + }); + + it('should return an empty tree if selected uniqueId is not part of the tree', () => { + const tree = CategoryTreeHelper.subTree(combined, 'C'); + expect(tree.rootIds).toBeEmpty(); + expect(tree.edges).toBeEmpty(); + expect(tree.nodes).toBeEmpty(); + }); + + it('should return copy of tree if selector is undefined', () => { + const tree = CategoryTreeHelper.subTree(combined, undefined); + expect(CategoryTreeHelper.equals(tree, combined)).toBeTrue(); + }); + + it('should extract sub tree for A', () => { + const tree = CategoryTreeHelper.subTree(combined, 'A'); + + expect(tree).toMatchInlineSnapshot(` + └─ A + ├─ A.1 + │ ├─ A.1.a + │ └─ A.1.b + └─ A.2 + + `); + + expect(tree.rootIds).toMatchInlineSnapshot(` + Array [ + "A", + ] + `); + expect(Object.keys(tree.nodes)).toMatchInlineSnapshot(` + Array [ + "A", + "A.1", + "A.1.a", + "A.2", + "A.1.b", + ] + `); + expect(Object.keys(tree.edges)).toMatchInlineSnapshot(` + Array [ + "A", + "A.1", + ] + `); + }); + + it('should extract sub tree for A.1', () => { + const tree = CategoryTreeHelper.subTree(combined, 'A.1'); + + expect(tree).toMatchInlineSnapshot(` + └─ dangling + └─ A.1 + ├─ A.1.a + └─ A.1.b + + `); + + expect(tree.rootIds).toBeEmpty(); + expect(Object.keys(tree.nodes)).toMatchInlineSnapshot(` + Array [ + "A.1", + "A.1.a", + "A.1.b", + ] + `); + expect(Object.keys(tree.edges)).toMatchInlineSnapshot(` + Array [ + "A.1", + ] + `); + }); + + it('should extract sub tree for A.1.a', () => { + const tree = CategoryTreeHelper.subTree(combined, 'A.1.a'); + + expect(tree).toMatchInlineSnapshot(` + └─ dangling + └─ A.1.a + + `); + + expect(tree.rootIds).toBeEmpty(); + expect(Object.keys(tree.nodes)).toMatchInlineSnapshot(` + Array [ + "A.1.a", + ] + `); + expect(Object.keys(tree.edges)).toBeEmpty(); + }); + }); + + describe('equals', () => { + const catA = { uniqueId: 'A', categoryPath: ['A'] } as Category; + const catB = { uniqueId: 'B', categoryPath: ['B'] } as Category; + + it('should return true for simple equal trees', () => { + const tree1 = categoryTree([catA]); + const tree2 = categoryTree([catA]); + + expect(CategoryTreeHelper.equals(tree1, tree2)).toBeTrue(); + }); + + it('should return true if category was copied', () => { + const tree1 = categoryTree([catA]); + const tree2 = categoryTree([{ ...catA }]); + + expect(CategoryTreeHelper.equals(tree1, tree2)).toBeTrue(); + }); + + it('should return false for simple unequal trees', () => { + const tree1 = categoryTree([catA]); + const tree2 = categoryTree([catB]); + + expect(CategoryTreeHelper.equals(tree1, tree2)).toBeFalse(); + }); + + it('should return true for simple unordered trees', () => { + const tree1 = categoryTree([catA, catB]); + const tree2 = categoryTree([catB, catA]); + + expect(CategoryTreeHelper.equals(tree1, tree2)).toBeTrue(); + }); + }); }); diff --git a/src/app/core/models/category-tree/category-tree.helper.ts b/src/app/core/models/category-tree/category-tree.helper.ts index 18f7236539..a61ce3a13e 100644 --- a/src/app/core/models/category-tree/category-tree.helper.ts +++ b/src/app/core/models/category-tree/category-tree.helper.ts @@ -1,3 +1,5 @@ +import { isEqual, pick } from 'lodash-es'; + import { Category } from 'ish-core/models/category/category.model'; import { CategoryTree } from './category-tree.model'; @@ -131,4 +133,51 @@ export class CategoryTreeHelper { const singleCategoryTree = CategoryTreeHelper.single(category); return CategoryTreeHelper.merge(tree, singleCategoryTree); } + + /** + * Extract a sub tree. + */ + static subTree(tree: CategoryTree, uniqueId: string): CategoryTree { + if (!uniqueId) { + return tree; + } + + const select = (e: string) => e.startsWith(uniqueId); + return { + rootIds: tree.rootIds.filter(select), + edges: pick(tree.edges, ...Object.keys(tree.edges).filter(select)), + nodes: pick(tree.nodes, ...Object.keys(tree.nodes).filter(select)), + }; + } + + private static rootIdsEqual(t1: string[], t2: string[]) { + return t1.length === t2.length && t1.every(e => t2.includes(e)); + } + + private static edgesEqual(t1: { [id: string]: string[] }, t2: { [id: string]: string[] }) { + return isEqual(t1, t2); + } + + private static categoriesEqual(t1: { [id: string]: Category }, t2: { [id: string]: Category }) { + const keys1 = Object.keys(t1); + const keys2 = Object.keys(t2); + return ( + keys1.length === keys2.length && + keys1.every(id => keys2.includes(id)) && + keys1.every(id => isEqual(t1[id], t2[id])) + ); + } + + /** + * Perform check for equality. Order of items is ignored. + */ + static equals(tree1: CategoryTree, tree2: CategoryTree): boolean { + return ( + tree1 && + tree2 && + CategoryTreeHelper.rootIdsEqual(tree1.rootIds, tree2.rootIds) && + CategoryTreeHelper.edgesEqual(tree1.edges, tree2.edges) && + CategoryTreeHelper.categoriesEqual(tree1.nodes, tree2.nodes) + ); + } } 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 c5257421d2..7ac7e4402b 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 77700d423f..81807271fc 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/models/category/category.helper.spec.ts b/src/app/core/models/category/category.helper.spec.ts index a7b2b7c8e5..8a689077a6 100644 --- a/src/app/core/models/category/category.helper.spec.ts +++ b/src/app/core/models/category/category.helper.spec.ts @@ -4,38 +4,6 @@ import { CategoryCompletenessLevel, CategoryHelper } from './category.helper'; import { Category } from './category.model'; describe('Category Helper', () => { - describe('equals', () => { - const dataProvider = () => { - const emptyCategory = {} as Category; - const category1 = { uniqueId: '1' } as Category; - const category2 = { uniqueId: '2' } as Category; - const category3 = { name: 'dummy', uniqueId: '1' } as Category; - const category4 = { name: 'other', uniqueId: '1' } as Category; - const category5 = { name: 'd', uniqueId: '0' } as Category; - return [ - { cat1: emptyCategory, cat2: undefined, result: false }, - { cat1: emptyCategory, cat2: undefined, result: false }, - { cat1: emptyCategory, cat2: emptyCategory, result: true }, - { cat1: category1, cat2: category1, result: true }, - { cat1: category2, cat2: category1, result: false }, - { cat1: emptyCategory, cat2: category1, result: false }, - { cat1: category1, cat2: emptyCategory, result: false }, - { cat1: category5, cat2: category1, result: false }, - { cat1: category1, cat2: category5, result: false }, - { cat1: category1, cat2: category3, result: true }, - { cat1: category3, cat2: category4, result: true }, - ]; - }; - - using(dataProvider, slice => { - it(`should return ${slice.result} when comparing '${JSON.stringify(slice.cat1)}' and '${JSON.stringify( - slice.cat2 - )}'`, () => { - expect(CategoryHelper.equals(slice.cat1, slice.cat2)).toBe(slice.result); - }); - }); - }); - describe('getCategoryPath', () => { function dataProvider() { return [ diff --git a/src/app/core/models/category/category.helper.ts b/src/app/core/models/category/category.helper.ts index 3e2249f7e2..920ac4d7bf 100644 --- a/src/app/core/models/category/category.helper.ts +++ b/src/app/core/models/category/category.helper.ts @@ -7,16 +7,6 @@ export enum CategoryCompletenessLevel { export class CategoryHelper { static uniqueIdSeparator = '.'; - /** - * @returns True if the categories are equal, false if not. - * 'equal' means - * - both categories are defined - * - the uniqueId of the categories is the same - */ - static equals(self: Category, category: Category): boolean { - return !!self && !!category && self.uniqueId === category.uniqueId; - } - /** * Converts a given uniqueId of a category in a REST API category path. * @example diff --git a/src/app/core/models/navigation-category/navigation-category.model.ts b/src/app/core/models/navigation-category/navigation-category.model.ts new file mode 100644 index 0000000000..acfa218dbf --- /dev/null +++ b/src/app/core/models/navigation-category/navigation-category.model.ts @@ -0,0 +1,6 @@ +export interface NavigationCategory { + uniqueId: string; + name: string; + url: string; + hasChildren: boolean; +} 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 af2081be01..c06ef8cbfd 100644 --- a/src/app/core/store/shopping/categories/categories.selectors.spec.ts +++ b/src/app/core/store/shopping/categories/categories.selectors.spec.ts @@ -20,10 +20,11 @@ import { } from './categories.actions'; import { getBreadcrumbForCategoryPage, + getCategory, getCategoryEntities, getCategoryLoading, + getNavigationCategories, getSelectedCategory, - getTopLevelCategories, isTopLevelCategoriesLoaded, } from './categories.selectors'; @@ -75,10 +76,10 @@ describe('Categories Selectors', () => { it('should not select any selected category when used', () => { expect(getSelectedCategory(store$.state)).toBeUndefined(); + expect(getCategory(catA.uniqueId)(store$.state)).toBeUndefined(); }); - it('should not select any top level categories when used', () => { - expect(getTopLevelCategories(store$.state)).toBeEmpty(); + it('should not have any top level categories loaded when used', () => { expect(isTopLevelCategoriesLoaded(store$.state)).toBeFalse(); }); }); @@ -125,6 +126,7 @@ describe('Categories Selectors', () => { it('should return the category information when used', () => { expect(getCategoryEntities(store$.state)).toHaveProperty(catA.uniqueId); expect(getCategoryLoading(store$.state)).toBeFalse(); + expect(getCategory(catA.uniqueId)(store$.state).uniqueId).toEqual(catA.uniqueId); }); it('should not select the irrelevant category when used', () => { @@ -145,6 +147,7 @@ describe('Categories Selectors', () => { it('should return the category information when used', () => { expect(getCategoryEntities(store$.state)).toHaveProperty(catA.uniqueId); expect(getCategoryLoading(store$.state)).toBeFalse(); + expect(getCategory(catA.uniqueId)(store$.state).uniqueId).toEqual(catA.uniqueId); }); it('should select the selected category when used', () => { @@ -192,22 +195,80 @@ describe('Categories Selectors', () => { describe('loading top level categories', () => { beforeEach(() => { - store$.dispatch( - loadTopLevelCategoriesSuccess({ - categories: categoryTree([ - { uniqueId: 'A', categoryPath: ['A'] }, - { uniqueId: 'B', categoryPath: ['B'] }, - ] as Category[]), - }) - ); - }); - - it('should select root categories when used', () => { - expect(getTopLevelCategories(store$.state).map(x => x.uniqueId)).toEqual(['A', 'B']); + const cA = { name: 'name_A', uniqueId: 'A', categoryPath: ['A'] } as Category; + const cA1 = { name: 'name_A.1', uniqueId: 'A.1', categoryPath: ['A', 'A.1'] } as Category; + const cA1a = { name: 'name_A.1.a', uniqueId: 'A.1.a', categoryPath: ['A', 'A.1', 'A.1.a'] } as Category; + const cA1b = { name: 'name_A.1.b', uniqueId: 'A.1.b', categoryPath: ['A', 'A.1', 'A.1.b'] } as Category; + const cA2 = { name: 'name_A.2', uniqueId: 'A.2', categoryPath: ['A', 'A.2'] } as Category; + const cB = { name: 'name_B', uniqueId: 'B', categoryPath: ['B'] } as Category; + store$.dispatch(loadTopLevelCategoriesSuccess({ categories: categoryTree([cA, cA1, cA1a, cA1b, cA2, cB]) })); }); 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 191b33b3e6..41d2eb2dfb 100644 --- a/src/app/core/store/shopping/categories/categories.selectors.ts +++ b/src/app/core/store/shopping/categories/categories.selectors.ts @@ -2,8 +2,10 @@ import { Dictionary } from '@ngrx/entity'; 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 { CategoryView, createCategoryView } from 'ish-core/models/category-view/category-view.model'; import { Category, CategoryHelper } from 'ish-core/models/category/category.model'; +import { NavigationCategory } from 'ish-core/models/navigation-category/navigation-category.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'; @@ -17,10 +19,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 @@ -28,10 +41,6 @@ export const getSelectedCategory = createSelector( export const getCategoryLoading = createSelector(getCategoriesState, categories => categories.loading); -export const getTopLevelCategories = createSelector(getCategoryTree, tree => - tree.rootIds.map(id => createCategoryView(tree, id)) -); - export const isTopLevelCategoriesLoaded = createSelector(getCategoriesState, state => state.topLevelLoaded); export const getBreadcrumbForCategoryPage = createSelectorFactory(projector => @@ -47,3 +56,24 @@ export const getBreadcrumbForCategoryPage = createSelectorFactory(projector => })) : undefined ); + +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 974dcac9f5..554c565127 100644 --- a/src/app/core/store/shopping/shopping-store.spec.ts +++ b/src/app/core/store/shopping/shopping-store.spec.ts @@ -491,12 +491,8 @@ describe('Shopping Store', () => { categories: tree(A,A.123) [Categories API] Load Category Success: categories: tree(A.123,A.123.456) - [Categories Internal] Load Category: - categoryId: "A.123" [Viewconf Internal] Set Breadcrumb Data: breadcrumbData: [{"text":"nA","link":"/nA-catA"},{"text":"nA123","link":"/nA... - [Categories API] 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"} @@ -735,10 +731,6 @@ describe('Shopping Store', () => { categories: tree(A,A.123) [Categories API] Load Category Success: categories: tree(A.123,A.123.456) - [Categories Internal] Load Category: - categoryId: "A.123" - [Categories API] 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 db11a95f65..d3cbef4601 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 e3dd78bda4..4f76e8d382 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.spec.ts b/src/app/pages/category/category-list/category-list.component.spec.ts index e4761010cf..56e978438e 100644 --- a/src/app/pages/category/category-list/category-list.component.spec.ts +++ b/src/app/pages/category/category-list/category-list.component.spec.ts @@ -2,8 +2,6 @@ import { ComponentFixture, TestBed, async } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { MockComponent } from 'ng-mocks'; -import { Category } from 'ish-core/models/category/category.model'; - import { CategoryTileComponent } from '../category-tile/category-tile.component'; import { CategoryListComponent } from './category-list.component'; @@ -24,10 +22,7 @@ describe('Category List Component', () => { fixture = TestBed.createComponent(CategoryListComponent); component = fixture.componentInstance; element = fixture.nativeElement; - component.categories = [ - { uniqueId: 'uid1', name: 'name1', images: [{ effectiveUrl: '/url1.png' }] }, - { uniqueId: 'uid2', name: 'name2', images: [{ effectiveUrl: '/url2.png' }] }, - ] as Category[]; + component.categories = ['uid1', 'uid2']; }); it('should be created', () => { @@ -36,8 +31,12 @@ describe('Category List Component', () => { expect(() => fixture.detectChanges()).not.toThrow(); expect(element).toMatchInlineSnapshot(` `); }); 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 7139f2bb0a..647e2e3a94 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 83673a98bf..96e145056f 100644 --- a/src/app/pages/category/category-navigation/category-navigation.component.html +++ b/src/app/pages/category/category-navigation/category-navigation.component.html @@ -1,18 +1,17 @@ -
+
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 13fff5ab2d..dc4b3bb1ba 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 { NavigationCategory } from 'ish-core/models/navigation-category/navigation-category.model'; import { CategoryRoutePipe } from 'ish-core/routing/category/category-route.pipe'; -import { categoryTree } from 'ish-core/utils/dev/test-data-utils'; import { CategoryNavigationComponent } from './category-navigation.component'; @@ -15,27 +16,70 @@ 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'); }); })); it('should be created', () => { expect(component).toBeTruthy(); expect(element).toBeTruthy(); + expect(() => component.ngOnChanges()).not.toThrow(); expect(() => fixture.detectChanges()).not.toThrow(); }); + + it('should create all links for tree', () => { + component.ngOnChanges(); + fixture.detectChanges(); + + expect(element.querySelectorAll('a')).toMatchInlineSnapshot(` + NodeList [ + nA , + + nA.1 + , + nA.2 , + nB , + ] + `); + }); + + it('should create all links for top level category', () => { + component.uniqueId = 'A'; + component.ngOnChanges(); + fixture.detectChanges(); + + expect(element.querySelectorAll('a')).toMatchInlineSnapshot(` + NodeList [ + + nA.1 + , + nA.2 , + ] + `); + }); }); 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 2f57f9de15..9e22e45b28 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, OnChanges, 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 { CategoryHelper } from 'ish-core/models/category/category.model'; +import { ShoppingFacade } from 'ish-core/facades/shopping.facade'; +import { NavigationCategory } from 'ish-core/models/navigation-category/navigation-category.model'; @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, OnChanges { + @Input() uniqueId: string; - categoryEquals = CategoryHelper.equals; + navigationCategories$: Observable; + currentCategoryId$: Observable; + + constructor(private shoppingFacade: ShoppingFacade) {} + + ngOnInit() { + this.currentCategoryId$ = this.shoppingFacade.selectedCategory$.pipe(map(c => c?.uniqueId)); + } + + ngOnChanges() { + this.navigationCategories$ = this.shoppingFacade.navigationCategories$(this.uniqueId); + } } diff --git a/src/app/pages/category/category-page.component.html b/src/app/pages/category/category-page.component.html index b6061da61e..1d94df947e 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 d5caa19b7c..657ef39b06 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 170e778149..9166b6a7fa 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 e686f5d793..36e292b7c7 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/shell/header/header-navigation/header-navigation.component.html b/src/app/shell/header/header-navigation/header-navigation.component.html index b3d56a82f0..73da88917d 100644 --- a/src/app/shell/header/header-navigation/header-navigation.component.html +++ b/src/app/shell/header/header-navigation/header-navigation.component.html @@ -7,26 +7,28 @@ *ngFor="let category of categories$ | async" #submenu class="dropdown" - [ngClass]="{ open: isOpened(category) }" + [ngClass]="{ open: isOpened(category.uniqueId) }" (mouseover)="subMenuShow(submenu)" (mouseleave)="subMenuHide(submenu)" (click)="subMenuHide(submenu)" >
{{ 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 6b42a08d3a..369210c812 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 @@ -6,10 +6,8 @@ import { of } from 'rxjs'; import { 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 { NavigationCategory } from 'ish-core/models/navigation-category/navigation-category.model'; import { CategoryRoutePipe } from 'ish-core/routing/category/category-route.pipe'; -import { categoryTree } from 'ish-core/utils/dev/test-data-utils'; import { SubCategoryNavigationComponent } from 'ish-shell/header/sub-category-navigation/sub-category-navigation.component'; import { HeaderNavigationComponent } from './header-navigation.component'; @@ -40,19 +38,12 @@ describe('Header Navigation Component', () => { component = fixture.componentInstance; element = fixture.nativeElement; - const categories = categoryTree([ - { uniqueId: 'A', name: 'CAT_A', categoryPath: ['A'] }, - { uniqueId: 'B', name: 'CAT_B', categoryPath: ['B'] }, - { uniqueId: 'C', name: 'CAT_C', categoryPath: ['C'] }, - ] as Category[]); - - const topLevelCategories = [ - createCategoryView(categories, 'A'), - createCategoryView(categories, 'B'), - createCategoryView(categories, 'C'), - ]; - - when(shoppingFacade.topLevelCategories$).thenReturn(of(topLevelCategories)); + const categories = [ + { uniqueId: 'A', name: 'CAT_A', url: '/cat/A', hasChildren: true }, + { uniqueId: 'B', name: 'CAT_B', url: '/cat/B' }, + { uniqueId: 'C', name: 'CAT_C', url: '/cat/C' }, + ] as NavigationCategory[]; + when(shoppingFacade.navigationCategories$()).thenReturn(of(categories)); }); it('should be created', () => { @@ -62,43 +53,23 @@ describe('Header Navigation Component', () => { expect(element).toMatchInlineSnapshot(` `); diff --git a/src/app/shell/header/header-navigation/header-navigation.component.ts b/src/app/shell/header/header-navigation/header-navigation.component.ts index 5927c8c349..d09560451a 100644 --- a/src/app/shell/header/header-navigation/header-navigation.component.ts +++ b/src/app/shell/header/header-navigation/header-navigation.component.ts @@ -2,8 +2,7 @@ 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'; -import { Category } from 'ish-core/models/category/category.model'; +import { NavigationCategory } from 'ish-core/models/navigation-category/navigation-category.model'; @Component({ selector: 'ish-header-navigation', @@ -13,14 +12,14 @@ import { Category } from 'ish-core/models/category/category.model'; export class HeaderNavigationComponent implements OnInit { @Input() view: 'auto' | 'small' | 'full' = 'auto'; - categories$: Observable; + categories$: Observable; openedCategories = []; constructor(private shoppingFacade: ShoppingFacade) {} ngOnInit() { - this.categories$ = this.shoppingFacade.topLevelCategories$; + this.categories$ = this.shoppingFacade.navigationCategories$(); } /** @@ -45,16 +44,16 @@ export class HeaderNavigationComponent implements OnInit { * 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); } } 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 0c22c696e2..0000000000 --- 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 177146edc9..faf3a27408 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 613cfe55ca..902f76a166 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 { NavigationCategory } from 'ish-core/models/navigation-category/navigation-category.model'; import { CategoryRoutePipe } from 'ish-core/routing/category/category-route.pipe'; -import { categoryTree } from 'ish-core/utils/dev/test-data-utils'; 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,25 @@ 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 5611f66eff..4b36a634c9 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/models/navigation-category/navigation-category.model'; /** * 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); } }