Dynamic component instantiator from html

This commit is contained in:
Roman Hotsiy 2016-10-30 17:57:26 +02:00
parent c39166266c
commit f786955442
No known key found for this signature in database
GPG Key ID: 5CB7B3ACABA57CB0
4 changed files with 177 additions and 2 deletions

View File

@ -0,0 +1,88 @@
'use strict';
import {
Injectable,
Renderer,
ComponentRef,
Type,
Injector,
Inject,
ComponentFactoryResolver
} from '@angular/core';
type NodesOrComponents = HTMLElement | ComponentRef<any>;
export const COMPONENT_PARSER_ALLOWED = 'COMPONENT_PARSER_ALLOWED';
const COMPONENT_REGEXP = '^\\s*<!-- ReDoc-Inject:\\s+?{component}\\s+?-->\\s*$';
@Injectable()
export class ComponentParser {
private renderer: Renderer;
private allowedComponents: any;
static contains(content: string, componentSelector: string) {
let regexp = new RegExp(COMPONENT_REGEXP.replace('{component}', `<${componentSelector}.*>`), 'mi');
return regexp.test(content);
}
static build(componentSelector) {
return `<!-- ReDoc-Inject: <${componentSelector}> -->`;
}
constructor(
private resolver: ComponentFactoryResolver,
@Inject(COMPONENT_PARSER_ALLOWED) allowedComponents
) {
this.allowedComponents = allowedComponents;
}
setRenderer(_renderer: Renderer) {
this.renderer = _renderer;
}
splitIntoNodesOrComponents(content: string, injector: Injector):NodesOrComponents[] {
let componentDefs = [];
let match;
let anyCompRegexp = new RegExp(COMPONENT_REGEXP.replace('{component}', '(.*?)'), 'gmi');
while (match = anyCompRegexp.exec(content)) {
componentDefs.push(match[1]);
}
let splitCompRegexp = new RegExp(COMPONENT_REGEXP.replace('{component}', '.*?'), 'mi');
let htmlParts = content.split(splitCompRegexp);
let res = [];
for (let i = 0; i < htmlParts.length; i++) {
let node = this.renderer.createElement(null, 'div');
this.renderer.setElementProperty(node, 'innerHTML', htmlParts[i]);
if (htmlParts[i]) res.push(node);
if (componentDefs[i]) {
let componentRef = this.createComponentByHtml(componentDefs[i], injector);
res.push(componentRef);
}
}
return res;
}
createComponentByHtml(htmlTag: string, injector:Injector):ComponentRef<any>| null {
let {componentType, options} = this._parseHtml(htmlTag);
if (!componentType) return null;
let factory = this.resolver.resolveComponentFactory(componentType);
return factory.create(injector);
}
private _parseHtml(htmlTag: string):{componentType: Type<any> | null, options: any} {
// TODO: for now only primitive parsing by tagname
let match = /<([\w_-]+).*?>/.exec(htmlTag);
if (match.length <= 1) return { componentType: null, options: null };
let componentName = match[1];
let componentType = this.allowedComponents[componentName];
// TODO parse options
let options = {};
return {
componentType,
options
};
}
}

View File

@ -0,0 +1,39 @@
'use strict';
import {
Injectable,
ComponentFactory,
ComponentRef,
ViewContainerRef
} from '@angular/core';
@Injectable()
export class ContentProjector {
instantiateAndProject<T>(componentFactory: ComponentFactory<T>,
parentView:ViewContainerRef, projectedNodesOrComponents: any[]):ComponentRef<T> {
let contextInjector = parentView.parentInjector;
let projectedNodes = [];
let componentRefs:ComponentRef<any>[] = [];
for (let i=0; i < projectedNodesOrComponents.length; i++) {
let nodeOrCompRef = projectedNodesOrComponents[i];
if (nodeOrCompRef instanceof ComponentRef) {
projectedNodes.push(nodeOrCompRef.location.nativeElement);
componentRefs.push(nodeOrCompRef);
} else {
projectedNodes.push(nodeOrCompRef);
}
}
let parentCompRef = parentView.createComponent(componentFactory, null, contextInjector, [projectedNodes]);
let appElement = (<any>parentView)._element;
appElement.nestedViews = appElement.nestedViews || [];
for (let i=0; i < componentRefs.length; i++) {
let compRef = componentRefs[i];
appElement.nestedViews.push((<any>compRef.hostView).internalView);
(<any>compRef.hostView).internalView.addToContentChildren(appElement);
}
return parentCompRef;
}
}

View File

@ -0,0 +1,47 @@
'use strict';
import {
Component,
EventEmitter,
Output,
Input,
OnInit,
ViewContainerRef,
ComponentFactoryResolver,
Renderer
} from '@angular/core';
import {
ComponentParser,
ContentProjector
} from '../../../services/';
@Component({
selector: 'dynamic-ng2-viewer',
template: ''
})
export class DynamicNg2Viewer implements OnInit {
@Input() html: string;
constructor(
private view: ViewContainerRef,
private projector: ContentProjector,
private parser: ComponentParser,
private resolver: ComponentFactoryResolver,
private renderer: Renderer) {
}
ngOnInit() {
this.parser.setRenderer(this.renderer);
let nodesOrComponents = this.parser.splitIntoNodesOrComponents(this.html, this.view.injector);
let wrapperFactory = this.resolver.resolveComponentFactory(DynamicNg2Wrapper);
let ref = this.projector.instantiateAndProject(wrapperFactory, this.view, nodesOrComponents);
ref.changeDetectorRef.markForCheck();
}
}
@Component({
selector: 'dynamic-ng2-wrapper',
template: '<ng-content></ng-content>'
})
export class DynamicNg2Wrapper {}

View File

@ -5,9 +5,10 @@ import { Tabs, Tab } from './Tabs/tabs';
import { Zippy } from './Zippy/zippy';
import { CopyButton } from './CopyButton/copy-button.directive';
import { SelectOnClick } from './SelectOnClick/select-on-click.directive';
import { DynamicNg2Viewer, DynamicNg2Wrapper } from './DynamicNg2Viewer/dynamic-ng2-viewer.component';
export const REDOC_COMMON_DIRECTIVES = [
DropDown, StickySidebar, Tabs, Tab, Zippy, CopyButton, SelectOnClick
DropDown, StickySidebar, Tabs, Tab, Zippy, CopyButton, SelectOnClick, DynamicNg2Viewer, DynamicNg2Wrapper
];
export { DropDown, StickySidebar, Tabs, Tab, Zippy, CopyButton, SelectOnClick }
export { DropDown, StickySidebar, Tabs, Tab, Zippy, CopyButton, SelectOnClick, DynamicNg2Viewer, DynamicNg2Wrapper }