Browse Source

feat: add projection strategies

pull/3544/head
Arman Ozak 6 years ago
parent
commit
cad3e623d0
  1. 1
      npm/ng-packs/packages/core/src/lib/strategies/index.ts
  2. 181
      npm/ng-packs/packages/core/src/lib/strategies/projection.strategy.ts
  3. 245
      npm/ng-packs/packages/core/src/lib/tests/projection.strategy.spec.ts

1
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';

181
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<T = any> {
constructor(public content: T) {}
abstract injectContent(injector: Injector): ComponentRefOrEmbeddedViewRef<T>;
}
export class ComponentProjectionStrategy<T extends Type<any>> extends ProjectionStrategy<T> {
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<InferedInstanceOf<T>>(this.content);
const componentRef = this.containerStrategy.containerRef.createComponent(
factory,
this.containerStrategy.getIndex(),
injector,
);
this.contextStrategy.setContext(componentRef);
return componentRef as ComponentRefOrEmbeddedViewRef<T>;
}
}
export class RootComponentProjectionStrategy<T extends Type<any>> extends ProjectionStrategy<T> {
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<InferedInstanceOf<T>>(this.content)
.create(injector);
this.contextStrategy.setContext(componentRef);
appRef.attachView(componentRef.hostView);
const element: HTMLElement = (componentRef.hostView as EmbeddedViewRef<any>).rootNodes[0];
this.domStrategy.insertElement(element);
return componentRef as ComponentRefOrEmbeddedViewRef<T>;
}
}
export class TemplateProjectionStrategy<T extends TemplateRef<any>> extends ProjectionStrategy<T> {
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<T>;
}
}
export const PROJECTION_STRATEGY = {
AppendComponentToBody<T extends Type<unknown>>(
component: T,
contextStrategy?: ComponentContextStrategy<T>,
) {
return new RootComponentProjectionStrategy<T>(component, contextStrategy);
},
AppendComponentToContainer<T extends Type<unknown>>(
component: T,
containerRef: ViewContainerRef,
contextStrategy?: ComponentContextStrategy<T>,
) {
return new ComponentProjectionStrategy<T>(
component,
CONTAINER_STRATEGY.Append(containerRef),
contextStrategy,
);
},
AppendTemplateToContainer<T extends TemplateRef<unknown>>(
template: T,
containerRef: ViewContainerRef,
contextStrategy?: TemplateContextStrategy<T>,
) {
return new TemplateProjectionStrategy<T>(
template,
CONTAINER_STRATEGY.Append(containerRef),
contextStrategy,
);
},
PrependComponentToContainer<T extends Type<unknown>>(
component: T,
containerRef: ViewContainerRef,
contextStrategy?: ComponentContextStrategy<T>,
) {
return new ComponentProjectionStrategy<T>(
component,
CONTAINER_STRATEGY.Prepend(containerRef),
contextStrategy,
);
},
PrependTemplateToContainer<T extends TemplateRef<unknown>>(
template: T,
containerRef: ViewContainerRef,
contextStrategy?: TemplateContextStrategy<T>,
) {
return new TemplateProjectionStrategy<T>(
template,
CONTAINER_STRATEGY.Prepend(containerRef),
contextStrategy,
);
},
ProjectComponentToContainer<T extends Type<unknown>>(
component: T,
containerRef: ViewContainerRef,
contextStrategy?: ComponentContextStrategy<T>,
) {
return new ComponentProjectionStrategy<T>(
component,
CONTAINER_STRATEGY.Clear(containerRef),
contextStrategy,
);
},
ProjectTemplateToContainer<T extends TemplateRef<unknown>>(
template: T,
containerRef: ViewContainerRef,
contextStrategy?: TemplateContextStrategy<T>,
) {
return new TemplateProjectionStrategy<T>(
template,
CONTAINER_STRATEGY.Clear(containerRef),
contextStrategy,
);
},
};
type ComponentRefOrEmbeddedViewRef<T> = T extends Type<infer U>
? ComponentRef<U>
: T extends TemplateRef<infer C>
? EmbeddedViewRef<C>
: never;

245
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: '<div class="foo">{{ bar || baz }}</div>',
})
class TestComponent {
bar: string;
baz = 'baz';
}
@Component({
template: '<ng-container #container></ng-container>',
})
class HostComponent {
@ViewChild('container', { static: true, read: ViewContainerRef })
containerRef: ViewContainerRef;
}
let containerStrategy: ContainerStrategy;
let spectator: Spectator<HostComponent>;
let componentRef: ComponentRef<TestComponent>;
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: '<div class="foo">{{ bar || baz }}</div>',
})
class TestComponent {
bar: string;
baz = 'baz';
}
@Component({ template: '' })
class HostComponent {}
let spectator: Spectator<HostComponent>;
let componentRef: ComponentRef<TestComponent>;
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: `
<ng-template #template let-bar>
<div class="foo">{{ bar || baz }}</div>
</ng-template>
<ng-container #container></ng-container>
`,
})
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<HostComponent>;
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<typeof templateRef>({ $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()),
);
},
);
});
Loading…
Cancel
Save