Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(elements): create custom elements without NgModule #46475

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions goldens/public-api/platform-browser/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ export class By {
static directive(type: Type<any>): Predicate<DebugNode>;
}

// @public
export function createApplication(options?: ApplicationConfig): Promise<ApplicationRef>;
pkozlowski-opensource marked this conversation as resolved.
Show resolved Hide resolved

// @public
export function disableDebugTools(): void;

Expand Down
56 changes: 32 additions & 24 deletions packages/core/src/application_ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,8 @@ export function runPlatformInitializers(injector: Injector): void {
}

/**
* Internal bootstrap application API that implements the core bootstrap logic.
* Internal create application API that implements the core application creation logic and optional
* bootstrap logic.
*
* Platforms (such as `platform-browser`) may require different set of application and platform
* providers for an application to function correctly. As a result, platforms may use this function
Expand All @@ -186,17 +187,20 @@ export function runPlatformInitializers(injector: Injector): void {
*
* @returns A promise that returns an `ApplicationRef` instance once resolved.
*/
export function internalBootstrapApplication(config: {
rootComponent: Type<unknown>,
export function internalCreateApplication(config: {
rootComponent?: Type<unknown>,
appProviders?: Array<Provider|ImportedNgModuleProviders>,
platformProviders?: Provider[],
}): Promise<ApplicationRef> {
const {rootComponent, appProviders, platformProviders} = config;
NG_DEV_MODE && assertStandaloneComponentType(rootComponent);

if (NG_DEV_MODE && rootComponent !== undefined) {
assertStandaloneComponentType(rootComponent);
}

const platformInjector = createOrReusePlatformInjector(platformProviders as StaticProvider[]);

const ngZone = new NgZone(getNgZoneOptions());
const ngZone = getNgZone('zone.js', getNgZoneOptions());

return ngZone.run(() => {
// Create root application injector based on a set of providers configured at the platform
Expand All @@ -205,10 +209,11 @@ export function internalBootstrapApplication(config: {
{provide: NgZone, useValue: ngZone}, //
...(appProviders || []), //
];
const appInjector = createEnvironmentInjector(

const envInjector = createEnvironmentInjector(
allAppProviders, platformInjector as EnvironmentInjector, 'Environment Injector');

const exceptionHandler: ErrorHandler|null = appInjector.get(ErrorHandler, null);
const exceptionHandler: ErrorHandler|null = envInjector.get(ErrorHandler, null);
if (NG_DEV_MODE && !exceptionHandler) {
throw new RuntimeError(
RuntimeErrorCode.ERROR_HANDLER_NOT_FOUND,
Expand All @@ -223,27 +228,30 @@ export function internalBootstrapApplication(config: {
}
});
});

// If the whole platform is destroyed, invoke the `destroy` method
// for all bootstrapped applications as well.
const destroyListener = () => envInjector.destroy();
const onPlatformDestroyListeners = platformInjector.get(PLATFORM_DESTROY_LISTENERS);
onPlatformDestroyListeners.add(destroyListener);

envInjector.onDestroy(() => {
onErrorSubscription.unsubscribe();
onPlatformDestroyListeners.delete(destroyListener);
});

return _callAndReportToErrorHandler(exceptionHandler!, ngZone, () => {
const initStatus = appInjector.get(ApplicationInitStatus);
const initStatus = envInjector.get(ApplicationInitStatus);
initStatus.runInitializers();

return initStatus.donePromise.then(() => {
const localeId = appInjector.get(LOCALE_ID, DEFAULT_LOCALE_ID);
const localeId = envInjector.get(LOCALE_ID, DEFAULT_LOCALE_ID);
setLocaleId(localeId || DEFAULT_LOCALE_ID);

const appRef = appInjector.get(ApplicationRef);

// If the whole platform is destroyed, invoke the `destroy` method
// for all bootstrapped applications as well.
const destroyListener = () => appRef.destroy();
const onPlatformDestroyListeners = platformInjector.get(PLATFORM_DESTROY_LISTENERS, null);
onPlatformDestroyListeners?.add(destroyListener);

appRef.onDestroy(() => {
onPlatformDestroyListeners?.delete(destroyListener);
onErrorSubscription.unsubscribe();
});

appRef.bootstrap(rootComponent);
const appRef = envInjector.get(ApplicationRef);
if (rootComponent !== undefined) {
appRef.bootstrap(rootComponent);
}
return appRef;
});
});
Expand Down Expand Up @@ -493,7 +501,7 @@ export class PlatformRef {
}

private _moduleDoBootstrap(moduleRef: InternalNgModuleRef<any>): void {
const appRef = moduleRef.injector.get(ApplicationRef) as ApplicationRef;
const appRef = moduleRef.injector.get(ApplicationRef);
if (moduleRef._bootstrapComponents.length > 0) {
moduleRef._bootstrapComponents.forEach(f => appRef.bootstrap(f));
} else if (moduleRef.instance.ngDoBootstrap) {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/core_private_export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

export {ALLOW_MULTIPLE_PLATFORMS as ɵALLOW_MULTIPLE_PLATFORMS, internalBootstrapApplication as ɵinternalBootstrapApplication} from './application_ref';
export {ALLOW_MULTIPLE_PLATFORMS as ɵALLOW_MULTIPLE_PLATFORMS, internalCreateApplication as ɵinternalCreateApplication} from './application_ref';
export {APP_ID_RANDOM_PROVIDER as ɵAPP_ID_RANDOM_PROVIDER} from './application_tokens';
export {defaultIterableDiffers as ɵdefaultIterableDiffers, defaultKeyValueDiffers as ɵdefaultKeyValueDiffers} from './change_detection/change_detection';
export {ChangeDetectorStatus as ɵChangeDetectorStatus, isDefaultChangeDetectionStrategy as ɵisDefaultChangeDetectionStrategy} from './change_detection/constants';
Expand Down
12 changes: 9 additions & 3 deletions packages/core/test/application_ref_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,20 +333,26 @@ class SomeComponent {
withModule(
{providers},
waitForAsync(inject([EnvironmentInjector], (parentInjector: EnvironmentInjector) => {
// This is a temporary type to represent an instance of an R3Injector, which
// can be destroyed.
// The type will be replaced with a different one once destroyable injector
// type is available.
type DestroyableInjector = EnvironmentInjector&{destroyed?: boolean};

createRootEl();

const injector = createApplicationRefInjector(parentInjector);
const injector = createApplicationRefInjector(parentInjector) as DestroyableInjector;

const appRef = injector.get(ApplicationRef);
appRef.bootstrap(SomeComponent);

expect(appRef.destroyed).toBeFalse();
expect((injector as any).destroyed).toBeFalse();
expect(injector.destroyed).toBeFalse();

appRef.destroy();

expect(appRef.destroyed).toBeTrue();
expect((injector as any).destroyed).toBeTrue();
expect(injector.destroyed).toBeTrue();
}))));
});

Expand Down
52 changes: 52 additions & 0 deletions packages/elements/test/create-custom-element-env_spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {Component} from '@angular/core';
import {createApplication} from '@angular/platform-browser';
import {browserDetection} from '@angular/platform-browser/testing/src/browser_util';

import {createCustomElement} from '../public_api';

if (browserDetection.supportsCustomElements) {
describe('createCustomElement with env injector', () => {
let testContainer: HTMLDivElement;

beforeEach(() => {
testContainer = document.createElement('div');
document.body.appendChild(testContainer);
});

afterEach(() => {
document.body.removeChild(testContainer);
(testContainer as any) = null;
});

it('should use provided EnvironmentInjector to create a custom element', async () => {
@Component({
standalone: true,
template: `Hello, standalone element!`,
})
class TestStandaloneCmp {
}

const appRef = await createApplication();

try {
const NgElementCtor = createCustomElement(TestStandaloneCmp, {injector: appRef.injector});
pkozlowski-opensource marked this conversation as resolved.
Show resolved Hide resolved

customElements.define('test-standalone-cmp', NgElementCtor);
const customEl = document.createElement('test-standalone-cmp');
testContainer.appendChild(customEl);

expect(testContainer.innerText).toBe('Hello, standalone element!');
} finally {
appRef.destroy();
}
});
});
}
32 changes: 26 additions & 6 deletions packages/platform-browser/src/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import {CommonModule, DOCUMENT, XhrFactory, ɵPLATFORM_BROWSER_ID as PLATFORM_BROWSER_ID} from '@angular/common';
import {APP_ID, ApplicationModule, ApplicationRef, createPlatformFactory, ErrorHandler, ImportedNgModuleProviders, Inject, InjectionToken, ModuleWithProviders, NgModule, NgZone, Optional, PLATFORM_ID, PLATFORM_INITIALIZER, platformCore, PlatformRef, Provider, RendererFactory2, SkipSelf, StaticProvider, Testability, TestabilityRegistry, Type, ɵINJECTOR_SCOPE as INJECTOR_SCOPE, ɵinternalBootstrapApplication as internalBootstrapApplication, ɵsetDocument, ɵTESTABILITY as TESTABILITY, ɵTESTABILITY_GETTER as TESTABILITY_GETTER} from '@angular/core';
import {APP_ID, ApplicationModule, ApplicationRef, createPlatformFactory, ErrorHandler, ImportedNgModuleProviders, Inject, InjectionToken, ModuleWithProviders, NgModule, NgZone, Optional, PLATFORM_ID, PLATFORM_INITIALIZER, platformCore, PlatformRef, Provider, RendererFactory2, SkipSelf, StaticProvider, Testability, TestabilityRegistry, Type, ɵINJECTOR_SCOPE as INJECTOR_SCOPE, ɵinternalCreateApplication as internalCreateApplication, ɵsetDocument, ɵTESTABILITY as TESTABILITY, ɵTESTABILITY_GETTER as TESTABILITY_GETTER} from '@angular/core';

import {BrowserDomAdapter} from './browser/browser_adapter';
import {SERVER_TRANSITION_PROVIDERS, TRANSITION_ID} from './browser/server-transition';
Expand All @@ -22,7 +22,7 @@ import {DomSharedStylesHost, SharedStylesHost} from './dom/shared_styles_host';
const NG_DEV_MODE = typeof ngDevMode === 'undefined' || !!ngDevMode;

/**
* Set of config options available during the bootstrap operation via `bootstrapApplication` call.
* Set of config options available during the application bootstrap operation.
*
* @developerPreview
* @publicApi
Expand Down Expand Up @@ -96,14 +96,34 @@ export interface ApplicationConfig {
*/
export function bootstrapApplication(
rootComponent: Type<unknown>, options?: ApplicationConfig): Promise<ApplicationRef> {
return internalBootstrapApplication({
rootComponent,
return internalCreateApplication({rootComponent, ...createProvidersConfig(options)});
}

/**
* Create an instance of an Angular application without bootstrapping any components. This is useful
* for the situation where one wants to decouple application environment creation (a platform and
* associated injectors) from rendering components on a screen. Components can be subsequently
* bootstrapped on the returned `ApplicationRef`.
*
* @param options Extra configuration for the application environment, see `ApplicationConfig` for
* additional info.
* @returns A promise that returns an `ApplicationRef` instance once resolved.
*
* @publicApi
* @developerPreview
*/
export function createApplication(options?: ApplicationConfig) {
return internalCreateApplication(createProvidersConfig(options));
}

function createProvidersConfig(options?: ApplicationConfig) {
return {
appProviders: [
...BROWSER_MODULE_PROVIDERS,
...(options?.providers ?? []),
],
platformProviders: INTERNAL_BROWSER_PLATFORM_PROVIDERS,
});
platformProviders: INTERNAL_BROWSER_PLATFORM_PROVIDERS
};
}

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/platform-browser/src/platform-browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

export {ApplicationConfig, bootstrapApplication, BrowserModule, platformBrowser, provideProtractorTestingSupport} from './browser';
export {ApplicationConfig, bootstrapApplication, BrowserModule, createApplication, platformBrowser, provideProtractorTestingSupport} from './browser';
export {Meta, MetaDefinition} from './browser/meta';
export {Title} from './browser/title';
export {disableDebugTools, enableDebugTools} from './browser/tools/tools';
Expand Down
4 changes: 2 additions & 2 deletions packages/platform-server/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {ApplicationRef, ImportedNgModuleProviders, importProvidersFrom, NgModuleFactory, NgModuleRef, PlatformRef, Provider, StaticProvider, Type, ɵinternalBootstrapApplication as internalBootstrapApplication, ɵisPromise} from '@angular/core';
import {ApplicationRef, ImportedNgModuleProviders, importProvidersFrom, NgModuleFactory, NgModuleRef, PlatformRef, Provider, StaticProvider, Type, ɵinternalCreateApplication as internalCreateApplication, ɵisPromise} from '@angular/core';
import {BrowserModule, ɵTRANSITION_ID} from '@angular/platform-browser';
import {first} from 'rxjs/operators';

Expand Down Expand Up @@ -152,7 +152,7 @@ export function renderApplication<T>(rootComponent: Type<T>, options: {
importProvidersFrom(ServerModule),
...(options.providers ?? []),
];
return _render(platform, internalBootstrapApplication({rootComponent, appProviders}));
return _render(platform, internalCreateApplication({rootComponent, appProviders}));
}

/**
Expand Down
3 changes: 2 additions & 1 deletion packages/platform-server/test/integration_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -740,7 +740,8 @@ describe('platform-server integration', () => {

// Run the set of tests with regular and standalone components.
[true, false].forEach((isStandalone: boolean) => {
it('using renderModule should work', waitForAsync(() => {
it(`using ${isStandalone ? 'renderApplication' : 'renderModule'} should work`,
waitForAsync(() => {
const options = {document: doc};
const bootstrap = isStandalone ?
renderApplication(MyAsyncServerAppStandalone, {...options, appId: 'simple-cmp'}) :
Expand Down