-
-
Notifications
You must be signed in to change notification settings - Fork 9.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: rewrite rendering feature of angular client
TODO: add description
- Loading branch information
Showing
9 changed files
with
408 additions
and
344 deletions.
There are no files selected for viewing
115 changes: 115 additions & 0 deletions
115
app/angular/src/client/preview/angular/ComponentClassFromStoryComponent.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
import { | ||
AfterViewInit, | ||
ChangeDetectorRef, | ||
Component, | ||
ElementRef, | ||
Inject, | ||
OnDestroy, | ||
Type, | ||
ViewChild, | ||
ViewContainerRef, | ||
} from '@angular/core'; | ||
import { Subscription, Subject } from 'rxjs'; | ||
import { map, skip } from 'rxjs/operators'; | ||
|
||
import { ICollection } from '../types'; | ||
import { STORY_PROPS } from './app.token'; | ||
import { RenderNgAppService } from './RenderNgAppService'; | ||
|
||
const findComponentDecoratorMetadata = (component: any) => { | ||
const decoratorKey = '__annotations__'; | ||
const decorators: any[] = | ||
Reflect && | ||
Reflect.getOwnPropertyDescriptor && | ||
Reflect.getOwnPropertyDescriptor(component, decoratorKey) | ||
? Reflect.getOwnPropertyDescriptor(component, decoratorKey).value | ||
: (component as any)[decoratorKey]; | ||
|
||
const ngComponentDecorator: Component | undefined = decorators.find( | ||
(decorator) => decorator instanceof Component | ||
); | ||
|
||
return ngComponentDecorator; | ||
}; | ||
|
||
const toInputsOutputs = (props: ICollection = {}) => { | ||
return Object.entries(props).reduce( | ||
(previousValue, [key, value]) => { | ||
if (typeof value === 'function') { | ||
return { ...previousValue, outputs: { ...previousValue.outputs, [key]: value } }; | ||
} | ||
|
||
return { ...previousValue, inputs: { ...previousValue.inputs, [key]: value } }; | ||
}, | ||
{ inputs: {}, outputs: {} } as { inputs: Record<string, any>; outputs: Record<string, any> } | ||
); | ||
}; | ||
|
||
/** | ||
* Wraps the story component into a component | ||
* | ||
* @param component | ||
* @param initialProps | ||
*/ | ||
export const createComponentClassFromStoryComponent = ( | ||
component: any, | ||
initialProps?: ICollection | ||
): Type<any> => { | ||
const ngComponentMetadata = findComponentDecoratorMetadata(component); | ||
|
||
const { inputs: initialInputs, outputs: initialOutputs } = toInputsOutputs(initialProps); | ||
|
||
const templateInputs = Object.keys(initialInputs) | ||
.map((i) => `[${i}]="${i}"`) | ||
.join(' '); | ||
const templateOutputs = Object.keys(initialOutputs) | ||
.map((i) => `(${i})="${i}($event)"`) | ||
.join(' '); | ||
|
||
@Component({ | ||
selector: RenderNgAppService.SELECTOR_STORYBOOK_WRAPPER, | ||
// Simulates the `component` integration in a template | ||
// `props` are converted into Inputs/Outputs to be added directly in the template so as the component can use them during its initailization | ||
// - The outputs are connected only once here | ||
// - Only inputs present in initial `props` value are added. They will be overwritten and completed as necessary after the component is initialized | ||
template: `<${ngComponentMetadata.selector} ${templateInputs} ${templateOutputs} #storyComponentRef></${ngComponentMetadata.selector}>`, | ||
}) | ||
class StoryBookComponentWrapperComponent implements AfterViewInit, OnDestroy { | ||
private storyPropsSubscription: Subscription; | ||
|
||
@ViewChild('storyComponentRef', { static: true }) storyComponentElementRef: ElementRef; | ||
|
||
@ViewChild('storyComponentRef', { read: ViewContainerRef, static: true }) | ||
storyComponentViewContainerRef: ViewContainerRef; | ||
|
||
constructor( | ||
@Inject(STORY_PROPS) private storyProps$: Subject<ICollection | undefined>, | ||
private changeDetectorRef: ChangeDetectorRef | ||
) { | ||
// Initializes template Inputs/Outputs values | ||
Object.assign(this, initialProps); | ||
} | ||
|
||
ngAfterViewInit(): void { | ||
// Once target component has been initialized, the storyProps$ observable keeps target component inputs up to date | ||
this.storyPropsSubscription = this.storyProps$ | ||
.pipe(skip(1), map(toInputsOutputs)) | ||
.subscribe(({ inputs }) => { | ||
// Replace inputs with new ones from props | ||
Object.assign(this.storyComponentElementRef, inputs); | ||
|
||
// `markForCheck` the component in case this uses changeDetection: OnPush | ||
// And then forces the `detectChanges` | ||
this.storyComponentViewContainerRef.injector.get(ChangeDetectorRef).markForCheck(); | ||
this.changeDetectorRef.detectChanges(); | ||
}); | ||
} | ||
|
||
ngOnDestroy(): void { | ||
if (this.storyPropsSubscription != null) { | ||
this.storyPropsSubscription.unsubscribe(); | ||
} | ||
} | ||
} | ||
return StoryBookComponentWrapperComponent; | ||
}; |
52 changes: 52 additions & 0 deletions
52
app/angular/src/client/preview/angular/ComponentClassFromStoryTemplate.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import { Inject, ChangeDetectorRef, Component, OnDestroy, OnInit, Type } from '@angular/core'; | ||
import { Subject, Subscription } from 'rxjs'; | ||
|
||
import { ICollection } from '../types'; | ||
import { STORY_PROPS } from './app.token'; | ||
import { RenderNgAppService } from './RenderNgAppService'; | ||
|
||
/** | ||
* Wraps the story template into a component | ||
* | ||
* @param template {string} | ||
* @param styles {string[]} | ||
*/ | ||
export const createComponentClassFromStoryTemplate = ( | ||
template: string, | ||
styles: string[] | ||
): Type<any> => { | ||
@Component({ | ||
selector: RenderNgAppService.SELECTOR_STORYBOOK_WRAPPER, | ||
template, | ||
styles, | ||
}) | ||
class StoryBookTemplateWrapperComponent implements OnInit, OnDestroy { | ||
private storyPropsSubscription: Subscription; | ||
|
||
// eslint-disable-next-line no-useless-constructor | ||
constructor( | ||
@Inject(STORY_PROPS) private storyProps$: Subject<ICollection | undefined>, | ||
private changeDetectorRef: ChangeDetectorRef | ||
) {} | ||
|
||
ngOnInit(): void { | ||
// Subscribes to the observable storyProps$ to keep these properties up to date | ||
this.storyPropsSubscription = this.storyProps$.subscribe((storyProps) => { | ||
if (!storyProps) { | ||
// All props are added as component properties | ||
Object.assign(this, storyProps); | ||
} | ||
|
||
this.changeDetectorRef.detectChanges(); | ||
this.changeDetectorRef.markForCheck(); | ||
}); | ||
} | ||
|
||
ngOnDestroy(): void { | ||
if (this.storyPropsSubscription != null) { | ||
this.storyPropsSubscription.unsubscribe(); | ||
} | ||
} | ||
} | ||
return StoryBookTemplateWrapperComponent; | ||
}; |
26 changes: 26 additions & 0 deletions
26
app/angular/src/client/preview/angular/NgModulesAnalyzer.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { Component, NgModule } from '@angular/core'; | ||
import { getExistenceOfComponentInModules } from './NgModulesAnalyzer'; | ||
|
||
const FooComponent = Component({})(class {}); | ||
|
||
const BarComponent = Component({})(class {}); | ||
|
||
const BetaModule = NgModule({ declarations: [FooComponent] })(class {}); | ||
|
||
const AlphaModule = NgModule({ imports: [BetaModule] })(class {}); | ||
|
||
describe('getExistenceOfComponentInModules', () => { | ||
it('should return true when the component is already declared in one of modules', () => { | ||
expect(getExistenceOfComponentInModules(FooComponent, [], [AlphaModule])).toEqual(true); | ||
}); | ||
|
||
it('should return true if the component is in moduleDeclarations', () => { | ||
expect(getExistenceOfComponentInModules(BarComponent, [BarComponent], [AlphaModule])).toEqual( | ||
true | ||
); | ||
}); | ||
|
||
it('should return false if the component is not declared', () => { | ||
expect(getExistenceOfComponentInModules(BarComponent, [], [AlphaModule])).toEqual(false); | ||
}); | ||
}); |
59 changes: 59 additions & 0 deletions
59
app/angular/src/client/preview/angular/NgModulesAnalyzer.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import { NgModule } from '@angular/core'; | ||
|
||
/** | ||
* Avoid component redeclaration | ||
* | ||
* Checks recursively if the component has already been declared in all import Module | ||
*/ | ||
export const getExistenceOfComponentInModules = ( | ||
componentToFind: any, | ||
moduleDeclarations: any[], | ||
moduleImports: any[] | ||
): boolean => { | ||
if ( | ||
moduleDeclarations && | ||
moduleDeclarations.some((declaration) => declaration === componentToFind) | ||
) { | ||
// Found component in declarations array | ||
return true; | ||
} | ||
if (!moduleImports) { | ||
return false; | ||
} | ||
|
||
return moduleImports.some((importItem) => { | ||
const extractedNgModuleMetadata = extractNgModuleMetadata(importItem); | ||
if (!extractedNgModuleMetadata) { | ||
// Not an NgModule | ||
return false; | ||
} | ||
return getExistenceOfComponentInModules( | ||
componentToFind, | ||
extractedNgModuleMetadata.declarations, | ||
extractedNgModuleMetadata.imports | ||
); | ||
}); | ||
}; | ||
|
||
const extractNgModuleMetadata = (importItem: any): NgModule => { | ||
const target = importItem && importItem.ngModule ? importItem.ngModule : importItem; | ||
const decoratorKey = '__annotations__'; | ||
const decorators: any[] = | ||
Reflect && | ||
Reflect.getOwnPropertyDescriptor && | ||
Reflect.getOwnPropertyDescriptor(target, decoratorKey) | ||
? Reflect.getOwnPropertyDescriptor(target, decoratorKey).value | ||
: target[decoratorKey]; | ||
|
||
if (!decorators || decorators.length === 0) { | ||
return null; | ||
} | ||
|
||
const ngModuleDecorator: NgModule | undefined = decorators.find( | ||
(decorator) => decorator instanceof NgModule | ||
); | ||
if (!ngModuleDecorator) { | ||
return null; | ||
} | ||
return ngModuleDecorator; | ||
}; |
Oops, something went wrong.