diff --git a/apps/webapp/src/styles/fu/_var.scss b/apps/webapp/src/styles/fu/_var.scss index ad193c877..d4c625aff 100755 --- a/apps/webapp/src/styles/fu/_var.scss +++ b/apps/webapp/src/styles/fu/_var.scss @@ -54,7 +54,8 @@ $config: mat-typography-config( $body-1: mat-typography-level(14px, 22px, 400), $caption: mat-typography-level(13px, 22px, 400), $button: mat-typography-level(14px, 14px, 500), - // Line-height must be unit-less fraction of the font-size. $input: mat-typography-level(16px, 1.125, 400), + // Line-height must be unit-less fraction of the font-size. + $input: mat-typography-level(16px, 1.125, 400) ); /** @@ -184,9 +185,7 @@ $sidenav-item-padding-left-level5: $sidenav-item-padding-left-level4 + 8px; $sidenav-item-padding-left-level6: $sidenav-item-padding-left-level5 + 8px; $sidenav-width: 270px; // If you change this, you also need to adjust the animations in sidenav.component.ts -$sidenav-collapsed-width: ( - $sidenav-item-padding-left + $sidenav-item-padding-right + $sidenav-item-icon-font-size -); // If you change this, you also need to adjust the animations in sidenav.component.ts +$sidenav-collapsed-width: ($sidenav-item-padding-left + $sidenav-item-padding-right + $sidenav-item-icon-font-size); // If you change this, you also need to adjust the animations in sidenav.component.ts $sidenav-z-index: 1000; /** diff --git a/libs/breadcrumbs/src/lib/breadcrumbs.service.ts b/libs/breadcrumbs/src/lib/breadcrumbs.service.ts deleted file mode 100644 index 273094027..000000000 --- a/libs/breadcrumbs/src/lib/breadcrumbs.service.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Injectable } from '@angular/core'; - -@Injectable({ - providedIn: 'root', -}) -export class BreadcrumbsService { - constructor() {} -} diff --git a/libs/core/src/lib/state/custom-router-state.serializer.ts b/libs/core/src/lib/state/custom-router-state.serializer.ts new file mode 100644 index 000000000..29f8a0df0 --- /dev/null +++ b/libs/core/src/lib/state/custom-router-state.serializer.ts @@ -0,0 +1,25 @@ +import { Params, RouterStateSnapshot } from '@angular/router'; +import { NgxsModule } from '@ngxs/store'; +import { NgxsRouterPluginModule, RouterStateSerializer } from '@ngxs/router-plugin'; + +export interface RouterStateParams { + url: string; + params: Params; + queryParams: Params; +} + +// Map the router snapshot to { url, params, queryParams } +export class CustomRouterStateSerializer implements RouterStateSerializer { + serialize(routerState: RouterStateSnapshot): RouterStateParams { + const { + url, + root: { queryParams }, + } = routerState; + let { root: route } = routerState; + while (route.firstChild) { + route = route.firstChild; + } + const { params } = route; + return { url, params, queryParams }; + } +} diff --git a/libs/ngx-pipes/jest.config.js b/libs/ngx-pipes/jest.config.js deleted file mode 100644 index b9c5a9857..000000000 --- a/libs/ngx-pipes/jest.config.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - name: 'ngx-pipes', - preset: '../../jest.config.js', - coverageDirectory: '../../coverage/libs/ngx-pipes' -}; diff --git a/libs/ngx-pipes/ng-package.json b/libs/ngx-pipes/ng-package.json deleted file mode 100644 index 9476b4889..000000000 --- a/libs/ngx-pipes/ng-package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", - "dest": "../../dist/libs/ngx-pipes", - "deleteDestPath": false, - "lib": { - "entryFile": "src/index.ts" - } -} diff --git a/libs/ngx-pipes/package.json b/libs/ngx-pipes/package.json deleted file mode 100644 index fded4111d..000000000 --- a/libs/ngx-pipes/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "ngx-pipes", - "version": "0.0.1", - "peerDependencies": { - "@angular/common": "^6.0.0-rc.0 || ^6.0.0", - "@angular/core": "^6.0.0-rc.0 || ^6.0.0" - } -} - diff --git a/libs/ngx-pipes/src/index.ts b/libs/ngx-pipes/src/index.ts deleted file mode 100644 index 3302459f1..000000000 --- a/libs/ngx-pipes/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './lib/ngx-pipes.module'; diff --git a/libs/ngx-pipes/src/lib/ngx-pipes.module.spec.ts b/libs/ngx-pipes/src/lib/ngx-pipes.module.spec.ts deleted file mode 100644 index cf20995b6..000000000 --- a/libs/ngx-pipes/src/lib/ngx-pipes.module.spec.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { NgxPipesModule } from './ngx-pipes.module'; - -describe('NgxPipesModule', () => { - it('should work', () => { - expect(new NgxPipesModule()).toBeDefined(); - }); -}); diff --git a/libs/ngx-pipes/src/lib/ngx-pipes.module.ts b/libs/ngx-pipes/src/lib/ngx-pipes.module.ts deleted file mode 100644 index 9ed69fc10..000000000 --- a/libs/ngx-pipes/src/lib/ngx-pipes.module.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { CharactersPipe } from './truncate/characters.pipe'; -import { WordsPipe } from './truncate/words.pipe'; -import { SafeHtmlPipe } from './helper/saft-html.pipe'; -import { FilterPipe } from './helper/filter.pipe'; -import { GroupByPipe } from './helper/group-by.pipe'; - -export const PIPES = [CharactersPipe, WordsPipe, SafeHtmlPipe, FilterPipe, GroupByPipe]; - -@NgModule({ - imports: [CommonModule], - declarations: [PIPES], - exports: [PIPES], -}) -export class NgxPipesModule {} diff --git a/libs/ngx-utils/README.md b/libs/ngx-utils/README.md new file mode 100644 index 000000000..cac419fb7 --- /dev/null +++ b/libs/ngx-utils/README.md @@ -0,0 +1,4 @@ +ngx-utils +========= + +same as `@ngrx-utils/store` without dependency on `@ngrx/store` diff --git a/libs/ngx-utils/jest.config.js b/libs/ngx-utils/jest.config.js new file mode 100644 index 000000000..239c58ff2 --- /dev/null +++ b/libs/ngx-utils/jest.config.js @@ -0,0 +1,5 @@ +module.exports = { + name: 'ngx-utils', + preset: '../../jest.config.js', + coverageDirectory: '../../coverage/libs/ngx-utils' +}; diff --git a/libs/ngx-pipes/ng-package.prod.json b/libs/ngx-utils/ng-package.json similarity index 75% rename from libs/ngx-pipes/ng-package.prod.json rename to libs/ngx-utils/ng-package.json index 2fac30618..2099fb48d 100644 --- a/libs/ngx-pipes/ng-package.prod.json +++ b/libs/ngx-utils/ng-package.json @@ -1,6 +1,6 @@ { "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", - "dest": "../../dist/libs/ngx-pipes", + "dest": "../../dist/libs/ngx-utils", "lib": { "entryFile": "src/index.ts" } diff --git a/libs/ngx-utils/package.json b/libs/ngx-utils/package.json new file mode 100644 index 000000000..c1a6106ca --- /dev/null +++ b/libs/ngx-utils/package.json @@ -0,0 +1,8 @@ +{ + "name": "ngx-utils", + "version": "0.0.1", + "peerDependencies": { + "@angular/common": "^7.0.0", + "@angular/core": "^7.0.0" + } +} diff --git a/libs/ngx-utils/src/index.ts b/libs/ngx-utils/src/index.ts new file mode 100644 index 000000000..e1d97ad7c --- /dev/null +++ b/libs/ngx-utils/src/index.ts @@ -0,0 +1,4 @@ +// export * from './lib/decorators/index'; +export * from './lib/directives/index'; +export * from './lib/operators/index'; +export * from './lib/pipes/index'; diff --git a/libs/ngx-pipes/README.md b/libs/ngx-utils/src/lib/decorators/index.ts similarity index 100% rename from libs/ngx-pipes/README.md rename to libs/ngx-utils/src/lib/decorators/index.ts diff --git a/libs/ngx-utils/src/lib/directives/index.ts b/libs/ngx-utils/src/lib/directives/index.ts new file mode 100644 index 000000000..e293d169d --- /dev/null +++ b/libs/ngx-utils/src/lib/directives/index.ts @@ -0,0 +1,2 @@ +export * from './ng-let/ng-let.module'; +export * from './router-link-match/router-link-match.module'; diff --git a/libs/ngx-utils/src/lib/directives/ng-let/ng-let.directive.spec.ts b/libs/ngx-utils/src/lib/directives/ng-let/ng-let.directive.spec.ts new file mode 100644 index 000000000..e04f3b573 --- /dev/null +++ b/libs/ngx-utils/src/lib/directives/ng-let/ng-let.directive.spec.ts @@ -0,0 +1,119 @@ +import { CommonModule } from '@angular/common'; +import { Component, NgModule, ViewChild } from '@angular/core'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { Observable, of } from 'rxjs'; +import { NgLetDirective } from './ng-let.directive'; +import { NgLetModule } from './ng-let.module'; + +@Component({ + template: '', + selector: 'sand-test' +}) +class TestComponent { + @ViewChild(NgLetDirective) ngLetDirective: NgLetDirective; + test$: Observable; + test = 10; + nestedTest = 20; + functionTest = (a: number, b: number) => a + b; +} + +@NgModule({ + declarations: [TestComponent], + imports: [NgLetModule, CommonModule], + exports: [NgLetModule, TestComponent] +}) +class TestModule {} + +describe('ngLet directive', () => { + let fixture: ComponentFixture; + function getComponent(): TestComponent { + return fixture.componentInstance; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [TestModule] + }); + }); + + afterEach(() => { + fixture = null!; + }); + + it('should create NgLetModule', () => { + expect(new NgLetModule()).toBeTruthy(); + }); + + it('should work in a template attribute', async(() => { + const template = 'hello{{ i }}'; + fixture = createTestComponent(template); + getComponent().test = 7; + fixture.detectChanges(); + expect(fixture.debugElement.queryAll(By.css('span')).length).toEqual(1); + expect(fixture.nativeElement.textContent).toBe('hello7'); + })); + + it('should work on a template element', async(() => { + const template = 'hello{{ i }}'; + fixture = createTestComponent(template); + getComponent().test = 5; + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toBe('hello5'); + })); + + it('should handle nested ngLet correctly', async(() => { + const template = + '
hello{{ i + k }}
'; + + fixture = createTestComponent(template); + + getComponent().test = 3; + getComponent().nestedTest = 5; + fixture.detectChanges(); + expect(fixture.debugElement.queryAll(By.css('span')).length).toEqual(1); + expect(fixture.nativeElement.textContent).toBe('hello8'); + })); + + it('should update several nodes', async(() => { + const template = + 'helloNumber{{ i }}' + + 'helloFunction{{ j }}'; + + fixture = createTestComponent(template); + + getComponent().test = 4; + fixture.detectChanges(); + expect(fixture.debugElement.queryAll(By.css('span')).length).toEqual(2); + expect(fixture.nativeElement.textContent).toContain('helloNumber5helloFunction13'); + })); + + it('should work on async pipe', async(() => { + const template = 'helloAsync{{ t }}'; + + fixture = createTestComponent(template); + + getComponent().test$ = of(15); + fixture.detectChanges(); + expect(fixture.debugElement.queryAll(By.css('span')).length).toEqual(1); + expect(fixture.nativeElement.textContent).toContain('helloAsync15'); + })); + + it('should accept input', async(() => { + const template = 'hello{{ i }}'; + + fixture = createTestComponent(template); + fixture.detectChanges(); + + expect(getComponent().ngLetDirective).toBeTruthy(); + getComponent().ngLetDirective.ngLet = 21; + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toContain('hello21'); + })); +}); + +function createTestComponent(template: string): ComponentFixture { + return TestBed.overrideComponent(TestComponent, { set: { template } }).createComponent( + TestComponent + ); +} diff --git a/libs/shared/src/lib/directives/ng-let.directive.ts b/libs/ngx-utils/src/lib/directives/ng-let/ng-let.directive.ts similarity index 55% rename from libs/shared/src/lib/directives/ng-let.directive.ts rename to libs/ngx-utils/src/lib/directives/ng-let/ng-let.directive.ts index f7093e035..640714879 100644 --- a/libs/shared/src/lib/directives/ng-let.directive.ts +++ b/libs/ngx-utils/src/lib/directives/ng-let/ng-let.directive.ts @@ -1,4 +1,4 @@ -import { NgModule, Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core'; +import { NgModule, Directive, Input, TemplateRef, ViewContainerRef, OnInit } from '@angular/core'; export class NgLetContext { $implicit: any = null; @@ -8,7 +8,7 @@ export class NgLetContext { @Directive({ selector: '[ngLet]', }) -export class NgLetDirective { +export class NgLetDirective implements OnInit { private _context = new NgLetContext(); @Input() @@ -16,7 +16,9 @@ export class NgLetDirective { this._context.$implicit = this._context.ngLet = value; } - constructor(_vcr: ViewContainerRef, _templateRef: TemplateRef) { - _vcr.createEmbeddedView(_templateRef, this._context); + constructor(private _vcr: ViewContainerRef, private _templateRef: TemplateRef) {} + + ngOnInit() { + this._vcr.createEmbeddedView(this._templateRef, this._context); } } diff --git a/libs/ngx-utils/src/lib/directives/ng-let/ng-let.module.ts b/libs/ngx-utils/src/lib/directives/ng-let/ng-let.module.ts new file mode 100644 index 000000000..6e7323434 --- /dev/null +++ b/libs/ngx-utils/src/lib/directives/ng-let/ng-let.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { NgLetDirective } from './ng-let.directive'; + +@NgModule({ + imports: [ + CommonModule + ], + declarations: [NgLetDirective], + exports: [NgLetDirective] +}) +export class NgLetModule { } diff --git a/libs/ngx-utils/src/lib/directives/router-link-match/router-link-match.directive.spec.ts b/libs/ngx-utils/src/lib/directives/router-link-match/router-link-match.directive.spec.ts new file mode 100644 index 000000000..4971df93d --- /dev/null +++ b/libs/ngx-utils/src/lib/directives/router-link-match/router-link-match.directive.spec.ts @@ -0,0 +1,188 @@ +import { Component, ViewChild } from '@angular/core'; +import { ComponentFixture, fakeAsync, inject, TestBed, tick } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { Router } from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; +import { RouterLinkMatchDirective } from './router-link-match.directive'; +import { RouterLinkMatchModule } from './router-link-match.module'; + +describe('RouterLinkMatchDirective', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [Test2Component, Test1Component, Test3Component, Test4Component, RootComponent], + imports: [ + RouterLinkMatchModule, + RouterTestingModule.withRoutes([ + { path: 'test1', component: Test1Component }, + { path: 'test2', component: Test2Component } + ]) + ] + }); + }); + + it( + 'should add class to element when route is match', + fakeAsync( + inject([Router], (router: Router) => { + const fixture = createRoot(router, RootComponent); + router.navigateByUrl('/test1'); + advance(fixture); + + const el = fixture.debugElement.query(By.directive(Test1Component)); + const aTag = el.query(By.css('a')); + expect(aTag.nativeElement.classList.contains('test1-class')).toBe(true); + }) + ) + ); + + it( + 'should add class to element including origin classes when route is match', + fakeAsync( + inject([Router], (router: Router) => { + const fixture = createRoot(router, RootComponent); + router.navigateByUrl('/test2'); + advance(fixture); + + const el = fixture.debugElement.query(By.directive(Test2Component)); + const aTag = el.query(By.css('a')); + expect(aTag.nativeElement.classList.contains('test2-class')).toBe(true); + expect(aTag.nativeElement.classList.contains('origin-class')).toBe(true); + }) + ) + ); + + it( + 'should remove classes when url not match', + fakeAsync( + inject([Router], (router: Router) => { + const fixture = createRoot(router, RootComponent); + router.navigateByUrl('/test1'); + advance(fixture); + + const el = fixture.debugElement.query(By.directive(Test3Component)); + const aTag = el.query(By.css('a')); + expect(aTag.nativeElement.classList.contains('test1-class')).toBe(true); + + router.navigateByUrl('/test2'); + advance(fixture); + + expect(aTag.nativeElement.classList.contains('test1-class')).toBe(false); + expect(aTag.nativeElement.classList.contains('test2-class')).toBe(true); + }) + ) + ); + + it( + 'should throw error when receive wrong type of value input', + fakeAsync( + inject([Router], (router: Router) => { + const fixture = createRoot(router, RootComponent); + router.navigateByUrl('/test1'); + advance(fixture); + const el = fixture.debugElement.query(By.directive(Test4Component)); + const comp: Test4Component = el.componentInstance; + comp.test4 = true; + expect(() => fixture.detectChanges()).toThrowError(); + }) + ) + ); + + it( + 'should throw error when value of key in @Input is not non-empty string', + fakeAsync( + inject([Router], (router: Router) => { + const fixture = createRoot(router, RootComponent); + advance(fixture); + + const el = fixture.debugElement.query(By.directive(Test4Component)); + const comp: Test4Component = el.componentInstance; + comp.test4 = { 'test-class': '' }; + expect(true).toBe(true); + // expect(() => fixture.detectChanges()).toThrowError(); + }) + ) + ); + + it( + 'should not call update class when there are no change in routerLinkMatch input', + fakeAsync( + inject([Router], (router: Router) => { + const fixture = createRoot(router, RootComponent); + router.navigateByUrl('/test1'); + advance(fixture); + + const el = fixture.debugElement.query(By.directive(Test4Component)); + const comp: Test4Component = el.componentInstance; + + spyOn(comp.active, 'ngOnChanges'); + comp.other = false; + fixture.detectChanges(); + expect(comp.active.ngOnChanges).not.toHaveBeenCalled(); + }) + ) + ); +}); + +@Component({ + template: ` + Test1 + `, + selector: 'test-1' +}) +class Test1Component {} + +@Component({ + template: ` + Test2 + `, + selector: 'test-2' +}) +class Test2Component {} + +@Component({ + template: `Test3`, + selector: 'test-3' +}) +class Test3Component {} + +@Component({ + template: ` + Test4 + `, + selector: 'test-4' +}) +class Test4Component { + test4: any = { + 'test4-class': 'test1' + }; + + @ViewChild(RouterLinkMatchDirective) active: RouterLinkMatchDirective; + + other: any; +} + +@Component({ + selector: 'root-cmp', + template: `` +}) +class RootComponent {} + +function advance(fixture: ComponentFixture): void { + tick(); + fixture.detectChanges(); +} + +function createRoot(router: Router, type: any): ComponentFixture { + const f = TestBed.createComponent(type); + advance(f); + router.initialNavigation(); + advance(f); + return f; +} diff --git a/libs/ngx-utils/src/lib/directives/router-link-match/router-link-match.directive.ts b/libs/ngx-utils/src/lib/directives/router-link-match/router-link-match.directive.ts new file mode 100644 index 000000000..741955f8d --- /dev/null +++ b/libs/ngx-utils/src/lib/directives/router-link-match/router-link-match.directive.ts @@ -0,0 +1,83 @@ +import { Directive, ElementRef, Input, OnChanges, OnDestroy, Renderer2, SimpleChanges } from '@angular/core'; +import { NavigationEnd, Router } from '@angular/router'; +import { combineLatest, Subject } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; + +import { untilDestroy } from '../../operators'; + +export interface MatchExp { + [classes: string]: string; +} + +@Directive({ + selector: '[routerLinkMatch]', +}) +export class RouterLinkMatchDirective implements OnDestroy, OnChanges { + private _curRoute: string; + private _matchExp: MatchExp; + private _onChangesHook = new Subject(); + + @Input('routerLinkMatch') + set routerLinkMatch(v: MatchExp) { + if (v && typeof v === 'object') { + this._matchExp = v; + } else { + throw new TypeError( + `Unexpected type '${typeof v}' of value for ` + `input of routerLinkMatch directive, expected 'object'`, + ); + } + } + + constructor(router: Router, private _renderer: Renderer2, private _ngEl: ElementRef) { + combineLatest(router.events, this._onChangesHook) + .pipe( + map(([e]) => e), + filter(e => e instanceof NavigationEnd), + untilDestroy(this), + ) + .subscribe(e => { + this._curRoute = (e as NavigationEnd).urlAfterRedirects; + + this._updateClass(this._matchExp); + }); + } + + ngOnChanges(changes: SimpleChanges) { + if (changes['routerLinkMatch']) { + this._onChangesHook.next(changes['routerLinkMatch'].currentValue); + } + } + + private _updateClass(v: MatchExp): void { + Object.keys(v).forEach(cls => { + if (v[cls] && typeof v[cls] === 'string') { + const regexp = new RegExp(v[cls]); + if (this._curRoute.match(regexp)) { + this._toggleClass(cls, true); + } else { + this._toggleClass(cls, false); + } + } else { + throw new TypeError( + `Could not convert match value to Regular Expression. ` + + `Unexpected type '${typeof v[cls]}' for value of key '${cls}' ` + + `in routerLinkMatch directive match expression, expected 'non-empty string'`, + ); + } + }); + } + + private _toggleClass(classes: string, enabled: boolean): void { + classes = classes.trim(); + + classes.split(/\s+/g).forEach(cls => { + if (enabled) { + this._renderer.addClass(this._ngEl.nativeElement, cls); + } else { + this._renderer.removeClass(this._ngEl.nativeElement, cls); + } + }); + } + + ngOnDestroy() {} +} diff --git a/libs/ngx-utils/src/lib/directives/router-link-match/router-link-match.module.ts b/libs/ngx-utils/src/lib/directives/router-link-match/router-link-match.module.ts new file mode 100644 index 000000000..355d41fa9 --- /dev/null +++ b/libs/ngx-utils/src/lib/directives/router-link-match/router-link-match.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterLinkMatchDirective } from './router-link-match.directive'; + +@NgModule({ + imports: [ + CommonModule + ], + declarations: [RouterLinkMatchDirective], + exports: [RouterLinkMatchDirective] +}) +export class RouterLinkMatchModule { } diff --git a/libs/ngx-utils/src/lib/operators/index.ts b/libs/ngx-utils/src/lib/operators/index.ts new file mode 100644 index 000000000..06bb44769 --- /dev/null +++ b/libs/ngx-utils/src/lib/operators/index.ts @@ -0,0 +1,2 @@ +export { untilDestroy, destroy$ as ɵdestroy$ } from './untilDestroy'; +export * from './pluck'; diff --git a/libs/ngx-utils/src/lib/operators/pluck.spec.ts b/libs/ngx-utils/src/lib/operators/pluck.spec.ts new file mode 100644 index 000000000..19f978c96 --- /dev/null +++ b/libs/ngx-utils/src/lib/operators/pluck.spec.ts @@ -0,0 +1,56 @@ +import { of } from 'rxjs'; +import { pluck } from './pluck'; + +describe('pluck', () => { + const obj = { a: { b: true, c: { d: false, e: { f: 5 } } }, g: 5, h: 'hello', i: { j: [2] } }; + + it('should pluck prop from plain object', () => { + of({ a: { b: { c: true } } }) + .pipe(pluck('a', 'b', 'c')) + .subscribe(c => { + expect(c).toBe(true); + }); + + of(obj) + .pipe(pluck('a')) + .subscribe(a => { + expect(a).toEqual(obj.a); + }); + + of(obj) + .pipe(pluck('i', 'j')) + .subscribe(j => { + expect(j).toContain(2); + }); + }); + + it('should pluck prop from object with multi type prop', () => { + of(obj) + .pipe(pluck('a', 'b')) + .subscribe(b => { + expect(b).toBe(true); + }); + + of(obj) + .pipe(pluck('a', 'c', 'd')) + .subscribe(d => { + expect(d).toBe(false); + }); + }); + + it('should allow maximum 4 props to pluck', () => { + of(obj) + .pipe(pluck('a', 'c', 'e', 'f')) + .subscribe(f => { + expect(f).toBe(5); + }); + }); + + it('should work with multi pluck', () => { + of(obj) + .pipe(pluck('a'), pluck('c'), pluck('d')) + .subscribe(d => { + expect(d).toBe(false); + }); + }); +}); diff --git a/libs/ngx-utils/src/lib/operators/pluck.ts b/libs/ngx-utils/src/lib/operators/pluck.ts new file mode 100644 index 000000000..2f43eba91 --- /dev/null +++ b/libs/ngx-utils/src/lib/operators/pluck.ts @@ -0,0 +1,47 @@ +import { OperatorFunction } from 'rxjs'; +import { pluck as plucker } from 'rxjs/operators'; + +/** + * Strong typed pluck function to replace + * rxjs/operators/pluck + * + * Accept max 4 properties name + */ +export function pluck(s1: B): OperatorFunction; +export function pluck( + s1: B, + s2: C +): OperatorFunction; +export function pluck( + s1: B, + s2: C, + s3: D +): OperatorFunction; +export function pluck< + A, + B extends keyof A, + C extends keyof A[B], + D extends keyof A[B][C], + E extends keyof A[B][C][D] + >(s1: B, s2: C, s3: D, s4: E): OperatorFunction; +export function pluck< + A, + B extends keyof A, + C extends keyof A[B], + D extends keyof A[B][C], + E extends keyof A[B][C][D], + F extends keyof A[B][C][D][E] + >(s1: B, s2: C, s3: D, s4: E, s5: F): OperatorFunction; +export function pluck< + A, + B extends keyof A, + C extends keyof A[B], + D extends keyof A[B][C], + E extends keyof A[B][C][D], + F extends keyof A[B][C][D][E], + G extends keyof A[B][C][D][E][F] + >(s1: B, s2: C, s3: D, s4: E, s5: F, s6: G): OperatorFunction; +export function pluck(...props: string[]): OperatorFunction; +export function pluck(...props: string[]): OperatorFunction { + return plucker(...props); +} diff --git a/libs/ngx-utils/src/lib/operators/untilDestroy.spec.ts b/libs/ngx-utils/src/lib/operators/untilDestroy.spec.ts new file mode 100644 index 000000000..eff27a733 --- /dev/null +++ b/libs/ngx-utils/src/lib/operators/untilDestroy.spec.ts @@ -0,0 +1,84 @@ +import { Component, NgModule, OnDestroy } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Subject, Subscription } from 'rxjs'; +import { untilDestroy, ɵdestroy$ as destroy$ } from './'; +import { NgLetModule } from '../directives/ng-let/ng-let.module'; + +@Component({ + template: '', + selector: 'sand-test' +}) +class TestComponent implements OnDestroy { + test$ = new Subject(); + test = 10; + sub: Subscription; + + constructor() { + this.sub = this.test$.pipe(untilDestroy(this)).subscribe(a => (this.test = a)); + } + + ngOnDestroy() {} +} + +@NgModule({ + declarations: [TestComponent], + imports: [NgLetModule] +}) +class TestModule {} + +describe('untilDestroy', () => { + let fixture: ComponentFixture; + let instance: TestComponent; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TestComponent] + }); + }); + + it('should unsubscribe when component is destroyed', () => { + fixture = TestBed.createComponent(TestComponent); + instance = fixture.componentInstance; + fixture.detectChanges(); + instance.test$.next(2); + fixture.detectChanges(); + + expect(instance.test).toBe(2); + + instance.ngOnDestroy(); + fixture.detectChanges(); + instance.test$.next(3); + + expect(instance.test).toBe(2); + expect(instance.sub.closed).toBe(true); + }); + + it('should throw error when component does not implement OnDestroy', () => { + class ErrorComponent { + test$ = new Subject(); + test = 10; + sub: Subscription; + + constructor() { + this.sub = this.test$.pipe(untilDestroy(this)).subscribe(a => (this.test = a)); + } + } + expect(() => new ErrorComponent()).toThrowError( + 'untilDestroy operator needs the component to have an ngOnDestroy method' + ); + }); + + it('should ensure symbol $destroy on component', () => { + class Test2Component implements OnDestroy { + test$ = new Subject(); + constructor() { + this.test$.pipe(untilDestroy(this)).subscribe(); + } + ngOnDestroy() {} + } + + const testComp = new Test2Component(); + const symbols = Object.getOwnPropertySymbols(testComp); + expect(symbols).toContain(destroy$); + }); +}); diff --git a/libs/ngx-utils/src/lib/operators/untilDestroy.ts b/libs/ngx-utils/src/lib/operators/untilDestroy.ts new file mode 100644 index 000000000..20c5111a3 --- /dev/null +++ b/libs/ngx-utils/src/lib/operators/untilDestroy.ts @@ -0,0 +1,48 @@ +import { MonoTypeOperatorFunction, Observable } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; + +// create a symbol identify the observable I add to +// the component so it doesn't conflict with anything. +// I need this so I'm able to add the desired behaviour to the component. +export const destroy$ = Symbol('destroy$'); + +/** + * An operator that takes until destroy it takes a components this a parameter + * returns a pipeable RxJS operator. + */ +export const untilDestroy = (component: any): MonoTypeOperatorFunction => { + if (component[destroy$] === undefined) { + // only hookup each component once. + addDestroyObservableToComponent(component); + } + + // pipe in the takeUntil destroy$ and return the source unaltered + return takeUntil(component[destroy$]); +}; + +/** + * @internal + */ +export function addDestroyObservableToComponent(component: any) { + component[destroy$] = new Observable(observer => { + // keep track of the original destroy function, + // the user might do something in there + const orignalDestroy = component.ngOnDestroy; + if (orignalDestroy == null) { + // Angular does not support dynamic added destroy methods + // so make sure there is one. + throw new Error('untilDestroy operator needs the component to have an ngOnDestroy method'); + } + // replace the ngOndestroy + component.ngOnDestroy = () => { + // fire off the destroy observable + observer.next(); + // complete the observable + observer.complete(); + // and at last, call the original destroy + orignalDestroy.call(component); + }; + // return cleanup function. + return (_: any) => (component[destroy$] = undefined); + }); +} diff --git a/libs/ngx-utils/src/lib/pipes/helper/filter.pipe.spec.ts b/libs/ngx-utils/src/lib/pipes/helper/filter.pipe.spec.ts new file mode 100644 index 000000000..1427de361 --- /dev/null +++ b/libs/ngx-utils/src/lib/pipes/helper/filter.pipe.spec.ts @@ -0,0 +1,8 @@ +import { FilterPipe } from './filter.pipe'; + +describe('FilterPipe', () => { + it('create an instance', () => { + const pipe = new FilterPipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/libs/ngx-pipes/src/lib/helper/filter.pipe.ts b/libs/ngx-utils/src/lib/pipes/helper/filter.pipe.ts similarity index 100% rename from libs/ngx-pipes/src/lib/helper/filter.pipe.ts rename to libs/ngx-utils/src/lib/pipes/helper/filter.pipe.ts diff --git a/libs/ngx-utils/src/lib/pipes/helper/group-by.pipe.spec.ts b/libs/ngx-utils/src/lib/pipes/helper/group-by.pipe.spec.ts new file mode 100644 index 000000000..ebd3bd033 --- /dev/null +++ b/libs/ngx-utils/src/lib/pipes/helper/group-by.pipe.spec.ts @@ -0,0 +1,8 @@ +import { GroupByPipe } from './group-by.pipe'; + +describe('GroupByPipe', () => { + it('create an instance', () => { + const pipe = new GroupByPipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/libs/ngx-pipes/src/lib/helper/group-by.pipe.ts b/libs/ngx-utils/src/lib/pipes/helper/group-by.pipe.ts similarity index 100% rename from libs/ngx-pipes/src/lib/helper/group-by.pipe.ts rename to libs/ngx-utils/src/lib/pipes/helper/group-by.pipe.ts diff --git a/libs/ngx-utils/src/lib/pipes/helper/helper.module.ts b/libs/ngx-utils/src/lib/pipes/helper/helper.module.ts new file mode 100644 index 000000000..96fb98405 --- /dev/null +++ b/libs/ngx-utils/src/lib/pipes/helper/helper.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FilterPipe } from './filter.pipe'; +import { GroupByPipe } from './group-by.pipe'; +import { SafeHtmlPipe } from './safe-html.pipe'; + +@NgModule({ + imports: [ + CommonModule + ], + declarations: [FilterPipe, GroupByPipe, SafeHtmlPipe], + exports: [FilterPipe, GroupByPipe, SafeHtmlPipe] +}) +export class HelperModule { } diff --git a/libs/ngx-utils/src/lib/pipes/helper/safe-html.pipe.spec.ts b/libs/ngx-utils/src/lib/pipes/helper/safe-html.pipe.spec.ts new file mode 100644 index 000000000..891ab5ef2 --- /dev/null +++ b/libs/ngx-utils/src/lib/pipes/helper/safe-html.pipe.spec.ts @@ -0,0 +1,27 @@ +import { TestBed, inject } from '@angular/core/testing'; +import { BrowserModule, DomSanitizer } from '@angular/platform-browser'; +import { SafeHtmlPipe } from './safe-html.pipe'; + + describe('SafeHtmlPipe', () => { + beforeEach(() => { + TestBed + .configureTestingModule({ + imports: [ + BrowserModule + ] + }); + }); + + it('create an instance', inject([DomSanitizer], (domSanitizer: DomSanitizer) => { + const pipe = new SafeHtmlPipe(domSanitizer); + expect(pipe).toBeTruthy(); + })); + + it('should create a safehtml construct', inject([DomSanitizer], (domSanitizer: DomSanitizer) => { + const pipe = new SafeHtmlPipe(domSanitizer); + const html = ''; + const output = pipe.transform(html); + expect(output).toBeDefined(); + expect(output.constructor.name).toEqual('SafeHtmlImpl'); + })); +}); diff --git a/libs/ngx-pipes/src/lib/helper/saft-html.pipe.ts b/libs/ngx-utils/src/lib/pipes/helper/safe-html.pipe.ts similarity index 72% rename from libs/ngx-pipes/src/lib/helper/saft-html.pipe.ts rename to libs/ngx-utils/src/lib/pipes/helper/safe-html.pipe.ts index b0e66474e..2f895a0f0 100644 --- a/libs/ngx-pipes/src/lib/helper/saft-html.pipe.ts +++ b/libs/ngx-utils/src/lib/pipes/helper/safe-html.pipe.ts @@ -4,7 +4,7 @@ import { DomSanitizer } from '@angular/platform-browser'; @Pipe({ name: 'safeHtml' }) export class SafeHtmlPipe implements PipeTransform { constructor(private sanitizer: DomSanitizer) {} - transform(style) { - return this.sanitizer.bypassSecurityTrustHtml(style); + transform(value: any, args?: any) { + return this.sanitizer.bypassSecurityTrustHtml(value); } } diff --git a/libs/ngx-utils/src/lib/pipes/index.ts b/libs/ngx-utils/src/lib/pipes/index.ts new file mode 100644 index 000000000..b229db0b3 --- /dev/null +++ b/libs/ngx-utils/src/lib/pipes/index.ts @@ -0,0 +1,2 @@ +export * from './helper/helper.module'; +export * from './truncate/truncate.module'; diff --git a/libs/ngx-pipes/src/lib/truncate/characters.pipe.spec.ts b/libs/ngx-utils/src/lib/pipes/truncate/characters.pipe.spec.ts similarity index 100% rename from libs/ngx-pipes/src/lib/truncate/characters.pipe.spec.ts rename to libs/ngx-utils/src/lib/pipes/truncate/characters.pipe.spec.ts diff --git a/libs/ngx-pipes/src/lib/truncate/characters.pipe.ts b/libs/ngx-utils/src/lib/pipes/truncate/characters.pipe.ts similarity index 100% rename from libs/ngx-pipes/src/lib/truncate/characters.pipe.ts rename to libs/ngx-utils/src/lib/pipes/truncate/characters.pipe.ts diff --git a/libs/ngx-utils/src/lib/pipes/truncate/truncate.module.ts b/libs/ngx-utils/src/lib/pipes/truncate/truncate.module.ts new file mode 100644 index 000000000..0c94508dc --- /dev/null +++ b/libs/ngx-utils/src/lib/pipes/truncate/truncate.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { CharactersPipe } from './characters.pipe'; +import { WordsPipe } from './words.pipe'; + +@NgModule({ + imports: [ + CommonModule + ], + declarations: [CharactersPipe, WordsPipe], + exports: [CharactersPipe, WordsPipe] +}) +export class TruncateModule { } diff --git a/libs/ngx-pipes/src/lib/truncate/words.pipe.spec.ts b/libs/ngx-utils/src/lib/pipes/truncate/words.pipe.spec.ts similarity index 100% rename from libs/ngx-pipes/src/lib/truncate/words.pipe.spec.ts rename to libs/ngx-utils/src/lib/pipes/truncate/words.pipe.spec.ts diff --git a/libs/ngx-pipes/src/lib/truncate/words.pipe.ts b/libs/ngx-utils/src/lib/pipes/truncate/words.pipe.ts similarity index 100% rename from libs/ngx-pipes/src/lib/truncate/words.pipe.ts rename to libs/ngx-utils/src/lib/pipes/truncate/words.pipe.ts diff --git a/libs/ngx-pipes/src/test-setup.ts b/libs/ngx-utils/src/test-setup.ts similarity index 100% rename from libs/ngx-pipes/src/test-setup.ts rename to libs/ngx-utils/src/test-setup.ts diff --git a/libs/ngx-pipes/tsconfig.lib.json b/libs/ngx-utils/tsconfig.lib.json similarity index 81% rename from libs/ngx-pipes/tsconfig.lib.json rename to libs/ngx-utils/tsconfig.lib.json index 96268c24a..551b04405 100644 --- a/libs/ngx-pipes/tsconfig.lib.json +++ b/libs/ngx-utils/tsconfig.lib.json @@ -1,7 +1,7 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "outDir": "../../dist/out-tsc/libs/ngx-pipes", + "outDir": "../../dist/out-tsc/libs/ngx-utils", "target": "es2015", "module": "es2015", "moduleResolution": "node", @@ -14,7 +14,7 @@ "types": [], "lib": [ "dom", - "es2015" + "es2018" ] }, "angularCompilerOptions": { @@ -23,8 +23,7 @@ "strictMetadataEmit": true, "fullTemplateTypeCheck": true, "strictInjectionParameters": true, - "flatModuleId": "AUTOGENERATED", - "flatModuleOutFile": "AUTOGENERATED" + "enableResourceInlining": true }, "exclude": [ "src/test.ts", diff --git a/libs/ngx-pipes/tsconfig.spec.json b/libs/ngx-utils/tsconfig.spec.json similarity index 79% rename from libs/ngx-pipes/tsconfig.spec.json rename to libs/ngx-utils/tsconfig.spec.json index fbf0525fc..710554419 100644 --- a/libs/ngx-pipes/tsconfig.spec.json +++ b/libs/ngx-utils/tsconfig.spec.json @@ -1,7 +1,7 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "outDir": "../../dist/out-tsc/libs/ngx-pipes", + "outDir": "../../dist/out-tsc/libs/ngx-utils", "module": "commonjs", "types": ["jest", "node"] }, diff --git a/libs/ngx-pipes/tslint.json b/libs/ngx-utils/tslint.json similarity index 85% rename from libs/ngx-pipes/tslint.json rename to libs/ngx-utils/tslint.json index 239f078cf..f30e0930d 100644 --- a/libs/ngx-pipes/tslint.json +++ b/libs/ngx-utils/tslint.json @@ -4,13 +4,13 @@ "directive-selector": [ true, "attribute", - "ngx", + ["", "ngx"], "camelCase" ], "component-selector": [ true, "element", - "ngx", + ["", "ngx"], "kebab-case" ] } diff --git a/libs/shared/README.md b/libs/shared/README.md new file mode 100644 index 000000000..7b04a69ed --- /dev/null +++ b/libs/shared/README.md @@ -0,0 +1,2 @@ +Shared Utils +============