diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 91e7cbd6fbd..9a31d09ba97 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: - node-version: [18.x, 20.5.x] + node-version: [18.x, 20.x] steps: - name: Checkout @@ -56,7 +56,7 @@ jobs: run: | npm run build:i18n npm run test:i18n:dist - - name: Bundle Tree-Shake Test + - name: Bundle Tree-Shake & SSR Test run: npm run build:bundletest - name: Publish to coveralls.io if: github.repository == 'IgniteUI/igniteui-angular' && matrix.node-version == '18.x' diff --git a/angular.json b/angular.json index ead65a4bbe8..50dddb54339 100644 --- a/angular.json +++ b/angular.json @@ -270,6 +270,11 @@ "includePaths": [ "node_modules" ] + }, + "server": "projects/bundle-test/src/main.server.ts", + "prerender": true, + "ssr": { + "entry": "projects/bundle-test/server.ts" } }, "configurations": { diff --git a/projects/bundle-test/server.ts b/projects/bundle-test/server.ts new file mode 100644 index 00000000000..7083b14fe9d --- /dev/null +++ b/projects/bundle-test/server.ts @@ -0,0 +1,56 @@ +import { APP_BASE_HREF } from '@angular/common'; +import { CommonEngine } from '@angular/ssr'; +import express from 'express'; +import { fileURLToPath } from 'node:url'; +import { dirname, join, resolve } from 'node:path'; +import bootstrap from './src/main.server'; + +// The Express app is exported so that it can be used by serverless Functions. +export function app(): express.Express { + const server = express(); + const serverDistFolder = dirname(fileURLToPath(import.meta.url)); + const browserDistFolder = resolve(serverDistFolder, '../browser'); + const indexHtml = join(serverDistFolder, 'index.server.html'); + + const commonEngine = new CommonEngine(); + + server.set('view engine', 'html'); + server.set('views', browserDistFolder); + + // Example Express Rest API endpoints + // server.get('/api/**', (req, res) => { }); + // Serve static files from /browser + server.get('*.*', express.static(browserDistFolder, { + maxAge: '1y' + })); + + // All regular routes use the Angular engine + server.get('*', (req, res, next) => { + const { protocol, originalUrl, baseUrl, headers } = req; + + commonEngine + .render({ + bootstrap, + documentFilePath: indexHtml, + url: `${protocol}://${headers.host}${originalUrl}`, + publicPath: browserDistFolder, + providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }], + }) + .then((html) => res.send(html)) + .catch((err) => next(err)); + }); + + return server; +} + +function run(): void { + const port = process.env['PORT'] || 4000; + + // Start up the Node server + const server = app(); + server.listen(port, () => { + console.log(`Node Express server listening on http://localhost:${port}`); + }); +} + +run(); diff --git a/projects/bundle-test/src/app/app.component.spec.ts b/projects/bundle-test/src/app/app.component.spec.ts index 9dc7fa203ea..b460dd6df88 100644 --- a/projects/bundle-test/src/app/app.component.spec.ts +++ b/projects/bundle-test/src/app/app.component.spec.ts @@ -3,7 +3,7 @@ import { AppComponent } from './app.component'; describe('AppComponent', () => { beforeEach(() => TestBed.configureTestingModule({ - declarations: [AppComponent] + imports: [AppComponent] })); it('should create the app', () => { diff --git a/projects/bundle-test/src/app/app.component.ts b/projects/bundle-test/src/app/app.component.ts index 4e0f39d1749..7f18c3f3c64 100644 --- a/projects/bundle-test/src/app/app.component.ts +++ b/projects/bundle-test/src/app/app.component.ts @@ -1,10 +1,13 @@ import { Component } from '@angular/core'; import { ChipResourceStringsBG } from 'igniteui-angular-i18n'; +import { RouterOutlet } from '@angular/router'; @Component({ - selector: 'app-root', - templateUrl: './app.component.html', - styleUrls: ['./app.component.scss'] + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.scss'], + standalone: true, + imports: [RouterOutlet] }) export class AppComponent { protected chipStrings = ChipResourceStringsBG; diff --git a/projects/bundle-test/src/app/app.config.server.ts b/projects/bundle-test/src/app/app.config.server.ts new file mode 100644 index 00000000000..b4d57c94235 --- /dev/null +++ b/projects/bundle-test/src/app/app.config.server.ts @@ -0,0 +1,11 @@ +import { mergeApplicationConfig, ApplicationConfig } from '@angular/core'; +import { provideServerRendering } from '@angular/platform-server'; +import { appConfig } from './app.config'; + +const serverConfig: ApplicationConfig = { + providers: [ + provideServerRendering() + ] +}; + +export const config = mergeApplicationConfig(appConfig, serverConfig); diff --git a/projects/bundle-test/src/app/app.config.ts b/projects/bundle-test/src/app/app.config.ts new file mode 100644 index 00000000000..e5a3cf10050 --- /dev/null +++ b/projects/bundle-test/src/app/app.config.ts @@ -0,0 +1,9 @@ +import { ApplicationConfig } from '@angular/core'; +import { provideRouter } from '@angular/router'; + +import { routes } from './app.routes'; +import { provideClientHydration } from '@angular/platform-browser'; + +export const appConfig: ApplicationConfig = { + providers: [provideRouter(routes), provideClientHydration()] +}; diff --git a/projects/bundle-test/src/app/app.module.ts b/projects/bundle-test/src/app/app.module.ts deleted file mode 100644 index 2df2a345667..00000000000 --- a/projects/bundle-test/src/app/app.module.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { NgModule } from '@angular/core'; -import { BrowserModule } from '@angular/platform-browser'; - -import { AppComponent } from './app.component'; -import { AppRoutingModule } from './app-routing.module'; - -@NgModule({ - declarations: [ - AppComponent - ], - imports: [ - BrowserModule, - AppRoutingModule - ], - providers: [], - bootstrap: [AppComponent] -}) -export class AppModule { } diff --git a/projects/bundle-test/src/app/app-routing.module.ts b/projects/bundle-test/src/app/app.routes.ts similarity index 67% rename from projects/bundle-test/src/app/app-routing.module.ts rename to projects/bundle-test/src/app/app.routes.ts index 1bdc8840d08..00dc1935ec0 100644 --- a/projects/bundle-test/src/app/app-routing.module.ts +++ b/projects/bundle-test/src/app/app.routes.ts @@ -1,17 +1,12 @@ -import { NgModule } from '@angular/core'; -import { RouterModule, Routes } from '@angular/router'; +import { Routes } from '@angular/router'; import { ChipComponent } from './chip/chip.component'; +import { ButtonGroupComponent } from './button-group/button-group.component'; -const routes: Routes = [ +export const routes: Routes = [ { path: '', redirectTo: '/chip', pathMatch: 'full' }, { path: 'chip', component: ChipComponent }, + { path: 'button-group', component: ButtonGroupComponent} // { path: 'form', loadComponent: () => import('./form/form.component').then(m => m.FormComponent) }, // { path: 'stepper', loadComponent: () => import('./stepper/stepper.component').then(m => m.StepperComponent) }, // { path: 'grid', loadComponent: () => import('./grid/grid.component').then(m => m.GridComponent) } ]; - -@NgModule({ - imports: [RouterModule.forRoot(routes)], - exports: [RouterModule] -}) -export class AppRoutingModule { } diff --git a/projects/bundle-test/src/app/button-group/button-group.component.html b/projects/bundle-test/src/app/button-group/button-group.component.html new file mode 100644 index 00000000000..f2480132898 --- /dev/null +++ b/projects/bundle-test/src/app/button-group/button-group.component.html @@ -0,0 +1,5 @@ + + + + + diff --git a/projects/bundle-test/src/app/button-group/button-group.component.scss b/projects/bundle-test/src/app/button-group/button-group.component.scss new file mode 100644 index 00000000000..5d4e87f30f6 --- /dev/null +++ b/projects/bundle-test/src/app/button-group/button-group.component.scss @@ -0,0 +1,3 @@ +:host { + display: block; +} diff --git a/projects/bundle-test/src/app/button-group/button-group.component.ts b/projects/bundle-test/src/app/button-group/button-group.component.ts new file mode 100644 index 00000000000..cef55eeb82c --- /dev/null +++ b/projects/bundle-test/src/app/button-group/button-group.component.ts @@ -0,0 +1,14 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { IGX_BUTTON_GROUP_DIRECTIVES } from 'igniteui-angular'; + +@Component({ + selector: 'app-button-group', + standalone: true, + imports: [ + IGX_BUTTON_GROUP_DIRECTIVES + ], + templateUrl: './button-group.component.html', + styleUrl: './button-group.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ButtonGroupComponent { } diff --git a/projects/bundle-test/src/main.server.ts b/projects/bundle-test/src/main.server.ts new file mode 100644 index 00000000000..4b9d4d1545c --- /dev/null +++ b/projects/bundle-test/src/main.server.ts @@ -0,0 +1,7 @@ +import { bootstrapApplication } from '@angular/platform-browser'; +import { AppComponent } from './app/app.component'; +import { config } from './app/app.config.server'; + +const bootstrap = () => bootstrapApplication(AppComponent, config); + +export default bootstrap; diff --git a/projects/bundle-test/src/main.ts b/projects/bundle-test/src/main.ts index c58dc05cbc6..0f61d541f26 100644 --- a/projects/bundle-test/src/main.ts +++ b/projects/bundle-test/src/main.ts @@ -1,7 +1,6 @@ -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +import { AppComponent } from './app/app.component'; +import { bootstrapApplication } from '@angular/platform-browser'; +import { appConfig } from './app/app.config'; -import { AppModule } from './app/app.module'; - -platformBrowserDynamic().bootstrapModule(AppModule) - .catch(err => console.error(err)); +bootstrapApplication(AppComponent, appConfig).catch(err => console.error(err)); diff --git a/projects/bundle-test/src/styles.scss b/projects/bundle-test/src/styles.scss index 1996828f342..783aab82c74 100644 --- a/projects/bundle-test/src/styles.scss +++ b/projects/bundle-test/src/styles.scss @@ -18,6 +18,7 @@ $app-palette: palette( $include: ( igx-chip, + igx-buttongroup, // igx-checkbox, // igx-radio, // igx-switch, diff --git a/projects/bundle-test/tsconfig.app.json b/projects/bundle-test/tsconfig.app.json index 75147ba1395..909b1086139 100644 --- a/projects/bundle-test/tsconfig.app.json +++ b/projects/bundle-test/tsconfig.app.json @@ -3,7 +3,9 @@ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "../../out-tsc/app", - "types": [], + "types": [ + "node" + ], "paths": { "igniteui-angular": [ "dist/igniteui-angular" @@ -17,7 +19,9 @@ } }, "files": [ - "src/main.ts" + "src/main.ts", + "src/main.server.ts", + "server.ts" ], "include": [ "src/**/*.d.ts" diff --git a/projects/igniteui-angular/src/lib/buttonGroup/buttonGroup.component.ts b/projects/igniteui-angular/src/lib/buttonGroup/buttonGroup.component.ts index fb9304772c2..e3b2ee7d4db 100644 --- a/projects/igniteui-angular/src/lib/buttonGroup/buttonGroup.component.ts +++ b/projects/igniteui-angular/src/lib/buttonGroup/buttonGroup.component.ts @@ -470,7 +470,7 @@ export class IgxButtonGroupComponent extends DisplayDensityBase implements After this.mutationObserver = this.setMutationsObserver(); - this.mutationObserver.observe(this._el.nativeElement, this.observerConfig); + this.mutationObserver?.observe(this._el.nativeElement, this.observerConfig); } /** @@ -483,7 +483,7 @@ export class IgxButtonGroupComponent extends DisplayDensityBase implements After this.queryListNotifier$.next(); this.queryListNotifier$.complete(); - this.mutationObserver.disconnect(); + this.mutationObserver?.disconnect(); } /** @@ -513,28 +513,35 @@ export class IgxButtonGroupComponent extends DisplayDensityBase implements After } } - this.mutationObserver.observe(this._el.nativeElement, this.observerConfig); + this.mutationObserver?.observe(this._el.nativeElement, this.observerConfig); } private setMutationsObserver() { - return new MutationObserver((records, observer) => { - // Stop observing while handling changes - observer.disconnect(); + if (typeof MutationObserver !== 'undefined') { + return new MutationObserver((records, observer) => { + // Stop observing while handling changes + observer.disconnect(); - const updatedButtons = this.getUpdatedButtons(records); + const updatedButtons = this.getUpdatedButtons(records); - if (updatedButtons.length > 0) { - updatedButtons.forEach((button) => { - const index = this.buttons.map((b) => b.nativeElement).indexOf(button); - const args: IButtonGroupEventArgs = { owner: this, button: this.buttons[index], index }; + if (updatedButtons.length > 0) { + updatedButtons.forEach((button) => { + const index = this.buttons.map((b) => b.nativeElement).indexOf(button); + const args: IButtonGroupEventArgs = { owner: this, button: this.buttons[index], index }; - this.updateButtonSelectionState(index, args); - }); - } + this.updateButtonSelectionState(index, args); + }); + } - // Watch for changes again - observer.observe(this._el.nativeElement, this.observerConfig); - }); + // Watch for changes again + observer.observe(this._el.nativeElement, this.observerConfig); + + // Cleanup function + this._renderer.listen(this._el.nativeElement, 'DOMNodeRemoved', () => { + observer.disconnect(); + }); + }); + } } private getUpdatedButtons(records: MutationRecord[]) { diff --git a/tsconfig.json b/tsconfig.json index b25a1d729f4..ca7806c9773 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,18 +4,20 @@ "baseUrl": "./", "downlevelIteration": true, "importHelpers": true, - "module": "es2020", + "module": "es2022", "outDir": "./dist/out-tsc", "sourceMap": true, "declaration": false, "moduleResolution": "node", "experimentalDecorators": true, + "forceConsistentCasingInFileNames": true, + "esModuleInterop": true, "target": "ES2022", "typeRoots": [ "node_modules/@types" ], "lib": [ - "es2019", + "es2022", "dom" ], "paths": { @@ -45,6 +47,7 @@ "generateDeepReexports": true, "strictTemplates": true, "fullTemplateTypeCheck": true, - "strictInjectionParameters": true + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, } }