• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

worktile / ngx-planet / 744ebe0d-5c8a-4c3c-b34a-39a3f9aad090

11 Apr 2024 09:09AM UTC coverage: 92.775% (-0.4%) from 93.157%
744ebe0d-5c8a-4c3c-b34a-39a3f9aad090

Pull #326

circleci

lswl66
fix: fix review & add test
Pull Request #326: feat: support render portal component #INFR-12018

260 of 294 branches covered (88.44%)

Branch coverage included in aggregate %.

12 of 14 new or added lines in 2 files covered. (85.71%)

1 existing line in 1 file now uncovered.

703 of 744 relevant lines covered (94.49%)

19.22 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.35
/packages/planet/src/component/planet-component-loader.ts
1
import { DOCUMENT } from '@angular/common';
2
import {
3
    ApplicationRef,
4
    ComponentRef,
5
    ElementRef,
6
    EmbeddedViewRef,
7
    EnvironmentInjector,
8
    Inject,
9
    Injectable,
10
    Injector,
11
    NgModuleRef,
12
    NgZone,
13
    Type,
14
    createComponent,
15
    reflectComponentType
16
} from '@angular/core';
17
import { PlanetPortalApplication } from 'ngx-planet';
18
import { Observable, of, timer } from 'rxjs';
19
import { delayWhen, map, shareReplay } from 'rxjs/operators';
20
import { NgPlanetApplicationRef } from '../application/ng-planet-application-ref';
21
import { PlanetApplicationRef } from '../application/planet-application-ref';
22
import {
23
    getApplicationLoader,
24
    getApplicationService,
25
    getBootstrappedPlanetApplicationRef,
26
    getPlanetApplicationRef,
27
    globalPlanet
28
} from '../global-planet';
29
import { coerceArray } from '../helpers';
30
import { PlanetComponentRef } from './planet-component-types';
31
import { PlantComponentConfig } from './plant-component.config';
32

33
const componentWrapperClass = 'planet-component-wrapper';
1✔
34

35
function isComponentType<T>(component: PlanetComponent<T>): component is Type<T> {
36
    return typeof component === 'function' && component.constructor === Function;
18✔
37
}
38

39
export type PlanetComponent<T = unknown> =
40
    | {
41
          name: string;
42
          component: Type<T>;
43
      }
44
    | Type<T>;
45

46
@Injectable({
47
    providedIn: 'root'
48
})
49
export class PlanetComponentLoader {
1✔
50
    private get applicationLoader() {
51
        return getApplicationLoader();
1✔
52
    }
53

54
    private get applicationService() {
55
        return getApplicationService();
9✔
56
    }
57

58
    constructor(
59
        private applicationRef: ApplicationRef,
17✔
60
        private ngModuleRef: NgModuleRef<any>,
17✔
61
        private ngZone: NgZone,
17✔
62
        @Inject(DOCUMENT) private document: any
17✔
63
    ) {}
64

65
    private getPlantAppRef(name: string): Observable<PlanetApplicationRef> {
66
        const plantAppRef = getBootstrappedPlanetApplicationRef(name);
19✔
67
        if (plantAppRef) {
19✔
68
            return of(plantAppRef);
18✔
69
        } else {
70
            const app = this.applicationService.getAppByName(name);
1✔
71
            return this.applicationLoader.preload(app, true).pipe(
1✔
72
                map(() => {
73
                    return getPlanetApplicationRef(name);
1✔
74
                })
75
            );
76
        }
77
    }
78

79
    private createInjector<TData>(parentInjector: Injector, componentRef: PlanetComponentRef<TData>): Injector {
80
        return Injector.create({
9✔
81
            providers: [
82
                {
83
                    provide: PlanetComponentRef,
84
                    useValue: componentRef
85
                }
86
            ],
87
            parent: parentInjector
88
        });
89
    }
90

91
    private getContainerElement(config: PlantComponentConfig): HTMLElement {
92
        if (!config.container) {
9✔
93
            throw new Error(`config 'container' cannot be null`);
1✔
94
        } else {
95
            if ((config.container as ElementRef).nativeElement) {
8!
96
                return (config.container as ElementRef).nativeElement;
×
97
            } else {
98
                return config.container as HTMLElement;
8✔
99
            }
100
        }
101
    }
102

103
    private insertComponentRootNodeToContainer(container: HTMLElement, componentRootNode: HTMLElement, hostClass: string) {
104
        const subApp = this.applicationService.getAppByName(this.ngModuleRef.instance.appName);
8✔
105
        componentRootNode.classList.add(componentWrapperClass);
8✔
106
        componentRootNode.setAttribute('planet-inline', '');
8✔
107
        if (hostClass) {
8✔
108
            componentRootNode.classList.add(hostClass);
3✔
109
        }
110
        if (subApp && subApp.stylePrefix) {
8✔
111
            componentRootNode.classList.add(subApp.stylePrefix);
1✔
112
        }
113
        // container 是注释则在前方插入,否则在元素内部插入
114
        if (container.nodeType === 8) {
8✔
115
            container.parentElement.insertBefore(componentRootNode, container);
1✔
116
        } else {
117
            container.appendChild(componentRootNode);
7✔
118
        }
119
    }
120

121
    private attachComponent<TData>(
122
        component: Type<unknown>,
123
        environmentInjector: EnvironmentInjector,
124
        config: PlantComponentConfig
125
    ): PlanetComponentRef<TData> {
126
        const plantComponentRef = new PlanetComponentRef();
9✔
127
        const appRef = this.applicationRef;
9✔
128
        const injector = this.createInjector<TData>(environmentInjector, plantComponentRef);
9✔
129
        const container = this.getContainerElement(config);
9✔
130
        const componentRef = createComponent(component, {
8✔
131
            environmentInjector: environmentInjector,
132
            elementInjector: injector
133
        });
134
        appRef.attachView(componentRef.hostView);
8✔
135
        const componentRootNode = this.getComponentRootNode(componentRef);
8✔
136
        this.insertComponentRootNodeToContainer(container, componentRootNode, config.hostClass || config.wrapperClass);
8✔
137
        if (config.initialState) {
8!
138
            Object.assign(componentRef.instance, config.initialState);
×
139
        }
140
        plantComponentRef.componentInstance = componentRef.instance;
8✔
141
        plantComponentRef.componentRef = componentRef;
8✔
142
        plantComponentRef.hostElement = componentRootNode;
8✔
143
        plantComponentRef.dispose = () => {
8✔
144
            if (appRef.viewCount > 0) {
1✔
145
                appRef.detachView(componentRef.hostView);
1✔
146
            }
147
            componentRef?.destroy();
1✔
148
            componentRootNode.remove();
1✔
149
        };
150
        return plantComponentRef;
8✔
151
    }
152

153
    /** Gets the root HTMLElement for an instantiated component. */
154
    private getComponentRootNode(componentRef: ComponentRef<any>): HTMLElement {
155
        return (componentRef.hostView as EmbeddedViewRef<any>).rootNodes[0] as HTMLElement;
8✔
156
    }
157

158
    private registerComponentFactory(componentOrComponents: PlanetComponent | PlanetComponent[]) {
159
        const app = this.ngModuleRef.instance.appName;
9✔
160
        let appRef$: Observable<NgPlanetApplicationRef | PlanetPortalApplication>;
161

162
        if (app) {
9!
163
            appRef$ = this.getPlantAppRef(app) as Observable<NgPlanetApplicationRef>;
9✔
164
        } else {
NEW
165
            appRef$ = of(globalPlanet.portalApplication);
×
166
        }
167

168
        appRef$.subscribe(appRef => {
9✔
169
            appRef.registerComponentFactory((componentName: string, config: PlantComponentConfig<any>) => {
9✔
170
                const components = coerceArray(componentOrComponents);
9✔
171
                const planetComponent = components.find(item => {
9✔
172
                    return isComponentType(item)
9✔
173
                        ? reflectComponentType(item).selector.includes(componentName)
174
                        : item.name === componentName;
175
                });
176
                if (planetComponent) {
9!
177
                    return this.ngZone.run(() => {
9✔
178
                        const componentRef = this.attachComponent<any>(
9✔
179
                            isComponentType(planetComponent) ? planetComponent : planetComponent.component,
9✔
180
                            appRef instanceof NgPlanetApplicationRef ? appRef.appModuleRef.injector : this.ngModuleRef.injector,
9!
181
                            config
182
                        );
183
                        return componentRef;
8✔
184
                    });
185
                } else {
186
                    throw Error(`unregistered component ${componentName} in app ${app}`);
×
187
                }
188
            });
189
        });
190
    }
191

192
    register(components: PlanetComponent | PlanetComponent[]) {
193
        setTimeout(() => {
9✔
194
            this.registerComponentFactory(components);
9✔
195
        });
196
    }
197

198
    load<TComp = unknown, TData = unknown>(
199
        app: string,
200
        componentName: string,
201
        config: PlantComponentConfig<TData>
202
    ): Observable<PlanetComponentRef<TComp>> {
203
        let appRef$: Observable<NgPlanetApplicationRef | PlanetPortalApplication>;
204
        if (app === globalPlanet.portalApplication.name) {
10!
NEW
UNCOV
205
            appRef$ = of(globalPlanet.portalApplication);
×
206
        } else {
207
            appRef$ = this.getPlantAppRef(app).pipe(
10✔
208
                delayWhen((appRef: NgPlanetApplicationRef) => {
209
                    if (appRef.getComponentFactory()) {
10✔
210
                        return of('');
3✔
211
                    } else {
212
                        // Because register use 'setTimeout',so timer 20
213
                        return timer(20);
7✔
214
                    }
215
                })
216
            );
217
        }
218
        const result = appRef$.pipe(
10✔
219
            map(appRef => {
220
                const componentFactory = appRef.getComponentFactory();
10✔
221
                if (componentFactory) {
10✔
222
                    return componentFactory<TData, TComp>(componentName, config);
9✔
223
                } else {
224
                    throw new Error(`${app}'s component(${componentName}) is not registered`);
1✔
225
                }
226
            }),
227
            shareReplay()
228
        );
229
        result.subscribe();
10✔
230
        return result;
10✔
231
    }
232
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc