From e112cf8dc7dc386c30010c817aa793a01250be8c Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Thu, 9 Apr 2020 12:06:09 +0300 Subject: [PATCH 01/17] refactor: simplify isFormDirty logic --- .../lib/components/modal/modal.component.ts | 26 +------------------ 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/modal/modal.component.ts b/npm/ng-packs/packages/theme-shared/src/lib/components/modal/modal.component.ts index e00cda5b55..2527ab033d 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/components/modal/modal.component.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/components/modal/modal.component.ts @@ -102,17 +102,7 @@ export class ModalComponent implements OnDestroy { destroy$ = new Subject(); get isFormDirty(): boolean { - let node: HTMLDivElement; - if (!this.modalContent) { - node = document.getElementById('modal-container') as HTMLDivElement; - } - - const nodes = getFlatNodes( - ((node || this.modalContent.nativeElement).querySelector('#abp-modal-body') as HTMLElement) - .childNodes, - ); - - return hasNgDirty(nodes); + return Boolean(document.querySelector('.modal-dialog .ng-dirty')); } constructor(private renderer: Renderer2, private confirmationService: ConfirmationService) {} @@ -178,17 +168,3 @@ export class ModalComponent implements OnDestroy { this.init.emit(); } } - -function getFlatNodes(nodes: NodeList): HTMLElement[] { - return Array.from(nodes).reduce( - (acc, val) => [ - ...acc, - ...(val.childNodes && val.childNodes.length ? getFlatNodes(val.childNodes) : [val]), - ], - [], - ); -} - -function hasNgDirty(nodes: HTMLElement[]) { - return nodes.findIndex(node => (node.className || '').indexOf('ng-dirty') > -1) > -1; -} From e202c944bbc6fcb7ede75e49a57ec1caab5f344e Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Fri, 10 Apr 2020 00:10:48 +0300 Subject: [PATCH 02/17] feat: add utility type for inferred Type & TemplateRef --- npm/ng-packs/packages/core/src/lib/models/index.ts | 1 + npm/ng-packs/packages/core/src/lib/models/utility.ts | 4 ++++ 2 files changed, 5 insertions(+) create mode 100644 npm/ng-packs/packages/core/src/lib/models/utility.ts diff --git a/npm/ng-packs/packages/core/src/lib/models/index.ts b/npm/ng-packs/packages/core/src/lib/models/index.ts index c38a38bfaf..c0950c3dee 100644 --- a/npm/ng-packs/packages/core/src/lib/models/index.ts +++ b/npm/ng-packs/packages/core/src/lib/models/index.ts @@ -6,3 +6,4 @@ export * from './profile'; export * from './replaceable-components'; export * from './rest'; export * from './session'; +export * from './utility'; diff --git a/npm/ng-packs/packages/core/src/lib/models/utility.ts b/npm/ng-packs/packages/core/src/lib/models/utility.ts new file mode 100644 index 0000000000..5fe76af3c9 --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/models/utility.ts @@ -0,0 +1,4 @@ +import { TemplateRef, Type } from '@angular/core'; + +export type InferedInstanceOf = T extends Type ? U : never; +export type InferedContextOf = T extends TemplateRef ? U : never; From 08421d37cfc95bc5bbc34d809937bbfa74ea10e8 Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Fri, 10 Apr 2020 00:11:41 +0300 Subject: [PATCH 03/17] feat: add context strategies --- .../src/lib/strategies/context.strategy.ts | 47 +++++++++++ .../packages/core/src/lib/strategies/index.ts | 1 + .../src/lib/tests/context.strategy.spec.ts | 79 +++++++++++++++++++ 3 files changed, 127 insertions(+) create mode 100644 npm/ng-packs/packages/core/src/lib/strategies/context.strategy.ts create mode 100644 npm/ng-packs/packages/core/src/lib/tests/context.strategy.spec.ts diff --git a/npm/ng-packs/packages/core/src/lib/strategies/context.strategy.ts b/npm/ng-packs/packages/core/src/lib/strategies/context.strategy.ts new file mode 100644 index 0000000000..e3d16c0573 --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/strategies/context.strategy.ts @@ -0,0 +1,47 @@ +import { ComponentRef, TemplateRef, Type } from '@angular/core'; +import { InferedContextOf, InferedInstanceOf } from '../models'; + +export abstract class ContextStrategy { + constructor(public context: Partial>) {} + + /* tslint:disable-next-line:no-unused-variable */ + setContext(componentRef?: ComponentRef>): Partial> { + return this.context; + } +} + +export class NoContextStrategy< + T extends Type | TemplateRef = any +> extends ContextStrategy { + constructor() { + super(undefined); + } +} + +export class ComponentContextStrategy = any> extends ContextStrategy { + setContext(componentRef: ComponentRef>): Partial> { + Object.keys(this.context).forEach(key => (componentRef.instance[key] = this.context[key])); + componentRef.changeDetectorRef.detectChanges(); + return this.context; + } +} + +export class TemplateContextStrategy = any> extends ContextStrategy { + setContext(): Partial> { + return this.context; + } +} + +export const CONTEXT_STRATEGY = { + None | TemplateRef = any>() { + return new NoContextStrategy(); + }, + Component = any>(context: Partial>) { + return new ComponentContextStrategy(context); + }, + Template = any>(context: Partial>) { + return new TemplateContextStrategy(context); + }, +}; + +type ContextType = T extends Type | TemplateRef ? U : never; diff --git a/npm/ng-packs/packages/core/src/lib/strategies/index.ts b/npm/ng-packs/packages/core/src/lib/strategies/index.ts index 2d6be484dd..28447748f7 100644 --- a/npm/ng-packs/packages/core/src/lib/strategies/index.ts +++ b/npm/ng-packs/packages/core/src/lib/strategies/index.ts @@ -1,5 +1,6 @@ export * from './content-security.strategy'; export * from './content.strategy'; +export * from './context.strategy'; export * from './cross-origin.strategy'; export * from './dom.strategy'; export * from './loading.strategy'; diff --git a/npm/ng-packs/packages/core/src/lib/tests/context.strategy.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/context.strategy.spec.ts new file mode 100644 index 0000000000..461c397457 --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/tests/context.strategy.spec.ts @@ -0,0 +1,79 @@ +import { ComponentRef } from '@angular/core'; +import { + ComponentContextStrategy, + CONTEXT_STRATEGY, + NoContextStrategy, + TemplateContextStrategy, +} from '../strategies'; +import { uuid } from '../utils'; + +describe('ComponentContextStrategy', () => { + describe('#setContext', () => { + let componentRef: ComponentRef; + + beforeEach( + () => + (componentRef = { + instance: { + x: '', + y: '', + z: '', + }, + changeDetectorRef: { + detectChanges: jest.fn(), + }, + } as any), + ); + + test.each` + props | values + ${['x']} | ${[uuid()]} + ${['x', 'y']} | ${[uuid(), uuid()]} + ${['x', 'y', 'z']} | ${[uuid(), uuid(), uuid()]} + `( + 'should set $props as $values and call detectChanges once', + ({ props, values }: { props: string[]; values: string[] }) => { + const context = {}; + props.forEach((prop, i) => { + context[prop] = values[i]; + }); + + const strategy = new ComponentContextStrategy(context); + strategy.setContext(componentRef); + + expect(props.every(prop => componentRef.instance[prop] === context[prop])).toBe(true); + expect(componentRef.changeDetectorRef.detectChanges).toHaveBeenCalledTimes(1); + }, + ); + }); +}); + +describe('NoContextStrategy', () => { + describe('#setContext', () => { + it('should return undefined', () => { + const strategy = new NoContextStrategy(); + expect(strategy.setContext(null)).toBeUndefined(); + }); + }); +}); + +describe('TemplateContextStrategy', () => { + describe('#setContext', () => { + it('should return context', () => { + const context = { x: uuid() }; + const strategy = new TemplateContextStrategy(context); + expect(strategy.setContext()).toEqual(context); + }); + }); +}); + +describe('CONTEXT_STRATEGY', () => { + test.each` + name | Strategy + ${'Component'} | ${ComponentContextStrategy} + ${'None'} | ${NoContextStrategy} + ${'Template'} | ${TemplateContextStrategy} + `('should successfully map $name to $Strategy.name', ({ name, Strategy }) => { + expect(CONTEXT_STRATEGY[name](undefined)).toEqual(new Strategy(undefined)); + }); +}); From 4f31c1bfc055759ed8ade25f4ebc11ba063764e6 Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Fri, 10 Apr 2020 00:12:37 +0300 Subject: [PATCH 04/17] feat: add container strategies --- .../src/lib/strategies/container.strategy.ts | 44 ++++++++++ .../packages/core/src/lib/strategies/index.ts | 1 + .../src/lib/tests/container.strategy.spec.ts | 80 +++++++++++++++++++ 3 files changed, 125 insertions(+) create mode 100644 npm/ng-packs/packages/core/src/lib/strategies/container.strategy.ts create mode 100644 npm/ng-packs/packages/core/src/lib/tests/container.strategy.spec.ts diff --git a/npm/ng-packs/packages/core/src/lib/strategies/container.strategy.ts b/npm/ng-packs/packages/core/src/lib/strategies/container.strategy.ts new file mode 100644 index 0000000000..dfe169e402 --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/strategies/container.strategy.ts @@ -0,0 +1,44 @@ +import { ViewContainerRef } from '@angular/core'; + +export abstract class ContainerStrategy { + constructor(public containerRef: ViewContainerRef) {} + + abstract getIndex(): number; + + prepare(): void {} +} + +export class ClearContainerStrategy extends ContainerStrategy { + getIndex(): number { + return 0; + } + + prepare() { + this.containerRef.clear(); + } +} + +export class InsertIntoContainerStrategy extends ContainerStrategy { + constructor(containerRef: ViewContainerRef, private index: number) { + super(containerRef); + } + + getIndex() { + return Math.min(Math.max(0, this.index), this.containerRef.length); + } +} + +export const CONTAINER_STRATEGY = { + Clear(containerRef: ViewContainerRef) { + return new ClearContainerStrategy(containerRef); + }, + Append(containerRef: ViewContainerRef) { + return new InsertIntoContainerStrategy(containerRef, containerRef.length); + }, + Prepend(containerRef: ViewContainerRef) { + return new InsertIntoContainerStrategy(containerRef, 0); + }, + Insert(containerRef: ViewContainerRef, index: number) { + return new InsertIntoContainerStrategy(containerRef, index); + }, +}; diff --git a/npm/ng-packs/packages/core/src/lib/strategies/index.ts b/npm/ng-packs/packages/core/src/lib/strategies/index.ts index 28447748f7..8c36b230ef 100644 --- a/npm/ng-packs/packages/core/src/lib/strategies/index.ts +++ b/npm/ng-packs/packages/core/src/lib/strategies/index.ts @@ -1,3 +1,4 @@ +export * from './container.strategy'; export * from './content-security.strategy'; export * from './content.strategy'; export * from './context.strategy'; diff --git a/npm/ng-packs/packages/core/src/lib/tests/container.strategy.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/container.strategy.spec.ts new file mode 100644 index 0000000000..e85e7b5a50 --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/tests/container.strategy.spec.ts @@ -0,0 +1,80 @@ +import { ViewContainerRef } from '@angular/core'; +import { + ClearContainerStrategy, + CONTAINER_STRATEGY, + InsertIntoContainerStrategy, +} from '../strategies'; + +describe('ClearContainerStrategy', () => { + const containerRef = ({ + clear: jest.fn(), + length: 7, + } as any) as ViewContainerRef; + + describe('#getIndex', () => { + it('should return 0', () => { + const strategy = new ClearContainerStrategy(containerRef); + expect(strategy.getIndex()).toBe(0); + }); + }); + + describe('#prepare', () => { + it('should call clear method of containerRef once', () => { + const strategy = new ClearContainerStrategy(containerRef); + strategy.prepare(); + expect(strategy.getIndex()).toBe(0); + expect(containerRef.clear).toHaveBeenCalledTimes(1); + }); + }); +}); + +describe('InsertIntoContainerStrategy', () => { + const containerRef = ({ + clear: jest.fn(), + length: 7, + } as any) as ViewContainerRef; + + describe('#getIndex', () => { + test.each` + index | expected + ${0} | ${0} + ${4} | ${4} + ${9} | ${7} + ${-1} | ${0} + ${Infinity} | ${7} + `( + 'should return $expected when index is given $index', + ({ index, expected }: { index: number; expected: number }) => { + const strategy = new InsertIntoContainerStrategy(containerRef, index); + expect(strategy.getIndex()).toBe(expected); + }, + ); + }); + + describe('#prepare', () => { + it('should not call clear method of containerRef', () => { + const strategy = new InsertIntoContainerStrategy(containerRef, 0); + strategy.prepare(); + expect(containerRef.clear).not.toHaveBeenCalled(); + }); + }); +}); + +describe('CONTAINER_STRATEGY', () => { + const containerRef = ({ + clear: jest.fn(), + length: 7, + } as any) as ViewContainerRef; + + test.each` + name | Strategy | index + ${'Clear'} | ${ClearContainerStrategy} | ${undefined} + ${'Append'} | ${InsertIntoContainerStrategy} | ${containerRef.length} + ${'Prepend'} | ${InsertIntoContainerStrategy} | ${0} + ${'Insert'} | ${InsertIntoContainerStrategy} | ${4} + `('should successfully map $name to $Strategy.name', ({ name, Strategy, index }) => { + expect(CONTAINER_STRATEGY[name](containerRef, index)).toEqual( + new Strategy(containerRef, index), + ); + }); +}); From cad3e623d04dcd9d1547aef4e7ee3f9783ee44de Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Fri, 10 Apr 2020 00:13:19 +0300 Subject: [PATCH 05/17] feat: add projection strategies --- .../packages/core/src/lib/strategies/index.ts | 1 + .../src/lib/strategies/projection.strategy.ts | 181 +++++++++++++ .../src/lib/tests/projection.strategy.spec.ts | 245 ++++++++++++++++++ 3 files changed, 427 insertions(+) create mode 100644 npm/ng-packs/packages/core/src/lib/strategies/projection.strategy.ts create mode 100644 npm/ng-packs/packages/core/src/lib/tests/projection.strategy.spec.ts diff --git a/npm/ng-packs/packages/core/src/lib/strategies/index.ts b/npm/ng-packs/packages/core/src/lib/strategies/index.ts index 8c36b230ef..2e621e7907 100644 --- a/npm/ng-packs/packages/core/src/lib/strategies/index.ts +++ b/npm/ng-packs/packages/core/src/lib/strategies/index.ts @@ -5,3 +5,4 @@ export * from './context.strategy'; export * from './cross-origin.strategy'; export * from './dom.strategy'; export * from './loading.strategy'; +export * from './projection.strategy'; diff --git a/npm/ng-packs/packages/core/src/lib/strategies/projection.strategy.ts b/npm/ng-packs/packages/core/src/lib/strategies/projection.strategy.ts new file mode 100644 index 0000000000..4d8c410480 --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/strategies/projection.strategy.ts @@ -0,0 +1,181 @@ +import { + ApplicationRef, + ComponentFactoryResolver, + ComponentRef, + EmbeddedViewRef, + Injector, + TemplateRef, + Type, + ViewContainerRef, +} from '@angular/core'; +import { InferedInstanceOf } from '../models/utility'; +import { ContainerStrategy, CONTAINER_STRATEGY } from './container.strategy'; +import { + ComponentContextStrategy, + ContextStrategy, + CONTEXT_STRATEGY, + TemplateContextStrategy, +} from './context.strategy'; +import { DomStrategy, DOM_STRATEGY } from './dom.strategy'; + +export abstract class ProjectionStrategy { + constructor(public content: T) {} + + abstract injectContent(injector: Injector): ComponentRefOrEmbeddedViewRef; +} + +export class ComponentProjectionStrategy> extends ProjectionStrategy { + constructor( + component: T, + private containerStrategy: ContainerStrategy, + private contextStrategy: ContextStrategy = CONTEXT_STRATEGY.None(), + ) { + super(component); + } + + injectContent(injector: Injector) { + this.containerStrategy.prepare(); + + const resolver = injector.get(ComponentFactoryResolver) as ComponentFactoryResolver; + const factory = resolver.resolveComponentFactory>(this.content); + + const componentRef = this.containerStrategy.containerRef.createComponent( + factory, + this.containerStrategy.getIndex(), + injector, + ); + this.contextStrategy.setContext(componentRef); + + return componentRef as ComponentRefOrEmbeddedViewRef; + } +} + +export class RootComponentProjectionStrategy> extends ProjectionStrategy { + constructor( + component: T, + private contextStrategy: ContextStrategy = CONTEXT_STRATEGY.None(), + private domStrategy: DomStrategy = DOM_STRATEGY.AppendToBody(), + ) { + super(component); + } + + injectContent(injector: Injector) { + const appRef = injector.get(ApplicationRef); + const resolver = injector.get(ComponentFactoryResolver) as ComponentFactoryResolver; + const componentRef = resolver + .resolveComponentFactory>(this.content) + .create(injector); + + this.contextStrategy.setContext(componentRef); + + appRef.attachView(componentRef.hostView); + const element: HTMLElement = (componentRef.hostView as EmbeddedViewRef).rootNodes[0]; + this.domStrategy.insertElement(element); + + return componentRef as ComponentRefOrEmbeddedViewRef; + } +} + +export class TemplateProjectionStrategy> extends ProjectionStrategy { + constructor( + template: T, + private containerStrategy: ContainerStrategy, + private contextStrategy = CONTEXT_STRATEGY.None(), + ) { + super(template); + } + + injectContent(injector: Injector) { + this.containerStrategy.prepare(); + + const embeddedViewRef = this.containerStrategy.containerRef.createEmbeddedView( + this.content, + this.contextStrategy.context, + this.containerStrategy.getIndex(), + ); + embeddedViewRef.detectChanges(); + + return embeddedViewRef as ComponentRefOrEmbeddedViewRef; + } +} + +export const PROJECTION_STRATEGY = { + AppendComponentToBody>( + component: T, + contextStrategy?: ComponentContextStrategy, + ) { + return new RootComponentProjectionStrategy(component, contextStrategy); + }, + AppendComponentToContainer>( + component: T, + containerRef: ViewContainerRef, + contextStrategy?: ComponentContextStrategy, + ) { + return new ComponentProjectionStrategy( + component, + CONTAINER_STRATEGY.Append(containerRef), + contextStrategy, + ); + }, + AppendTemplateToContainer>( + template: T, + containerRef: ViewContainerRef, + contextStrategy?: TemplateContextStrategy, + ) { + return new TemplateProjectionStrategy( + template, + CONTAINER_STRATEGY.Append(containerRef), + contextStrategy, + ); + }, + PrependComponentToContainer>( + component: T, + containerRef: ViewContainerRef, + contextStrategy?: ComponentContextStrategy, + ) { + return new ComponentProjectionStrategy( + component, + CONTAINER_STRATEGY.Prepend(containerRef), + contextStrategy, + ); + }, + PrependTemplateToContainer>( + template: T, + containerRef: ViewContainerRef, + contextStrategy?: TemplateContextStrategy, + ) { + return new TemplateProjectionStrategy( + template, + CONTAINER_STRATEGY.Prepend(containerRef), + contextStrategy, + ); + }, + ProjectComponentToContainer>( + component: T, + containerRef: ViewContainerRef, + contextStrategy?: ComponentContextStrategy, + ) { + return new ComponentProjectionStrategy( + component, + CONTAINER_STRATEGY.Clear(containerRef), + contextStrategy, + ); + }, + ProjectTemplateToContainer>( + template: T, + containerRef: ViewContainerRef, + contextStrategy?: TemplateContextStrategy, + ) { + return new TemplateProjectionStrategy( + template, + CONTAINER_STRATEGY.Clear(containerRef), + contextStrategy, + ); + }, +}; + +type ComponentRefOrEmbeddedViewRef = T extends Type + ? ComponentRef + : T extends TemplateRef + ? EmbeddedViewRef + : never; diff --git a/npm/ng-packs/packages/core/src/lib/tests/projection.strategy.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/projection.strategy.spec.ts new file mode 100644 index 0000000000..71a531c5eb --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/tests/projection.strategy.spec.ts @@ -0,0 +1,245 @@ +import { + Component, + ComponentRef, + EmbeddedViewRef, + TemplateRef, + ViewChild, + ViewContainerRef, +} from '@angular/core'; +import { createComponentFactory, Spectator } from '@ngneat/spectator/jest'; +import { + ComponentProjectionStrategy, + ContainerStrategy, + CONTAINER_STRATEGY, + CONTEXT_STRATEGY, + DOM_STRATEGY, + PROJECTION_STRATEGY, + RootComponentProjectionStrategy, + TemplateProjectionStrategy, +} from '../strategies'; + +describe('ComponentProjectionStrategy', () => { + @Component({ + template: '
{{ bar || baz }}
', + }) + class TestComponent { + bar: string; + baz = 'baz'; + } + + @Component({ + template: '', + }) + class HostComponent { + @ViewChild('container', { static: true, read: ViewContainerRef }) + containerRef: ViewContainerRef; + } + + let containerStrategy: ContainerStrategy; + let spectator: Spectator; + let componentRef: ComponentRef; + + const createComponent = createComponentFactory({ + component: HostComponent, + entryComponents: [TestComponent], + }); + + beforeEach(() => { + spectator = createComponent({}); + containerStrategy = CONTAINER_STRATEGY.Clear(spectator.component.containerRef); + }); + + afterEach(() => { + componentRef.destroy(); + spectator.detectChanges(); + }); + + describe('#injectContent', () => { + it('should should insert content into container and return a ComponentRef', () => { + const strategy = new ComponentProjectionStrategy(TestComponent, containerStrategy); + componentRef = strategy.injectContent(spectator); + spectator.detectChanges(); + + const div = spectator.query('div.foo'); + expect(div.textContent).toBe('baz'); + expect(componentRef).toBeInstanceOf(ComponentRef); + }); + + it('should be able to map context to projected component', () => { + const contextStrategy = CONTEXT_STRATEGY.Component({ bar: 'bar' }); + const strategy = new ComponentProjectionStrategy( + TestComponent, + containerStrategy, + contextStrategy, + ); + componentRef = strategy.injectContent(spectator); + spectator.detectChanges(); + + const div = spectator.query('div.foo'); + expect(div.textContent).toBe('bar'); + expect(componentRef.instance.bar).toBe('bar'); + }); + }); +}); + +describe('RootComponentProjectionStrategy', () => { + @Component({ + template: '
{{ bar || baz }}
', + }) + class TestComponent { + bar: string; + baz = 'baz'; + } + + @Component({ template: '' }) + class HostComponent {} + + let spectator: Spectator; + let componentRef: ComponentRef; + + const createComponent = createComponentFactory({ + component: HostComponent, + entryComponents: [TestComponent], + }); + + beforeEach(() => { + spectator = createComponent({}); + }); + + afterEach(() => { + componentRef.destroy(); + spectator.detectChanges(); + }); + + describe('#injectContent', () => { + it('should should insert content into body and return a ComponentRef', () => { + const strategy = new RootComponentProjectionStrategy(TestComponent); + componentRef = strategy.injectContent(spectator); + spectator.detectChanges(); + + const div = document.querySelector('body > ng-component > div.foo'); + expect(div.textContent).toBe('baz'); + expect(componentRef).toBeInstanceOf(ComponentRef); + componentRef.destroy(); + spectator.detectChanges(); + }); + + it('should be able to map context to projected component', () => { + const contextStrategy = CONTEXT_STRATEGY.Component({ bar: 'bar' }); + const strategy = new RootComponentProjectionStrategy(TestComponent, contextStrategy); + componentRef = strategy.injectContent(spectator); + spectator.detectChanges(); + + const div = document.querySelector('body > ng-component > div.foo'); + expect(div.textContent).toBe('bar'); + expect(componentRef.instance.bar).toBe('bar'); + }); + }); +}); + +describe('TemplateProjectionStrategy', () => { + @Component({ + template: ` + +
{{ bar || baz }}
+
+ + `, + }) + class HostComponent { + @ViewChild('container', { static: true, read: ViewContainerRef }) + containerRef: ViewContainerRef; + + @ViewChild('template', { static: true }) + templateRef: TemplateRef<{ $implicit?: string }>; + + baz = 'baz'; + } + + let containerStrategy: ContainerStrategy; + let spectator: Spectator; + let embeddedViewRef: EmbeddedViewRef<{ $implicit?: string }>; + + const createComponent = createComponentFactory({ + component: HostComponent, + }); + + beforeEach(() => { + spectator = createComponent({}); + containerStrategy = CONTAINER_STRATEGY.Clear(spectator.component.containerRef); + }); + + afterEach(() => { + embeddedViewRef.destroy(); + spectator.detectChanges(); + }); + + describe('#injectContent', () => { + it('should should insert content into container and return an EmbeddedViewRef', () => { + const templateRef = spectator.component.templateRef; + const strategy = new TemplateProjectionStrategy(templateRef, containerStrategy); + embeddedViewRef = strategy.injectContent(spectator); + spectator.detectChanges(); + + const div = spectator.query('div.foo'); + expect(div.textContent).toBe('baz'); + expect(embeddedViewRef).toHaveProperty('detectChanges'); + expect(embeddedViewRef).toHaveProperty('markForCheck'); + expect(embeddedViewRef).toHaveProperty('detach'); + expect(embeddedViewRef).toHaveProperty('reattach'); + expect(embeddedViewRef).toHaveProperty('destroy'); + expect(embeddedViewRef).toHaveProperty('rootNodes'); + expect(embeddedViewRef).toHaveProperty('context'); + }); + + it('should be able to map context to projected template', () => { + const templateRef = spectator.component.templateRef; + const contextStrategy = CONTEXT_STRATEGY.Template({ $implicit: 'bar' }); + const strategy = new TemplateProjectionStrategy( + templateRef, + containerStrategy, + contextStrategy, + ); + embeddedViewRef = strategy.injectContent(spectator); + spectator.detectChanges(); + + const div = spectator.query('div.foo'); + expect(div.textContent).toBe('bar'); + expect(embeddedViewRef.context).toEqual(contextStrategy.context); + }); + }); +}); + +describe('PROJECTION_STRATEGY', () => { + const content = undefined; + const containerRef = ({ length: 0 } as any) as ViewContainerRef; + test.each` + name | Strategy | containerStrategy + ${'AppendComponentToContainer'} | ${ComponentProjectionStrategy} | ${CONTAINER_STRATEGY.Append} + ${'AppendTemplateToContainer'} | ${TemplateProjectionStrategy} | ${CONTAINER_STRATEGY.Append} + ${'PrependComponentToContainer'} | ${ComponentProjectionStrategy} | ${CONTAINER_STRATEGY.Prepend} + ${'PrependTemplateToContainer'} | ${TemplateProjectionStrategy} | ${CONTAINER_STRATEGY.Prepend} + ${'ProjectComponentToContainer'} | ${ComponentProjectionStrategy} | ${CONTAINER_STRATEGY.Clear} + ${'ProjectTemplateToContainer'} | ${TemplateProjectionStrategy} | ${CONTAINER_STRATEGY.Clear} + `( + 'should successfully map $name to $Strategy.name with $containerStrategy.name container strategy', + ({ name, Strategy, containerStrategy }) => { + expect(PROJECTION_STRATEGY[name](content, containerRef)).toEqual( + new Strategy(content, containerStrategy(containerRef)), + ); + }, + ); + + const contextStrategy = undefined; + test.each` + name | Strategy | domStrategy + ${'AppendComponentToBody'} | ${RootComponentProjectionStrategy} | ${DOM_STRATEGY.AppendToBody} + `( + 'should successfully map $name to $Strategy.name with $domStrategy.name dom strategy', + ({ name, Strategy, domStrategy }) => { + expect(PROJECTION_STRATEGY[name](content, contextStrategy)).toEqual( + new Strategy(content, contextStrategy, domStrategy()), + ); + }, + ); +}); From a46ddf5db19c541f6a0c06edca8b7cd338dda306 Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Fri, 10 Apr 2020 00:13:51 +0300 Subject: [PATCH 06/17] feat: add projectContent method to DomInsertionService --- .../src/lib/services/dom-insertion.service.ts | 14 +++- .../lib/tests/dom-insertion.service.spec.ts | 66 +++++++++++++++++-- 2 files changed, 73 insertions(+), 7 deletions(-) diff --git a/npm/ng-packs/packages/core/src/lib/services/dom-insertion.service.ts b/npm/ng-packs/packages/core/src/lib/services/dom-insertion.service.ts index f1a59e4e43..5e53a64995 100644 --- a/npm/ng-packs/packages/core/src/lib/services/dom-insertion.service.ts +++ b/npm/ng-packs/packages/core/src/lib/services/dom-insertion.service.ts @@ -1,10 +1,13 @@ -import { Injectable } from '@angular/core'; +import { Injectable, Injector, TemplateRef, Type } from '@angular/core'; import { ContentStrategy } from '../strategies/content.strategy'; +import { ProjectionStrategy } from '../strategies/projection.strategy'; import { generateHash } from '../utils'; @Injectable({ providedIn: 'root' }) export class DomInsertionService { - readonly inserted = new Set(); + readonly inserted = new Set(); + + constructor(private injector: Injector) {} insertContent(contentStrategy: ContentStrategy) { const hash = generateHash(contentStrategy.content); @@ -14,4 +17,11 @@ export class DomInsertionService { contentStrategy.insertElement(); this.inserted.add(hash); } + + projectContent | TemplateRef>( + projectionStrategy: ProjectionStrategy, + injector = this.injector, + ) { + return projectionStrategy.injectContent(injector); + } } diff --git a/npm/ng-packs/packages/core/src/lib/tests/dom-insertion.service.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/dom-insertion.service.spec.ts index 2570636197..8ae75c2f15 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/dom-insertion.service.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/dom-insertion.service.spec.ts @@ -1,15 +1,71 @@ +import { Component, ComponentRef, NgModule } from '@angular/core'; import { createServiceFactory, SpectatorService } from '@ngneat/spectator'; import { DomInsertionService } from '../services'; -import { CONTENT_STRATEGY } from '../strategies'; +import { CONTENT_STRATEGY, PROJECTION_STRATEGY } from '../strategies'; describe('DomInsertionService', () => { + @Component({ template: '
bar
' }) + class TestComponent {} + + // createServiceFactory does not accept entryComponents directly + @NgModule({ + declarations: [TestComponent], + entryComponents: [TestComponent], + }) + class TestModule {} + let spectator: SpectatorService; - const createService = createServiceFactory(DomInsertionService); + const createService = createServiceFactory({ + service: DomInsertionService, + imports: [TestModule], + }); + let styleElements: NodeListOf; beforeEach(() => (spectator = createService())); - it('should be insert an element', () => { - spectator.service.insertContent(CONTENT_STRATEGY.AppendStyleToHead('.test {}')); - expect(spectator.service.inserted.has(1437348290)).toBe(true); + afterEach(() => styleElements.forEach(element => element.remove())); + + describe('#insertContent', () => { + it('should be able to insert given content', () => { + spectator.service.insertContent(CONTENT_STRATEGY.AppendStyleToHead('.test {}')); + styleElements = document.head.querySelectorAll('style'); + expect(styleElements.length).toBe(1); + expect(styleElements[0].textContent).toBe('.test {}'); + }); + + it('should insert only once', () => { + expect(spectator.service.inserted.has(1437348290)).toBe(false); + + spectator.service.insertContent(CONTENT_STRATEGY.AppendStyleToHead('.test {}')); + styleElements = document.head.querySelectorAll('style'); + + expect(styleElements.length).toBe(1); + expect(styleElements[0].textContent).toBe('.test {}'); + expect(spectator.service.inserted.has(1437348290)).toBe(true); + + spectator.service.insertContent(CONTENT_STRATEGY.AppendStyleToHead('.test {}')); + styleElements = document.head.querySelectorAll('style'); + + expect(styleElements.length).toBe(1); + expect(styleElements[0].textContent).toBe('.test {}'); + expect(spectator.service.inserted.has(1437348290)).toBe(true); + }); + + it('should be able to insert given content', () => { + spectator.service.insertContent(CONTENT_STRATEGY.AppendStyleToHead('.test {}')); + expect(spectator.service.inserted.has(1437348290)).toBe(true); + }); + }); + + describe('#projectContent', () => { + it('should call injectContent of given projectionStrategy and return what it returns', () => { + const strategy = PROJECTION_STRATEGY.AppendComponentToBody(TestComponent); + const componentRef = spectator.service.projectContent(strategy); + const foo = document.querySelector('body > ng-component > div.foo'); + + expect(componentRef).toBeInstanceOf(ComponentRef); + expect(foo.textContent).toBe('bar'); + componentRef.destroy(); + }); }); }); From 0bb9d2176020c35f62c4544ec735f9f59dbac19d Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Fri, 10 Apr 2020 01:34:59 +0300 Subject: [PATCH 07/17] feat: simplify predefined projection strategies --- .../src/lib/strategies/projection.strategy.ts | 61 +++++++++---------- .../src/lib/tests/projection.strategy.spec.ts | 49 ++++++++++++--- 2 files changed, 68 insertions(+), 42 deletions(-) diff --git a/npm/ng-packs/packages/core/src/lib/strategies/projection.strategy.ts b/npm/ng-packs/packages/core/src/lib/strategies/projection.strategy.ts index 4d8c410480..ad5927e09e 100644 --- a/npm/ng-packs/packages/core/src/lib/strategies/projection.strategy.ts +++ b/npm/ng-packs/packages/core/src/lib/strategies/projection.strategy.ts @@ -8,14 +8,9 @@ import { Type, ViewContainerRef, } from '@angular/core'; -import { InferedInstanceOf } from '../models/utility'; +import { InferedContextOf, InferedInstanceOf } from '../models/utility'; import { ContainerStrategy, CONTAINER_STRATEGY } from './container.strategy'; -import { - ComponentContextStrategy, - ContextStrategy, - CONTEXT_STRATEGY, - TemplateContextStrategy, -} from './context.strategy'; +import { ContextStrategy, CONTEXT_STRATEGY } from './context.strategy'; import { DomStrategy, DOM_STRATEGY } from './dom.strategy'; export abstract class ProjectionStrategy { @@ -78,14 +73,14 @@ export class RootComponentProjectionStrategy> extends Projec export class TemplateProjectionStrategy> extends ProjectionStrategy { constructor( - template: T, + templateRef: T, private containerStrategy: ContainerStrategy, private contextStrategy = CONTEXT_STRATEGY.None(), ) { - super(template); + super(templateRef); } - injectContent(injector: Injector) { + injectContent() { this.containerStrategy.prepare(); const embeddedViewRef = this.containerStrategy.containerRef.createEmbeddedView( @@ -100,76 +95,76 @@ export class TemplateProjectionStrategy> extends Proj } export const PROJECTION_STRATEGY = { - AppendComponentToBody>( - component: T, - contextStrategy?: ComponentContextStrategy, - ) { - return new RootComponentProjectionStrategy(component, contextStrategy); + AppendComponentToBody>(component: T, context?: InferedInstanceOf) { + return new RootComponentProjectionStrategy( + component, + context && CONTEXT_STRATEGY.Component(context), + ); }, AppendComponentToContainer>( component: T, containerRef: ViewContainerRef, - contextStrategy?: ComponentContextStrategy, + context?: InferedInstanceOf, ) { return new ComponentProjectionStrategy( component, CONTAINER_STRATEGY.Append(containerRef), - contextStrategy, + context && CONTEXT_STRATEGY.Component(context), ); }, AppendTemplateToContainer>( - template: T, + templateRef: T, containerRef: ViewContainerRef, - contextStrategy?: TemplateContextStrategy, + context?: InferedContextOf, ) { return new TemplateProjectionStrategy( - template, + templateRef, CONTAINER_STRATEGY.Append(containerRef), - contextStrategy, + context && CONTEXT_STRATEGY.Template(context), ); }, PrependComponentToContainer>( component: T, containerRef: ViewContainerRef, - contextStrategy?: ComponentContextStrategy, + context?: InferedInstanceOf, ) { return new ComponentProjectionStrategy( component, CONTAINER_STRATEGY.Prepend(containerRef), - contextStrategy, + context && CONTEXT_STRATEGY.Component(context), ); }, PrependTemplateToContainer>( - template: T, + templateRef: T, containerRef: ViewContainerRef, - contextStrategy?: TemplateContextStrategy, + context?: InferedContextOf, ) { return new TemplateProjectionStrategy( - template, + templateRef, CONTAINER_STRATEGY.Prepend(containerRef), - contextStrategy, + context && CONTEXT_STRATEGY.Template(context), ); }, ProjectComponentToContainer>( component: T, containerRef: ViewContainerRef, - contextStrategy?: ComponentContextStrategy, + context?: InferedInstanceOf, ) { return new ComponentProjectionStrategy( component, CONTAINER_STRATEGY.Clear(containerRef), - contextStrategy, + context && CONTEXT_STRATEGY.Component(context), ); }, ProjectTemplateToContainer>( - template: T, + templateRef: T, containerRef: ViewContainerRef, - contextStrategy?: TemplateContextStrategy, + context?: InferedContextOf, ) { return new TemplateProjectionStrategy( - template, + templateRef, CONTAINER_STRATEGY.Clear(containerRef), - contextStrategy, + context && CONTEXT_STRATEGY.Template(context), ); }, }; diff --git a/npm/ng-packs/packages/core/src/lib/tests/projection.strategy.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/projection.strategy.spec.ts index 71a531c5eb..1166d44f57 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/projection.strategy.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/projection.strategy.spec.ts @@ -178,7 +178,7 @@ describe('TemplateProjectionStrategy', () => { it('should should insert content into container and return an EmbeddedViewRef', () => { const templateRef = spectator.component.templateRef; const strategy = new TemplateProjectionStrategy(templateRef, containerStrategy); - embeddedViewRef = strategy.injectContent(spectator); + embeddedViewRef = strategy.injectContent(); spectator.detectChanges(); const div = spectator.query('div.foo'); @@ -200,7 +200,7 @@ describe('TemplateProjectionStrategy', () => { containerStrategy, contextStrategy, ); - embeddedViewRef = strategy.injectContent(spectator); + embeddedViewRef = strategy.injectContent(); spectator.detectChanges(); const div = spectator.query('div.foo'); @@ -213,6 +213,8 @@ describe('TemplateProjectionStrategy', () => { describe('PROJECTION_STRATEGY', () => { const content = undefined; const containerRef = ({ length: 0 } as any) as ViewContainerRef; + let context = undefined; + test.each` name | Strategy | containerStrategy ${'AppendComponentToContainer'} | ${ComponentProjectionStrategy} | ${CONTAINER_STRATEGY.Append} @@ -222,23 +224,52 @@ describe('PROJECTION_STRATEGY', () => { ${'ProjectComponentToContainer'} | ${ComponentProjectionStrategy} | ${CONTAINER_STRATEGY.Clear} ${'ProjectTemplateToContainer'} | ${TemplateProjectionStrategy} | ${CONTAINER_STRATEGY.Clear} `( - 'should successfully map $name to $Strategy.name with $containerStrategy.name container strategy', + 'should successfully map $name to $Strategy.name with $containerStrategy.name container strategy and $contextStrategy.name context strategy', ({ name, Strategy, containerStrategy }) => { - expect(PROJECTION_STRATEGY[name](content, containerRef)).toEqual( - new Strategy(content, containerStrategy(containerRef)), + expect(PROJECTION_STRATEGY[name](content, containerRef, context)).toEqual( + new Strategy(content, containerStrategy(containerRef), CONTEXT_STRATEGY.None()), ); }, ); - - const contextStrategy = undefined; test.each` name | Strategy | domStrategy ${'AppendComponentToBody'} | ${RootComponentProjectionStrategy} | ${DOM_STRATEGY.AppendToBody} `( 'should successfully map $name to $Strategy.name with $domStrategy.name dom strategy', ({ name, Strategy, domStrategy }) => { - expect(PROJECTION_STRATEGY[name](content, contextStrategy)).toEqual( - new Strategy(content, contextStrategy, domStrategy()), + expect(PROJECTION_STRATEGY[name](content, context)).toEqual( + new Strategy(content, CONTEXT_STRATEGY.None(), domStrategy()), + ); + }, + ); + + test.each` + name | Strategy | containerStrategy | contextStrategy + ${'AppendComponentToContainer'} | ${ComponentProjectionStrategy} | ${CONTAINER_STRATEGY.Append} | ${CONTEXT_STRATEGY.Component} + ${'AppendTemplateToContainer'} | ${TemplateProjectionStrategy} | ${CONTAINER_STRATEGY.Append} | ${CONTEXT_STRATEGY.Template} + ${'PrependComponentToContainer'} | ${ComponentProjectionStrategy} | ${CONTAINER_STRATEGY.Prepend} | ${CONTEXT_STRATEGY.Component} + ${'PrependTemplateToContainer'} | ${TemplateProjectionStrategy} | ${CONTAINER_STRATEGY.Prepend} | ${CONTEXT_STRATEGY.Template} + ${'ProjectComponentToContainer'} | ${ComponentProjectionStrategy} | ${CONTAINER_STRATEGY.Clear} | ${CONTEXT_STRATEGY.Component} + ${'ProjectTemplateToContainer'} | ${TemplateProjectionStrategy} | ${CONTAINER_STRATEGY.Clear} | ${CONTEXT_STRATEGY.Template} + `( + 'should successfully map $name to $Strategy.name with $containerStrategy.name container strategy and $contextStrategy.name context strategy', + ({ name, Strategy, containerStrategy, contextStrategy }) => { + context = { x: true }; + expect(PROJECTION_STRATEGY[name](content, containerRef, context)).toEqual( + new Strategy(content, containerStrategy(containerRef), contextStrategy(context)), + ); + }, + ); + + test.each` + name | Strategy | contextStrategy | domStrategy + ${'AppendComponentToBody'} | ${RootComponentProjectionStrategy} | ${CONTEXT_STRATEGY.Component} | ${DOM_STRATEGY.AppendToBody} + `( + 'should successfully map $name to $Strategy.name with $contextStrategy.name context strategy and $domStrategy.name dom strategy', + ({ name, Strategy, domStrategy, contextStrategy }) => { + context = { x: true }; + expect(PROJECTION_STRATEGY[name](content, context)).toEqual( + new Strategy(content, contextStrategy(context), domStrategy()), ); }, ); From 9af0c5843b66b5f3d6fc3afb497fbf29b909b47d Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Fri, 10 Apr 2020 02:27:37 +0300 Subject: [PATCH 08/17] fix: replace infered with inferred --- .../packages/core/src/lib/models/utility.ts | 4 ++-- .../src/lib/strategies/context.strategy.ts | 12 +++++------ .../src/lib/strategies/projection.strategy.ts | 20 +++++++++---------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/npm/ng-packs/packages/core/src/lib/models/utility.ts b/npm/ng-packs/packages/core/src/lib/models/utility.ts index 5fe76af3c9..e84ad15340 100644 --- a/npm/ng-packs/packages/core/src/lib/models/utility.ts +++ b/npm/ng-packs/packages/core/src/lib/models/utility.ts @@ -1,4 +1,4 @@ import { TemplateRef, Type } from '@angular/core'; -export type InferedInstanceOf = T extends Type ? U : never; -export type InferedContextOf = T extends TemplateRef ? U : never; +export type InferredInstanceOf = T extends Type ? U : never; +export type InferredContextOf = T extends TemplateRef ? U : never; diff --git a/npm/ng-packs/packages/core/src/lib/strategies/context.strategy.ts b/npm/ng-packs/packages/core/src/lib/strategies/context.strategy.ts index e3d16c0573..21007eae1c 100644 --- a/npm/ng-packs/packages/core/src/lib/strategies/context.strategy.ts +++ b/npm/ng-packs/packages/core/src/lib/strategies/context.strategy.ts @@ -1,11 +1,11 @@ import { ComponentRef, TemplateRef, Type } from '@angular/core'; -import { InferedContextOf, InferedInstanceOf } from '../models'; +import { InferredContextOf, InferredInstanceOf } from '../models'; export abstract class ContextStrategy { constructor(public context: Partial>) {} /* tslint:disable-next-line:no-unused-variable */ - setContext(componentRef?: ComponentRef>): Partial> { + setContext(componentRef?: ComponentRef>): Partial> { return this.context; } } @@ -19,7 +19,7 @@ export class NoContextStrategy< } export class ComponentContextStrategy = any> extends ContextStrategy { - setContext(componentRef: ComponentRef>): Partial> { + setContext(componentRef: ComponentRef>): Partial> { Object.keys(this.context).forEach(key => (componentRef.instance[key] = this.context[key])); componentRef.changeDetectorRef.detectChanges(); return this.context; @@ -27,7 +27,7 @@ export class ComponentContextStrategy = any> extends Context } export class TemplateContextStrategy = any> extends ContextStrategy { - setContext(): Partial> { + setContext(): Partial> { return this.context; } } @@ -36,10 +36,10 @@ export const CONTEXT_STRATEGY = { None | TemplateRef = any>() { return new NoContextStrategy(); }, - Component = any>(context: Partial>) { + Component = any>(context: Partial>) { return new ComponentContextStrategy(context); }, - Template = any>(context: Partial>) { + Template = any>(context: Partial>) { return new TemplateContextStrategy(context); }, }; diff --git a/npm/ng-packs/packages/core/src/lib/strategies/projection.strategy.ts b/npm/ng-packs/packages/core/src/lib/strategies/projection.strategy.ts index ad5927e09e..e7a62383a2 100644 --- a/npm/ng-packs/packages/core/src/lib/strategies/projection.strategy.ts +++ b/npm/ng-packs/packages/core/src/lib/strategies/projection.strategy.ts @@ -8,7 +8,7 @@ import { Type, ViewContainerRef, } from '@angular/core'; -import { InferedContextOf, InferedInstanceOf } from '../models/utility'; +import { InferredContextOf, InferredInstanceOf } from '../models/utility'; import { ContainerStrategy, CONTAINER_STRATEGY } from './container.strategy'; import { ContextStrategy, CONTEXT_STRATEGY } from './context.strategy'; import { DomStrategy, DOM_STRATEGY } from './dom.strategy'; @@ -32,7 +32,7 @@ export class ComponentProjectionStrategy> extends Projection this.containerStrategy.prepare(); const resolver = injector.get(ComponentFactoryResolver) as ComponentFactoryResolver; - const factory = resolver.resolveComponentFactory>(this.content); + const factory = resolver.resolveComponentFactory>(this.content); const componentRef = this.containerStrategy.containerRef.createComponent( factory, @@ -58,7 +58,7 @@ export class RootComponentProjectionStrategy> extends Projec const appRef = injector.get(ApplicationRef); const resolver = injector.get(ComponentFactoryResolver) as ComponentFactoryResolver; const componentRef = resolver - .resolveComponentFactory>(this.content) + .resolveComponentFactory>(this.content) .create(injector); this.contextStrategy.setContext(componentRef); @@ -95,7 +95,7 @@ export class TemplateProjectionStrategy> extends Proj } export const PROJECTION_STRATEGY = { - AppendComponentToBody>(component: T, context?: InferedInstanceOf) { + AppendComponentToBody>(component: T, context?: InferredInstanceOf) { return new RootComponentProjectionStrategy( component, context && CONTEXT_STRATEGY.Component(context), @@ -104,7 +104,7 @@ export const PROJECTION_STRATEGY = { AppendComponentToContainer>( component: T, containerRef: ViewContainerRef, - context?: InferedInstanceOf, + context?: InferredInstanceOf, ) { return new ComponentProjectionStrategy( component, @@ -115,7 +115,7 @@ export const PROJECTION_STRATEGY = { AppendTemplateToContainer>( templateRef: T, containerRef: ViewContainerRef, - context?: InferedContextOf, + context?: InferredContextOf, ) { return new TemplateProjectionStrategy( templateRef, @@ -126,7 +126,7 @@ export const PROJECTION_STRATEGY = { PrependComponentToContainer>( component: T, containerRef: ViewContainerRef, - context?: InferedInstanceOf, + context?: InferredInstanceOf, ) { return new ComponentProjectionStrategy( component, @@ -137,7 +137,7 @@ export const PROJECTION_STRATEGY = { PrependTemplateToContainer>( templateRef: T, containerRef: ViewContainerRef, - context?: InferedContextOf, + context?: InferredContextOf, ) { return new TemplateProjectionStrategy( templateRef, @@ -148,7 +148,7 @@ export const PROJECTION_STRATEGY = { ProjectComponentToContainer>( component: T, containerRef: ViewContainerRef, - context?: InferedInstanceOf, + context?: InferredInstanceOf, ) { return new ComponentProjectionStrategy( component, @@ -159,7 +159,7 @@ export const PROJECTION_STRATEGY = { ProjectTemplateToContainer>( templateRef: T, containerRef: ViewContainerRef, - context?: InferedContextOf, + context?: InferredContextOf, ) { return new TemplateProjectionStrategy( templateRef, From 22df327dd0a128f9b887611430063a1e6751144d Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Fri, 10 Apr 2020 02:43:32 +0300 Subject: [PATCH 09/17] docs: add how context strategies work --- docs/en/UI/Angular/Context-Strategy.md | 117 +++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 docs/en/UI/Angular/Context-Strategy.md diff --git a/docs/en/UI/Angular/Context-Strategy.md b/docs/en/UI/Angular/Context-Strategy.md new file mode 100644 index 0000000000..a474c50ad6 --- /dev/null +++ b/docs/en/UI/Angular/Context-Strategy.md @@ -0,0 +1,117 @@ +# ContextStrategy + +`ContextStrategy` is an abstract class exposed by @abp/ng.core package. There are three context strategies extending it: `ComponentContextStrategy`, `TemplateContextStrategy`, and `NoContextStrategy`. Implementing the same methods and properties, all of these strategies help you define how projected content will get their context. + + + +## ComponentContextStrategy + +`ComponentContextStrategy` is a class that extends `ContextStrategy`. It lets you **pass context to a projected component**. + + +### constructor + +```js +constructor(public context: Partial>) {} +``` + +- `T` refers to component type here, i.e. `Type`. +- `InferredInstanceOf` is a utility type exposed by @abp/ng.core package. It infers component shape. +- `context` will be mapped to properties of the projected component. + + +### setContext + +```js +setContext(componentRef: ComponentRef>): Partial> +``` + +This method maps each prop of the context to the component property with the same name and calls change detection. It returns the context after mapping. + + + +## TemplateContextStrategy + +`TemplateContextStrategy` is a class that extends `ContextStrategy`. It lets you **pass context to a projected template**. + + +### constructor + +```js +constructor(public context: Partial>) {} +``` + +- `T` refers to template context type here, i.e. `TemplateRef`. +- `InferredContextOf` is a utility type exposed by @abp/ng.core package. It infers context shape. +- `context` will be mapped to properties of the projected template. + + +### setContext + +```js +setContext(): Partial> +``` + +This method does nothing and only returns the context, because template context is not mapped but passed in as parameter to `createEmbeddedView` method. + + + +## NoContextStrategy + +`NoContextStrategy` is a class that extends `ContextStrategy`. It lets you **skip passing any context to projected content**. + + +### constructor + +```js +constructor() +``` + +Unlike other context strategies, `NoContextStrategy` contructor takes no parameters. + + +### setContext + +```js +setContext(): undefined +``` + +Since there is no context, this method gets no parameters and will return `undefined`. + + + +## Predefined Context Strategies + +Predefined context strategies are accessible via `CONTEXT_STRATEGY` constant. + + +### None + +```js +CONTEXT_STRATEGY.None() +``` + +This strategy will not pass any context to the projected content. + + +### Component + +```js +CONTEXT_STRATEGY.Component(context: Partial>) +``` + +This strategy will help you pass the given context to the projected component. + + +### Template + +```js +CONTEXT_STRATEGY.Template(context: Partial>) +``` + +This strategy will help you pass the given context to the projected template. + + +## See Also + +- [ProjectionStrategy](./Projection-Strategy.md) From e13b9c83efbd9679a56657392150ff440302febc Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Fri, 10 Apr 2020 02:43:41 +0300 Subject: [PATCH 10/17] docs: add how container strategies work --- docs/en/UI/Angular/Container-Strategy.md | 101 +++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 docs/en/UI/Angular/Container-Strategy.md diff --git a/docs/en/UI/Angular/Container-Strategy.md b/docs/en/UI/Angular/Container-Strategy.md new file mode 100644 index 0000000000..3610c5ddd8 --- /dev/null +++ b/docs/en/UI/Angular/Container-Strategy.md @@ -0,0 +1,101 @@ +# ContainerStrategy + +`ContainerStrategy` is an abstract class exposed by @abp/ng.core package. There are two container strategies extending it: `ClearContainerStrategy` and `InsertIntoContainerStrategy`. Implementing the same methods and properties, both of these strategies help you define how your containers will be prepared and where your content will be projected. + + + +## API + +`ClearContainerStrategy` is a class that extends `ContainerStrategy`. It lets you **clear a container before projecting content in it**. + + +### constructor + +```js +constructor( + public containerRef: ViewContainerRef, + private index?: number, // works only in InsertIntoContainerStrategy +) +``` + +- `containerRef` is the `ViewContainerRef` that will be used when projecting the content. + + +### getIndex + +```js +getIndex(): number +``` + +This method return the given index clamped by `0` and `length` of the `containerRef`. For strategies without an index, it returns `0`. + + +### prepare + +```js +prepare(): void +``` + +This method is called before content projection. Based on used container strategy, it either clears the container or does nothing (noop). + + + +## ClearContainerStrategy + +`ClearContainerStrategy` is a class that extends `ContainerStrategy`. It lets you **clear a container before projecting content in it**. + + + +## InsertIntoContainerStrategy + +`InsertIntoContainerStrategy` is a class that extends `ContainerStrategy`. It lets you **project your content at a specific node index in the container**. + + + +## Predefined Container Strategies + +Predefined container strategies are accessible via `CONTAINER_STRATEGY` constant. + + +### Clear + +```js +CONTAINER_STRATEGY.Clear(containerRef: ViewContainerRef) +``` + +Clears given container before content projection. + + +### Append + +```js +CONTAINER_STRATEGY.Append(containerRef: ViewContainerRef) +``` + +Projected content will be appended to the container. + + +### Prepend + +```js +CONTAINER_STRATEGY.Prepend(containerRef: ViewContainerRef) +``` + +Projected content will be prepended to the container. + + +### Insert + +```js +CONTAINER_STRATEGY.Insert( + containerRef: ViewContainerRef, + index: number, +) +``` + +Projected content will be inserted into to the container at given index (clamped by `0` and `length` of the `containerRef`). + + +## See Also + +- [ProjectionStrategy](./Projection-Strategy.md) From 6d5df6127c6a36490fbb7fdb45b543bebb323749 Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Fri, 10 Apr 2020 02:44:40 +0300 Subject: [PATCH 11/17] docs: fix predefined loading strategies description --- docs/en/UI/Angular/Loading-Strategy.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/UI/Angular/Loading-Strategy.md b/docs/en/UI/Angular/Loading-Strategy.md index 1a7e7b362f..5322d13eda 100644 --- a/docs/en/UI/Angular/Loading-Strategy.md +++ b/docs/en/UI/Angular/Loading-Strategy.md @@ -57,7 +57,7 @@ This method creates and returns an observable stream that emits on success and t ## Predefined Loading Strategies -Predefined content security strategies are accessible via `LOADING_STRATEGY` constant. +Predefined loading strategies are accessible via `LOADING_STRATEGY` constant. ### AppendAnonymousScriptToHead From ece26207d08c124440f9f81c85860f44dee7720b Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Fri, 10 Apr 2020 02:45:13 +0300 Subject: [PATCH 12/17] docs: add how projection strategies work --- docs/en/UI/Angular/Projection-Strategy.md | 200 ++++++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 docs/en/UI/Angular/Projection-Strategy.md diff --git a/docs/en/UI/Angular/Projection-Strategy.md b/docs/en/UI/Angular/Projection-Strategy.md new file mode 100644 index 0000000000..4d546566b3 --- /dev/null +++ b/docs/en/UI/Angular/Projection-Strategy.md @@ -0,0 +1,200 @@ +# ProjectionStrategy + +`ProjectionStrategy` is an abstract class exposed by @abp/ng.core package. There are three projection strategies extending it: `ComponentProjectionStrategy`, `RootComponentProjectionStrategy`, and `TemplateProjectionStrategy`. Implementing the same methods and properties, all of these strategies help you define how your content projection will work. + + + +## ComponentProjectionStrategy + +`ComponentProjectionStrategy` is a class that extends `ProjectionStrategy`. It lets you **project a component into a container**. + + +### constructor + +```js +constructor( + component: T, + private containerStrategy: ContainerStrategy, + private contextStrategy?: ContextStrategy, +) +``` + +- `component` is class of the component you would like to project. +- `containerStrategy` is the `ContainerStrategy` that will be used when projecting the component. +- `contextStrategy` is the `ContextStrategy` that will be used on the projected component. (_default: None_) + +Please refer to [ContainerStrategy](./Container-Strategy.md) and [ContextStrategy](./Context-Strategy.md) documentation for their usage. + + +### injectContent + +```js +injectContent(injector: Injector): ComponentRef +``` + +This method prepares the container, resolves the component, sets its context, and projects it to the container. It returns a `ComponentRef` instance, which you should keep in order to clear projected components later on. + + + +## RootComponentProjectionStrategy + +`RootComponentProjectionStrategy` is a class that extends `ProjectionStrategy`. It lets you **project a component into the document**, such as appending it to ``. + + +### constructor + +```js +constructor( + component: T, + private contextStrategy?: ContextStrategy, + private domStrategy?: DomStrategy, +) +``` + +- `component` is class of the component you would like to project. +- `contextStrategy` is the `ContextStrategy` that will be used on the projected component. (_default: None_) +- `domStrategy` is the `DomStrategy` that will be used when inserting component. (_default: AppendToBody_) + +Please refer to [ContextStrategy](./Context-Strategy.md) and [DomStrategy](./Dom-Strategy.md) documentation for their usage. + + +### injectContent + +```js +injectContent(injector: Injector): ComponentRef +``` + +This method resolves the component, sets its context, and projects it to the document. It returns a `ComponentRef` instance, which you should keep in order to clear projected components later on. + + + +## TemplateProjectionStrategy + +`TemplateProjectionStrategy` is a class that extends `ProjectionStrategy`. It lets you **project a template into a container**. + + +### constructor + +```js +constructor( + template: T, + private containerStrategy: ContainerStrategy, + private contextStrategy?: ContextStrategy, +) +``` + +- `template` is `TemplateRef` you would like to project. +- `containerStrategy` is the `ContainerStrategy` that will be used when projecting the component. +- `contextStrategy` is the `ContextStrategy` that will be used on the projected component. (_default: None_) + +Please refer to [ContainerStrategy](./Container-Strategy.md) and [ContextStrategy](./Context-Strategy.md) documentation for their usage. + + +### injectContent + +```js +injectContent(): EmbeddedViewRef +``` + +This method prepares the container, and projects the template together with the defined context to it. It returns an `EmbeddedViewRef`, which you should keep in order to clear projected templates later on. + + + +## Predefined Projection Strategies + +Predefined projection strategies are accessible via `PROJECTION_STRATEGY` constant. + + +### AppendComponentToBody + +```js +PROJECTION_STRATEGY.AppendComponentToBody( + component: T, + contextStrategy?: ComponentContextStrategy, +) +``` + +Sets given context to the component and places it at the **end** of `` tag in the document. + + +### AppendComponentToContainer + +```js +PROJECTION_STRATEGY.AppendComponentToContainer( + component: T, + containerRef: ViewContainerRef, + contextStrategy?: ComponentContextStrategy, +) +``` + +Sets given context to the component and places it at the **end** of the container. + + +### AppendTemplateToContainer + +```js +PROJECTION_STRATEGY.AppendTemplateToContainer( + templateRef: T, + containerRef: ViewContainerRef, + contextStrategy?: ComponentContextStrategy, +) +``` + +Sets given context to the template and places it at the **end** of the container. + + +### PrependComponentToContainer + +```js +PROJECTION_STRATEGY.PrependComponentToContainer( + component: T, + containerRef: ViewContainerRef, + contextStrategy?: ComponentContextStrategy, +) +``` + +Sets given context to the component and places it at the **beginning** of the container. + + +### PrependTemplateToContainer + +```js +PROJECTION_STRATEGY.PrependTemplateToContainer( + templateRef: T, + containerRef: ViewContainerRef, + contextStrategy?: ComponentContextStrategy, +) +``` + +Sets given context to the template and places it at the **beginning** of the container. + + +### ProjectComponentToContainer + +```js +PROJECTION_STRATEGY.ProjectComponentToContainer( + component: T, + containerRef: ViewContainerRef, + contextStrategy?: ComponentContextStrategy, +) +``` + +Clears the container, sets given context to the component, and places it **in the cleared** the container. + + +### ProjectTemplateToContainer + +```js +PROJECTION_STRATEGY.ProjectTemplateToContainer( + templateRef: T, + containerRef: ViewContainerRef, + contextStrategy?: ComponentContextStrategy, +) +``` + +Clears the container, sets given context to the template, and places it **in the cleared** the container. + + +## See Also + +- [DomInsertionService](./Dom-Insertion-Service.md) From f1f79b5538a79a7f8f2cb11bd024af5fad561ce9 Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Fri, 10 Apr 2020 02:45:41 +0300 Subject: [PATCH 13/17] docs: add link to projection strategies --- docs/en/UI/Angular/Dom-Strategy.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/UI/Angular/Dom-Strategy.md b/docs/en/UI/Angular/Dom-Strategy.md index 2318e13205..e7b6c68b0f 100644 --- a/docs/en/UI/Angular/Dom-Strategy.md +++ b/docs/en/UI/Angular/Dom-Strategy.md @@ -87,3 +87,4 @@ DOM_STRATEGY.BeforeElement(target: HTMLElement) - [LazyLoadService](./Lazy-Load-Service.md) - [LoadingStrategy](./Loading-Strategy.md) - [ContentStrategy](./Content-Strategy.md) +- [ProjectionStrategy](./Projection-Strategy.md) From 4df815ce9cca1fe51724f7bd9c8b9b2f9d13e283 Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Fri, 10 Apr 2020 02:46:13 +0300 Subject: [PATCH 14/17] docs: add how to project components and templates --- docs/en/UI/Angular/Dom-Insertion-Service.md | 36 +++++++++++++++++---- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/docs/en/UI/Angular/Dom-Insertion-Service.md b/docs/en/UI/Angular/Dom-Insertion-Service.md index 5d7714d948..5159ae2c35 100644 --- a/docs/en/UI/Angular/Dom-Insertion-Service.md +++ b/docs/en/UI/Angular/Dom-Insertion-Service.md @@ -2,7 +2,6 @@ You can use the `DomInsertionService` in @abp/ng.core package in order to insert scripts and styles in an easy and explicit way. - ## Getting Started You do not have to provide the `DomInsertionService` at module or component level, because it is already **provided in root**. You can inject and start using it immediately in your components, directives, or services. @@ -20,8 +19,7 @@ class DemoComponent { ## Usage -You can use the `insertContent` method of `DomInsertionService` to create a `` element will place at the **end Please refer to [ContentStrategy](./Content-Strategy.md) to see all available content strategies and how you can build your own content strategy. - ### How to Insert Styles If you pass a `StyleContentStrategy` instance as the first parameter of `insertContent` method, the `DomInsertionService` will create a `` element will place at t Please refer to [ContentStrategy](./Content-Strategy.md) to see all available content strategies and how you can build your own content strategy. +### How to Project Components & Templates + +If you pass a `ProjectionStrategy` as the first parameter of `projectContent` method, the `DomInsertionService` will resolve the projected component or template and place it at the designated target, such as containers or document body. If provided, it will also pass the component or the template a context. + +```js +const componentRef = this.domInsertionService.projectContent( + PROJECTION_STRATEGY.AppendComponentToBody(SomeOverlayComponent) +); +``` + +In the example above, `SomeOverlayComponent` component will placed at the **end** of `` and a `ComponentRef` will be returned. + +> You should keep the returned `ComponentRef` instance, as it is a reference to the projected component and you will need that reference to destroy the projected view and the component instance. + +```js +const componentRef = this.domInsertionService.projectContent( + PROJECTION_STRATEGY.ProjectComponentToContainer( + SomeOverlayComponent, + viewContainerRefOfTarget, + { someProp: "SOME_VALUE" } + ) +); +``` + +In this example, the `viewContainerRefOfTarget`, which is a `ViewContainerRef` instance, will be cleared and `SomeOverlayComponent` component will placed inside it. Moreover, the given context will be applied, so `someProp` of the component will be set to `SOME_VALUE`. + +Please refer to [ProjectionStrategy](./Projection-Strategy.md) to see all available projection strategies and how you can build your own projection strategy. ## API ### insertContent ```js -insertContent(strategy: ContentStrategy): void +injectContent(injector: Injector): ComponentRef | EmbeddedViewRef ``` -`strategy` parameter is the primary focus here and is explained above. +`injector` parameter is the `Injector` instance you can pass to the projected content. It is not used in `TemplateProjectionStrategy`. ## What's Next? From af51255d3dff3c11b9cbfabe18928773f921c65a Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Fri, 10 Apr 2020 09:56:17 +0300 Subject: [PATCH 15/17] refactor: make content projection a separate service --- .../UI/Angular/Content-Projection-Service.md | 78 +++++++++++++++++++ docs/en/UI/Angular/Dom-Insertion-Service.md | 36 +-------- docs/en/docs-nav.json | 4 + .../services/content-projection.service.ts | 14 ++++ .../src/lib/services/dom-insertion.service.ts | 10 +-- .../packages/core/src/lib/services/index.ts | 1 + .../tests/content-projection.service.spec.ts | 38 +++++++++ .../lib/tests/dom-insertion.service.spec.ts | 32 +------- 8 files changed, 143 insertions(+), 70 deletions(-) create mode 100644 docs/en/UI/Angular/Content-Projection-Service.md create mode 100644 npm/ng-packs/packages/core/src/lib/services/content-projection.service.ts create mode 100644 npm/ng-packs/packages/core/src/lib/tests/content-projection.service.spec.ts diff --git a/docs/en/UI/Angular/Content-Projection-Service.md b/docs/en/UI/Angular/Content-Projection-Service.md new file mode 100644 index 0000000000..6bd30e44c6 --- /dev/null +++ b/docs/en/UI/Angular/Content-Projection-Service.md @@ -0,0 +1,78 @@ +# Content Projection + +You can use the `ContentProjectionService` in @abp/ng.core package in order to project content in an easy and explicit way. + +## Getting Started + +You do not have to provide the `ContentProjectionService` at module or component level, because it is already **provided in root**. You can inject and start using it immediately in your components, directives, or services. + +```js +import { ContentProjectionService } from '@abp/ng.core'; + +@Component({ + /* class metadata here */ +}) +class DemoComponent { + constructor(private contentProjectionService: ContentProjectionService) {} +} +``` + +## Usage + +You can use the `projectContent` method of `ContentProjectionService` to render components and templates dynamically in your project. + +### How to Project Components to Root Level + +If you pass a `RootComponentProjectionStrategy` as the first parameter of `projectContent` method, the `ContentProjectionService` will resolve the projected component and place it at the root level. If provided, it will also pass the component a context. + +```js +const strategy = PROJECTION_STRATEGY.AppendComponentToBody( + SomeOverlayComponent, + { someOverlayProp: "SOME_VALUE" } +); + +const componentRef = this.ContentProjectionService.projectContent(strategy); +``` + +In the example above, `SomeOverlayComponent` component will placed at the **end** of `` and a `ComponentRef` will be returned. Additionally, the given context will be applied, so `someOverlayProp` of the component will be set to `SOME_VALUE`. + +> You should keep the returned `ComponentRef` instance, as it is a reference to the projected component and you will need that reference to destroy the projected view and the component instance. + +### How to Project Components and Templates into a Container + +If you pass a `ComponentProjectionStrategy` or `TemplateProjectionStrategy` as the first parameter of `projectContent` method, and a `ViewContainerRef` as the second parameter of that strategy, the `ContentProjectionService` will project the component or template to the given container. If provided, it will also pass the component or the template a context. + +```js +const strategy = PROJECTION_STRATEGY.ProjectComponentToContainer( + SomeComponent, + viewContainerRefOfTarget, + { someProp: "SOME_VALUE" } +); + +const componentRef = this.ContentProjectionService.projectContent(strategy); +``` + +In this example, the `viewContainerRefOfTarget`, which is a `ViewContainerRef` instance, will be cleared and `SomeComponent` component will placed inside it. Moreover, the given context will be applied, so `someProp` of the component will be set to `SOME_VALUE`. + +> You should keep the returned `ComponentRef` or `EmbeddedViewRef`, as they are a reference to the projected content and you will need them to destroy it when necessary. + +Please refer to [ProjectionStrategy](./Projection-Strategy.md) to see all available projection strategies and how you can build your own projection strategy. + +## API + +### projectContent + +```js +projectContent | TemplateRef>( + projectionStrategy: ProjectionStrategy, + injector = this.injector, +): ComponentRef | EmbeddedViewRef +``` + +- `projectionStrategy` parameter is the primary focus here and is explained above. +- `injector` parameter is the `Injector` instance you can pass to the projected content. It is not used in `TemplateProjectionStrategy`. + + +## What's Next? + +- [TrackByService](./Track-By-Service.md) diff --git a/docs/en/UI/Angular/Dom-Insertion-Service.md b/docs/en/UI/Angular/Dom-Insertion-Service.md index 5159ae2c35..d5ea9fe3a2 100644 --- a/docs/en/UI/Angular/Dom-Insertion-Service.md +++ b/docs/en/UI/Angular/Dom-Insertion-Service.md @@ -1,4 +1,4 @@ -# How to Insert Scripts and Styles +# Dom Insertion (of Scripts and Styles) You can use the `DomInsertionService` in @abp/ng.core package in order to insert scripts and styles in an easy and explicit way. @@ -71,45 +71,17 @@ In the example above, `` element will place at t Please refer to [ContentStrategy](./Content-Strategy.md) to see all available content strategies and how you can build your own content strategy. -### How to Project Components & Templates - -If you pass a `ProjectionStrategy` as the first parameter of `projectContent` method, the `DomInsertionService` will resolve the projected component or template and place it at the designated target, such as containers or document body. If provided, it will also pass the component or the template a context. - -```js -const componentRef = this.domInsertionService.projectContent( - PROJECTION_STRATEGY.AppendComponentToBody(SomeOverlayComponent) -); -``` - -In the example above, `SomeOverlayComponent` component will placed at the **end** of `` and a `ComponentRef` will be returned. - -> You should keep the returned `ComponentRef` instance, as it is a reference to the projected component and you will need that reference to destroy the projected view and the component instance. - -```js -const componentRef = this.domInsertionService.projectContent( - PROJECTION_STRATEGY.ProjectComponentToContainer( - SomeOverlayComponent, - viewContainerRefOfTarget, - { someProp: "SOME_VALUE" } - ) -); -``` - -In this example, the `viewContainerRefOfTarget`, which is a `ViewContainerRef` instance, will be cleared and `SomeOverlayComponent` component will placed inside it. Moreover, the given context will be applied, so `someProp` of the component will be set to `SOME_VALUE`. - -Please refer to [ProjectionStrategy](./Projection-Strategy.md) to see all available projection strategies and how you can build your own projection strategy. - ## API ### insertContent ```js -injectContent(injector: Injector): ComponentRef | EmbeddedViewRef +insertContent(contentStrategy: ContentStrategy): void ``` -`injector` parameter is the `Injector` instance you can pass to the projected content. It is not used in `TemplateProjectionStrategy`. +- `contentStrategy` parameter is the primary focus here and is explained above. ## What's Next? -- [TrackByService](./Track-By-Service.md) +- [ContentProjectionService](./Content-Projection-Service.md) diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json index 471e215e3a..20214b8447 100644 --- a/docs/en/docs-nav.json +++ b/docs/en/docs-nav.json @@ -353,6 +353,10 @@ "text": "DomInsertionService", "path": "UI/Angular/Dom-Insertion-Service.md" }, + { + "text": "ContentProjectionService", + "path": "UI/Angular/Content-Projection-Service.md" + }, { "text": "TrackByService", "path": "UI/Angular/Track-By-Service.md" diff --git a/npm/ng-packs/packages/core/src/lib/services/content-projection.service.ts b/npm/ng-packs/packages/core/src/lib/services/content-projection.service.ts new file mode 100644 index 0000000000..dfcdea330a --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/services/content-projection.service.ts @@ -0,0 +1,14 @@ +import { Injectable, Injector, TemplateRef, Type } from '@angular/core'; +import { ProjectionStrategy } from '../strategies/projection.strategy'; + +@Injectable({ providedIn: 'root' }) +export class ContentProjectionService { + constructor(private injector: Injector) {} + + projectContent | TemplateRef>( + projectionStrategy: ProjectionStrategy, + injector = this.injector, + ) { + return projectionStrategy.injectContent(injector); + } +} diff --git a/npm/ng-packs/packages/core/src/lib/services/dom-insertion.service.ts b/npm/ng-packs/packages/core/src/lib/services/dom-insertion.service.ts index 5e53a64995..c10967ebbb 100644 --- a/npm/ng-packs/packages/core/src/lib/services/dom-insertion.service.ts +++ b/npm/ng-packs/packages/core/src/lib/services/dom-insertion.service.ts @@ -1,6 +1,5 @@ -import { Injectable, Injector, TemplateRef, Type } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { ContentStrategy } from '../strategies/content.strategy'; -import { ProjectionStrategy } from '../strategies/projection.strategy'; import { generateHash } from '../utils'; @Injectable({ providedIn: 'root' }) @@ -17,11 +16,4 @@ export class DomInsertionService { contentStrategy.insertElement(); this.inserted.add(hash); } - - projectContent | TemplateRef>( - projectionStrategy: ProjectionStrategy, - injector = this.injector, - ) { - return projectionStrategy.injectContent(injector); - } } diff --git a/npm/ng-packs/packages/core/src/lib/services/index.ts b/npm/ng-packs/packages/core/src/lib/services/index.ts index a64e721c67..f8b016bbd1 100644 --- a/npm/ng-packs/packages/core/src/lib/services/index.ts +++ b/npm/ng-packs/packages/core/src/lib/services/index.ts @@ -1,6 +1,7 @@ export * from './application-configuration.service'; export * from './auth.service'; export * from './config-state.service'; +export * from './content-projection.service'; export * from './dom-insertion.service'; export * from './lazy-load.service'; export * from './localization.service'; diff --git a/npm/ng-packs/packages/core/src/lib/tests/content-projection.service.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/content-projection.service.spec.ts new file mode 100644 index 0000000000..30f9f92e73 --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/tests/content-projection.service.spec.ts @@ -0,0 +1,38 @@ +import { Component, ComponentRef, NgModule } from '@angular/core'; +import { createServiceFactory, SpectatorService } from '@ngneat/spectator'; +import { ContentProjectionService } from '../services'; +import { PROJECTION_STRATEGY } from '../strategies'; + +describe('ContentProjectionService', () => { + @Component({ template: '
bar
' }) + class TestComponent {} + + // createServiceFactory does not accept entryComponents directly + @NgModule({ + declarations: [TestComponent], + entryComponents: [TestComponent], + }) + class TestModule {} + + let componentRef: ComponentRef; + let spectator: SpectatorService; + const createService = createServiceFactory({ + service: ContentProjectionService, + imports: [TestModule], + }); + + beforeEach(() => (spectator = createService())); + + afterEach(() => componentRef.destroy()); + + describe('#projectContent', () => { + it('should call injectContent of given projectionStrategy and return what it returns', () => { + const strategy = PROJECTION_STRATEGY.AppendComponentToBody(TestComponent); + componentRef = spectator.service.projectContent(strategy); + const foo = document.querySelector('body > ng-component > div.foo'); + + expect(componentRef).toBeInstanceOf(ComponentRef); + expect(foo.textContent).toBe('bar'); + }); + }); +}); diff --git a/npm/ng-packs/packages/core/src/lib/tests/dom-insertion.service.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/dom-insertion.service.spec.ts index 8ae75c2f15..f8e8565496 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/dom-insertion.service.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/dom-insertion.service.spec.ts @@ -1,25 +1,11 @@ -import { Component, ComponentRef, NgModule } from '@angular/core'; import { createServiceFactory, SpectatorService } from '@ngneat/spectator'; import { DomInsertionService } from '../services'; -import { CONTENT_STRATEGY, PROJECTION_STRATEGY } from '../strategies'; +import { CONTENT_STRATEGY } from '../strategies'; describe('DomInsertionService', () => { - @Component({ template: '
bar
' }) - class TestComponent {} - - // createServiceFactory does not accept entryComponents directly - @NgModule({ - declarations: [TestComponent], - entryComponents: [TestComponent], - }) - class TestModule {} - - let spectator: SpectatorService; - const createService = createServiceFactory({ - service: DomInsertionService, - imports: [TestModule], - }); let styleElements: NodeListOf; + let spectator: SpectatorService; + const createService = createServiceFactory(DomInsertionService); beforeEach(() => (spectator = createService())); @@ -56,16 +42,4 @@ describe('DomInsertionService', () => { expect(spectator.service.inserted.has(1437348290)).toBe(true); }); }); - - describe('#projectContent', () => { - it('should call injectContent of given projectionStrategy and return what it returns', () => { - const strategy = PROJECTION_STRATEGY.AppendComponentToBody(TestComponent); - const componentRef = spectator.service.projectContent(strategy); - const foo = document.querySelector('body > ng-component > div.foo'); - - expect(componentRef).toBeInstanceOf(ComponentRef); - expect(foo.textContent).toBe('bar'); - componentRef.destroy(); - }); - }); }); From 83d9ad3f08678dfeb3db9b3969ff921a52ec9cb4 Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Fri, 10 Apr 2020 10:04:04 +0300 Subject: [PATCH 16/17] refactor: remove unnecessary initializer --- .../packages/core/src/lib/tests/projection.strategy.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/npm/ng-packs/packages/core/src/lib/tests/projection.strategy.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/projection.strategy.spec.ts index 1166d44f57..a0b3bb61bb 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/projection.strategy.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/projection.strategy.spec.ts @@ -213,7 +213,7 @@ describe('TemplateProjectionStrategy', () => { describe('PROJECTION_STRATEGY', () => { const content = undefined; const containerRef = ({ length: 0 } as any) as ViewContainerRef; - let context = undefined; + let context: any; test.each` name | Strategy | containerStrategy From 6ae41180056d71cf4d12d4a9b2d98adefa7d46a2 Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Fri, 10 Apr 2020 10:25:38 +0300 Subject: [PATCH 17/17] refactor: remove unused injector --- .../packages/core/src/lib/services/dom-insertion.service.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/npm/ng-packs/packages/core/src/lib/services/dom-insertion.service.ts b/npm/ng-packs/packages/core/src/lib/services/dom-insertion.service.ts index c10967ebbb..d4b30b731d 100644 --- a/npm/ng-packs/packages/core/src/lib/services/dom-insertion.service.ts +++ b/npm/ng-packs/packages/core/src/lib/services/dom-insertion.service.ts @@ -1,4 +1,4 @@ -import { Injectable, Injector } from '@angular/core'; +import { Injectable } from '@angular/core'; import { ContentStrategy } from '../strategies/content.strategy'; import { generateHash } from '../utils'; @@ -6,8 +6,6 @@ import { generateHash } from '../utils'; export class DomInsertionService { readonly inserted = new Set(); - constructor(private injector: Injector) {} - insertContent(contentStrategy: ContentStrategy) { const hash = generateHash(contentStrategy.content);