From f786955442d830f0296784c9b9d40703c29818da Mon Sep 17 00:00:00 2001 From: Roman Hotsiy Date: Sun, 30 Oct 2016 17:57:26 +0200 Subject: [PATCH] Dynamic component instantiator from html --- lib/services/component-parser.service.ts | 88 +++++++++++++++++++ lib/services/content-projector.service.ts | 39 ++++++++ .../dynamic-ng2-viewer.component.ts | 47 ++++++++++ lib/shared/components/index.ts | 5 +- 4 files changed, 177 insertions(+), 2 deletions(-) create mode 100644 lib/services/component-parser.service.ts create mode 100644 lib/services/content-projector.service.ts create mode 100644 lib/shared/components/DynamicNg2Viewer/dynamic-ng2-viewer.component.ts diff --git a/lib/services/component-parser.service.ts b/lib/services/component-parser.service.ts new file mode 100644 index 00000000..2a3d1f82 --- /dev/null +++ b/lib/services/component-parser.service.ts @@ -0,0 +1,88 @@ +'use strict'; + +import { + Injectable, + Renderer, + ComponentRef, + Type, + Injector, + Inject, + ComponentFactoryResolver +} from '@angular/core'; + +type NodesOrComponents = HTMLElement | ComponentRef; +export const COMPONENT_PARSER_ALLOWED = 'COMPONENT_PARSER_ALLOWED'; + +const COMPONENT_REGEXP = '^\\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 ``; + } + + 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| 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 | 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 + }; + } +} diff --git a/lib/services/content-projector.service.ts b/lib/services/content-projector.service.ts new file mode 100644 index 00000000..a5d81653 --- /dev/null +++ b/lib/services/content-projector.service.ts @@ -0,0 +1,39 @@ +'use strict'; + +import { + Injectable, + ComponentFactory, + ComponentRef, + ViewContainerRef +} from '@angular/core'; + +@Injectable() +export class ContentProjector { + instantiateAndProject(componentFactory: ComponentFactory, + parentView:ViewContainerRef, projectedNodesOrComponents: any[]):ComponentRef { + let contextInjector = parentView.parentInjector; + + let projectedNodes = []; + let componentRefs:ComponentRef[] = []; + + 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 = (parentView)._element; + appElement.nestedViews = appElement.nestedViews || []; + for (let i=0; i < componentRefs.length; i++) { + let compRef = componentRefs[i]; + appElement.nestedViews.push((compRef.hostView).internalView); + (compRef.hostView).internalView.addToContentChildren(appElement); + } + return parentCompRef; + } +} diff --git a/lib/shared/components/DynamicNg2Viewer/dynamic-ng2-viewer.component.ts b/lib/shared/components/DynamicNg2Viewer/dynamic-ng2-viewer.component.ts new file mode 100644 index 00000000..56dbf968 --- /dev/null +++ b/lib/shared/components/DynamicNg2Viewer/dynamic-ng2-viewer.component.ts @@ -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: '' +}) +export class DynamicNg2Wrapper {} diff --git a/lib/shared/components/index.ts b/lib/shared/components/index.ts index 195e796a..b273f1a5 100644 --- a/lib/shared/components/index.ts +++ b/lib/shared/components/index.ts @@ -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 }