├── style.css ├── tsconfig.json ├── README.md ├── app ├── demo5 │ └── standaloneRedBorder.directive.ts ├── demo6 │ └── standaloneStar.pipe.ts ├── demo2 │ └── standaloneWithImport.component.ts ├── demo1 │ └── firstStandalone.component.ts ├── demo8 │ └── boostrappedStandalone.component.ts ├── demo7 │ ├── dynamicallyLoaded.component.ts │ └── dynamicallyLoading.component.ts ├── demo3 │ └── standaloneImportingStandalone.component.ts ├── demo4 │ └── standaloneWithProviders.component.ts ├── app.module.ts └── app.component.ts ├── package.json ├── index.ts ├── index.html └── standaloneShim.ts /style.css: -------------------------------------------------------------------------------- 1 | h1, h2 { 2 | font-family: Lato; 3 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext" 4 | } 5 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ng-standalone 2 | 3 | [Edit on StackBlitz ⚡️](https://stackblitz.com/edit/ng-standalone) -------------------------------------------------------------------------------- /app/demo5/standaloneRedBorder.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive } from '../../standaloneShim'; 2 | 3 | @Directive({ 4 | selector: '[standaloneRedBorder]', 5 | standalone: true, 6 | host: { 7 | style: 'border: 2px dashed red' 8 | } 9 | }) 10 | export class StandaloneRedBorderDirective {} 11 | -------------------------------------------------------------------------------- /app/demo6/standaloneStar.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe } from '../../standaloneShim'; 2 | import { PipeTransform } from '@angular/core'; 3 | 4 | @Pipe({ 5 | name: 'standaloneStar', 6 | standalone: true 7 | }) 8 | export class StandaloneStarPipe implements PipeTransform { 9 | transform(value) { 10 | const stars = new Array(value.length); 11 | return stars.fill('*').join(''); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/demo2/standaloneWithImport.component.ts: -------------------------------------------------------------------------------- 1 | import { FormsModule } from '@angular/forms'; 2 | import { Component } from '../../standaloneShim'; 3 | 4 | @Component({ 5 | selector: 'standalone-with-import-component', 6 | standalone: true, 7 | imports: [FormsModule], 8 | template: ` 9 | Forms work: (name = {{ name }}) 10 | ` 11 | }) 12 | export class StandaloneWithImportComponent { 13 | name = 'Daft Punk'; 14 | } 15 | -------------------------------------------------------------------------------- /app/demo1/firstStandalone.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '../../standaloneShim'; 2 | 3 | @Component({ 4 | selector: 'first-standalone-component', 5 | standalone: true, 6 | template: ` 7 | I'm first! 8 | 9 | {{ counter }} 10 | ` 11 | }) 12 | export class FirstStandaloneComponent { 13 | counter = 0; 14 | 15 | confirm() { 16 | this.counter++; 17 | console.log('confirmed!'); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/demo8/boostrappedStandalone.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '../../standaloneShim'; 2 | 3 | @Component({ 4 | selector: 'bootstrapped-standalone-component', 5 | standalone: true, 6 | template: ` 7 | I'm bootstrapped! 8 | 9 | {{ counter }} 10 | ` 11 | }) 12 | export class BootstrappedStandaloneComponent { 13 | counter = 0; 14 | 15 | confirm() { 16 | this.counter++; 17 | console.log('confirmed!'); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/demo7/dynamicallyLoaded.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '../../standaloneShim'; 2 | 3 | @Component({ 4 | selector: 'dynamically-loaded-standalone-component', 5 | standalone: true, 6 | template: ` 7 | I'm dynamically loaded! 8 | 9 | {{ counter }} 10 | ` 11 | }) 12 | export class DynamicallyLoadedComponent { 13 | counter = 0; 14 | 15 | confirm() { 16 | this.counter++; 17 | console.log('confirmed!'); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-standalone", 3 | "version": "0.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "@angular/animations": "12.2.0", 7 | "@angular/common": "12.2.0", 8 | "@angular/compiler": "12.2.0", 9 | "@angular/core": "12.2.0", 10 | "@angular/forms": "12.2.0", 11 | "@angular/platform-browser": "12.2.0", 12 | "@angular/platform-browser-dynamic": "12.2.0", 13 | "@angular/router": "^12.2.1", 14 | "rxjs": "7.3.0", 15 | "zone.js": "0.11.4" 16 | } 17 | } -------------------------------------------------------------------------------- /app/demo3/standaloneImportingStandalone.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '../../standaloneShim'; 2 | import { FirstStandaloneComponent } from '../demo1/firstStandalone.component'; 3 | 4 | @Component({ 5 | selector: 'standalone-importing-standalone-component', 6 | standalone: true, 7 | imports: [FirstStandaloneComponent], 8 | template: ` 9 | Turtles all the way down: 10 | 11 | ` 12 | }) 13 | export class StandaloneImportingStandaloneComponent {} 14 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import 'zone.js'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { bootstrapComponent } from './standaloneShim'; 5 | import { AppModule } from './app/app.module'; 6 | import { BootstrappedStandaloneComponent } from './app/demo8/boostrappedStandalone.component'; 7 | 8 | platformBrowserDynamic() 9 | .bootstrapModule(AppModule) 10 | .then((ref) => { 11 | // Ensure Angular destroys itself on hot reloads. 12 | if (window['ngRef']) { 13 | window['ngRef'].destroy(); 14 | } 15 | window['ngRef'] = ref; 16 | 17 | // Otherwise, log the boot error 18 | }) 19 | .catch((err) => console.error(err)); 20 | 21 | // demo #9 22 | setTimeout(() => 23 | bootstrapComponent(BootstrappedStandaloneComponent).then(() => { 24 | console.log('bootstrapped standalone component!'); 25 | }) 26 | ); 27 | -------------------------------------------------------------------------------- /app/demo4/standaloneWithProviders.component.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, InjectionToken, Inject } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { Component } from '../../standaloneShim'; 4 | 5 | export const locale = new InjectionToken('locale'); 6 | 7 | @NgModule({ 8 | providers: [ 9 | { provide: locale, multi: true, useValue: 'en' }, 10 | { provide: locale, multi: true, useValue: 'sk' }, 11 | ], 12 | }) 13 | class MyNgModuleWithProvider {} 14 | 15 | @Component({ 16 | selector: 'standalone-with-providers-component', 17 | standalone: true, 18 | imports: [CommonModule, MyNgModuleWithProvider], 19 | template: ` 20 | Supported locales: 21 | 24 | `, 25 | }) 26 | export class StandaloneWithProvidersComponent { 27 | constructor(@Inject(locale) protected locales) {} 28 | } 29 | -------------------------------------------------------------------------------- /app/demo7/dynamicallyLoading.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ComponentFactoryResolver, 3 | Inject, 4 | ViewContainerRef, 5 | } from '@angular/core'; 6 | import { Component, ViewContainerRefShim } from '../../standaloneShim'; 7 | 8 | @Component({ 9 | selector: 'dynamically-loading-component', 10 | standalone: true, 11 | template: 'dynamically loaded: ', 12 | }) 13 | export class DynamicallyLoadingComponent { 14 | vcRef: ViewContainerRefShim; 15 | 16 | // Hack: this will not be needed in the final version, one would just inject a ViewContainerRef 17 | constructor( 18 | // Hack: @Inject is required here, this will not be needed in the final version 19 | @Inject(ViewContainerRef) vcRef: ViewContainerRef, 20 | @Inject(ComponentFactoryResolver) cfResolver: ComponentFactoryResolver 21 | ) { 22 | this.vcRef = new ViewContainerRefShim(vcRef, cfResolver); 23 | } 24 | 25 | ngOnInit() { 26 | import('./dynamicallyLoaded.component').then((m) => 27 | this.vcRef.createComponent(m.DynamicallyLoadedComponent) 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 |

RFC: standalone components, directives and pipes - demo

2 | 3 |

4 | This is a simple demo project showing what a standalone 5 | @Component, @Directive, and @Pipe in 6 | Angular could look like in the future. This demo is based on the design 7 | proposed in the 8 | RFC: standalone components, directives and pipes - making Angular’s 10 | NgModules optional 12 |

13 | 14 |

15 | Demo implementation notes: The demo is built by wrapping an existing 16 | @Component, @Directive, and 17 | @Pipe decorators and generating a hidden (virtual) NgModule. The 18 | application, including the standalone entities, is then compiled using the JIT 19 | compiler. This is not the intended implementation of the design 20 | described in the RFC - it is a quick prototype enabling early experiments with 21 | the new APIs. 22 |

23 | 24 | loading... 25 | -------------------------------------------------------------------------------- /app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | 4 | import { AppComponent } from './app.component'; 5 | import { FirstStandaloneComponent } from './demo1/firstStandalone.component'; 6 | import { StandaloneWithImportComponent } from './demo2/standaloneWithImport.component'; 7 | import { StandaloneImportingStandaloneComponent } from './demo3/standaloneImportingStandalone.component'; 8 | import { 9 | StandaloneWithProvidersComponent, 10 | locale, 11 | } from './demo4/standaloneWithProviders.component'; 12 | import { StandaloneRedBorderDirective } from './demo5/standaloneRedBorder.directive'; 13 | import { StandaloneStarPipe } from './demo6/standaloneStar.pipe'; 14 | import { DynamicallyLoadingComponent } from './demo7/dynamicallyLoading.component'; 15 | import { DynamicallyLoadedComponent } from './demo7/dynamicallyLoaded.component'; 16 | 17 | @NgModule({ 18 | imports: [ 19 | BrowserModule, 20 | // hack: this will become just FirstStandaloneComponent in final version 21 | FirstStandaloneComponent['module'], 22 | StandaloneWithImportComponent['module'], 23 | StandaloneImportingStandaloneComponent['module'], 24 | StandaloneWithProvidersComponent['module'], 25 | StandaloneRedBorderDirective['module'], 26 | StandaloneStarPipe['module'], 27 | DynamicallyLoadingComponent['module'], 28 | // TODO: this is needed for demo #7 right now but should NOT be 29 | DynamicallyLoadedComponent['module'], 30 | ], 31 | // additional provider for demo #4 32 | providers: [{ provide: locale, multi: true, useValue: 'fr' }], 33 | declarations: [AppComponent], 34 | bootstrap: [AppComponent], 35 | }) 36 | export class AppModule {} 37 | -------------------------------------------------------------------------------- /standaloneShim.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component as NgComponent, 3 | Directive as NgDirective, 4 | Pipe as NgPipe, 5 | NgModule, 6 | ViewContainerRef, 7 | ComponentFactoryResolver, 8 | Type, 9 | ModuleWithProviders, 10 | SchemaMetadata, 11 | } from '@angular/core'; 12 | import { BrowserModule } from '@angular/platform-browser'; 13 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 14 | 15 | export function Component( 16 | componentMetadata: NgComponent & { 17 | standalone: true; 18 | // TODO: tighten up the types 19 | imports?: Array | ModuleWithProviders>; 20 | schemas?: Array; 21 | } 22 | ): ClassDecorator { 23 | //console.log(`Standalone @Component declared:`, componentMetadata); 24 | 25 | const ngComponentDecorator = NgComponent(componentMetadata); 26 | 27 | const exportedProviders = []; 28 | const processedImports = 29 | componentMetadata.imports?.map( 30 | (importable) => importable['module'] ?? importable 31 | ) ?? []; 32 | 33 | return function (componentClazz) { 34 | @NgModule({ 35 | declarations: [[componentClazz]], 36 | // TODO: is it a good idea to include CommonModule by default? 37 | imports: [processedImports], 38 | exports: [[componentClazz]], 39 | // TODO: surprisingly the JIT compiler still requires entryComponents for ComponentFactoryResolver to work 40 | entryComponents: [[componentClazz]], 41 | schemas: componentMetadata.schemas, 42 | }) 43 | class VirtualNgModule {} 44 | 45 | componentClazz['module'] = VirtualNgModule; 46 | 47 | return ngComponentDecorator(componentClazz); 48 | }; 49 | } 50 | 51 | export function Directive( 52 | directiveMedatada: NgDirective & { 53 | standalone: true; 54 | imports?: unknown[]; 55 | } 56 | ): ClassDecorator { 57 | //console.log(`Standalone @Directive declared:`, directiveMedatada); 58 | 59 | const ngDirectiveDecorator = NgDirective(directiveMedatada); 60 | 61 | const processedImports = 62 | directiveMedatada.imports?.map( 63 | (importable) => importable['module'] ?? importable 64 | ) ?? []; 65 | 66 | return function (directiveClazz) { 67 | @NgModule({ 68 | declarations: [[directiveClazz]], 69 | exports: [[directiveClazz]], 70 | imports: [processedImports], 71 | }) 72 | class VirtualNgModule {} 73 | 74 | directiveClazz['module'] = VirtualNgModule; 75 | 76 | return ngDirectiveDecorator(directiveClazz); 77 | }; 78 | } 79 | 80 | export function Pipe( 81 | pipeMetadata: NgPipe & { 82 | standalone: true; 83 | imports?: unknown[]; 84 | } 85 | ): ClassDecorator { 86 | //console.log(`Standalone @Pipe declared:`, pipeMetadata); 87 | 88 | const ngPipeDecorator = NgPipe(pipeMetadata); 89 | 90 | const processedImports = 91 | pipeMetadata.imports?.map( 92 | (importable) => importable['module'] ?? importable 93 | ) ?? []; 94 | 95 | return function (pipeClazz) { 96 | @NgModule({ 97 | declarations: [[pipeClazz]], 98 | exports: [[pipeClazz]], 99 | imports: [processedImports], 100 | }) 101 | class VirtualNgModule {} 102 | 103 | pipeClazz['module'] = VirtualNgModule; 104 | 105 | return ngPipeDecorator(pipeClazz); 106 | }; 107 | } 108 | 109 | export class ViewContainerRefShim { 110 | constructor( 111 | private viewContainerRef: ViewContainerRef, 112 | private componentFactoryResolver: ComponentFactoryResolver 113 | ) {} 114 | 115 | createComponent(componetClazz: Type) { 116 | const componentFactory = 117 | this.componentFactoryResolver.resolveComponentFactory(componetClazz); 118 | 119 | this.viewContainerRef.createComponent(componentFactory); 120 | } 121 | } 122 | 123 | export function bootstrapComponent( 124 | componetClazz: Type, 125 | platformModule = BrowserModule 126 | ) { 127 | @NgModule({ 128 | imports: [platformModule, componetClazz['module']], 129 | bootstrap: [componetClazz], 130 | }) 131 | class VirtualBootstrapNgModule {} 132 | 133 | return platformBrowserDynamic().bootstrapModule(VirtualBootstrapNgModule); 134 | } 135 | -------------------------------------------------------------------------------- /app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ElementRef, Inject } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'standalone-component-demo', 5 | template: ` 6 |

Demo #1: A Simple Component

7 |

Simple component with no dependencies

8 | 9 |

 10 |       @Component({
 11 |         selector: 'first-standalone-component',
 12 |         standalone: true,
 13 |         template: \`
 14 |           I'm first!
 15 |           <button (click)="confirm()">click to confirm</button>
 16 |           {{counter}}
 17 |         \`
 18 |       })
 19 |       export class FirstStandaloneComponent {
 20 |         counter = 0;
 21 | 
 22 |         confirm() {
 23 |           this.counter++;
 24 |           console.log('confirmed!');
 25 |         }
 26 |       }
 27 |       
 28 |     
29 | 30 |

Output

31 |
32 | 33 |
34 | 35 | 36 | 37 |

Demo #2: Standalone Component that imports an existing NgModule

38 |

39 | This one imports FormsModule - an existing NgModule - to demonstrate 40 | interop capabilities. 41 |

42 | 43 |

 44 |       @Component({
 45 |         selector: 'standalone-with-import-component',
 46 |         standalone: true,
 47 |         imports: [FormsModule],
 48 |         template: \`
 49 |           Forms work!
 50 |           <input [(ngModel)]="name" />
 51 |           (name = {{ name }})
 52 |         \`
 53 |       })
 54 |       export class StandaloneWithImportComponent {
 55 |         name = 'Daft Punk';
 56 |       }
 57 |     
58 | 59 |

Output

60 |
61 | 62 |
63 | 64 | 65 | 66 |

Demo #3: Standalone Component that imports a standalone Component

67 |

68 | This one imports FirstStandaloneComponent to show off 69 | composability. 70 |

71 | 72 |

 73 |       @Component({
 74 |         selector: 'standalone-importing-standalone-component',
 75 |         standalone: true,
 76 |         imports: [FirstStandaloneComponent],
 77 |         template: \`
 78 |           Turtles all the way down:
 79 |           <first-standalone-component></first-standalone-component>
 80 |         \`
 81 |       })
 82 |       export class StandaloneImportingStandaloneComponent {}
 83 |     
84 | 85 |

Output

86 |
87 | 88 |
89 | 90 | 91 | 92 |

Demo #4: Standalone Component with Multi Providers

93 |

94 | This one provides a multiprovider which it then uses in the template. 95 |

96 | 97 |

 98 |     export const locale = new InjectionToken<string[]>('locale');
 99 | 
100 |     @NgModule({
101 |       providers: [
102 |         { provide: locale, multi: true, useValue: 'en' },
103 |         { provide: locale, multi: true, useValue: 'sk' }
104 |       ]
105 |     })
106 |     class MyNgModuleWithProvider {}
107 |     
108 |     @Component({
109 |       selector: 'standalone-with-providers-component',
110 |       standalone: true,
111 |       imports: [CommonModule, MyNgModuleWithProvider],      
112 |       template: \`
113 |         Supported locales:
114 |         <ul>
115 |           <li *ngFor="let locale of locales">{{ locale }}</li>
116 |         </ul>
117 |       \`
118 |     })
119 |     export class StandaloneWithProvidersComponent {
120 |       constructor(@Inject(locale) protected locales) {}
121 |     }
122 |     
123 |     
124 | 125 |

Output

126 |
127 | 128 |
129 | 130 |

Demo #5: Standalone Directive

131 |

132 | This shows that standalone directives work in the same style as standalone 133 | components. 134 |

135 | 136 |

137 |       @Directive({
138 |         selector: '[standaloneRedBorder]',
139 |         standalone: true,
140 |         host: {
141 |           style: 'border: 2px dashed red'
142 |         }
143 |       })
144 |       export class StandaloneRedBorderDirective {}
145 | 
146 |       <first-standalone-component standaloneRedBorder></first-standalone-component>
147 |     
148 | 149 |

Output

150 |
151 | 154 |
155 | 156 | 157 | 158 |

Demo #6: Standalone Pipe

159 |

160 | This shows that standalone pipe work just fine as well. 161 |

162 | 163 |

164 |       @Pipe({
165 |         name: 'standaloneStar',
166 |         standalone: true
167 |       })
168 |       export class StandaloneStarPipe {
169 |         transform(value) {
170 |           const stars = new Array(value.length);
171 |           return stars.fill('*').join('');
172 |         }
173 |       }
174 | 
175 |       {{ 'hello there' | standaloneStarPipe }}
176 |     
177 | 178 |

Output

179 |
180 | {{ 'hello there' | standaloneStar }} 181 |
182 | 183 | 184 | 185 |

Demo #7: Dynamically Loaded Standalone Component

186 |

187 | Standalone components can be dynamically loaded. 188 |

189 | 190 |

191 |       @Component({
192 |         selector: 'dynamically-loading-component',
193 |         standalone: true,
194 |         template: 'dynamically loaded: '
195 |       })
196 |       export class DynamicallyLoadingComponent {
197 |         constructor(
198 |           private vcRef: ViewContainerRef,
199 |         ) {}
200 | 
201 |         ngOnInit() {
202 |           import('./dynamicallyLoaded.component').then((m) =>
203 |             this.vcRef.createComponent(m.DynamicallyLoadedComponent)
204 |           );
205 |         }
206 |       }
207 | 
208 |     
209 | 210 |

Output

211 |
212 | 213 |
214 | 215 |

Demo #8: Bootstrap standalone component

216 |

217 | Standalone components can be bootstrapped! 218 |

219 | 220 |

221 |       @Component({
222 |         selector: 'bootstrapped-standalone-component',
223 |         standalone: true,
224 |         template: \`
225 |           I'm bootstrapped!
226 |           <button (click)="confirm()">click to confirm</button>
227 |           {{ counter }}
228 |         \`
229 |       })
230 |       export class BootstrappedStandaloneComponent {
231 |         counter = 0;
232 |       
233 |         confirm() {
234 |           this.counter++;
235 |           console.log('confirmed!');
236 |         }
237 |       }
238 |       
239 | 240 | <bootstrapped-standalone-component></bootstrapped-standalone-component> 241 | 242 |
243 | 244 | bootstrapComponent(BootstrappedStandaloneComponent).then(() => { 245 | console.log('bootstrapped standalone component!'); 246 | }); 247 |
248 | 249 |

Output

250 |
251 | 252 |
253 |
254 | `, 255 | }) 256 | export class AppComponent { 257 | constructor(@Inject(ElementRef) private elementRef: ElementRef) {} 258 | 259 | ngOnInit() { 260 | // TODO: is this is easiest way to get Angular to ignore the element to be 261 | // bootstrapped? ngNonBindable seemed to work at first, but not it doesn't 262 | // so I ended up doing this... :-| 263 | this.elementRef.nativeElement 264 | .querySelector('#boostrappedStandaloneComponentPlaceholder') 265 | .appendChild(document.createElement('bootstrapped-standalone-component')); 266 | } 267 | } 268 | --------------------------------------------------------------------------------