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

Integrate ssr #157

Merged
merged 17 commits into from
Apr 6, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,24 @@
"outputPath": "dist/showcase",
"index": "projects/showcase/src/index.html",
"browser": "projects/showcase/src/main.ts",
"polyfills": ["projects/showcase/src/polyfills.ts"],
"polyfills": [
"projects/showcase/src/polyfills.ts", "zone.js"
HarelM marked this conversation as resolved.
Show resolved Hide resolved
],
"tsConfig": "projects/showcase/tsconfig.app.json",
"assets": [
"projects/showcase/src/favicon.ico",
"projects/showcase/src/assets",
"projects/showcase/src/app/demo/examples"
],
"styles": ["projects/showcase/src/styles.css"],
"scripts": []
"styles": [
"projects/showcase/src/styles.css"
],
"scripts": [],
"server": "projects/showcase/src/main.server.ts",
"prerender": true,
"ssr": {
"entry": "projects/showcase/server.ts"
}
},
"configurations": {
"production": {
Expand Down
193 changes: 66 additions & 127 deletions package-lock.json

Large diffs are not rendered by default.

10 changes: 8 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"lint": "ng lint",
"e2e": "ng run showcase:cypress-run",
"cypress:open": "ng run showcase:cypress-open",
"docs": "typedoc"
"docs": "typedoc",
"serve:ssr:showcase": "node dist/showcase/server/server.mjs"
HarelM marked this conversation as resolved.
Show resolved Hide resolved
},
"private": true,
"engines": {
Expand All @@ -27,12 +28,15 @@
"@angular/material": "^17.3.1",
"@angular/platform-browser": "^17.3.1",
"@angular/platform-browser-dynamic": "^17.3.1",
"@angular/platform-server": "^17.3.1",
"@angular/router": "^17.3.1",
"@angular/ssr": "^17.3.2",
"@ngrx/effects": "^17.1.1",
"@ngrx/router-store": "^17.1.1",
"@ngrx/store": "^17.1.1",
"@ngrx/store-devtools": "^17.1.1",
"@stackblitz/sdk": "^1.9.0",
"express": "^4.18.2",
"lodash-es": "^4.17.21",
"maplibre-gl": "^4.1.2",
"rxjs": "7.8.1",
Expand All @@ -52,10 +56,12 @@
"@cypress/schematic": "^2.5.1",
"@ngrx/schematics": "^17.1.1",
"@shellygo/cypress-test-utils": "^2.0.54",
"@types/express": "^4.17.17",
"@types/geojson": "^7946.0.14",
"@types/jasmine": "~5.1.4",
"@types/jasminewd2": "^2.0.13",
"@types/lodash-es": "^4.17.12",
"@types/node": "^18.18.0",
"@types/pixelmatch": "^5.2.6",
"@types/pngjs": "^6.0.4",
"@typescript-eslint/eslint-plugin": "^7.4.0",
Expand Down Expand Up @@ -86,4 +92,4 @@
"path": "./node_modules/cz-conventional-changelog"
}
}
}
}
25 changes: 15 additions & 10 deletions projects/ngx-maplibre-gl/src/lib/control/control.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
Input,
OnDestroy,
ViewChild,
afterNextRender,
} from '@angular/core';
import { ControlPosition, IControl } from 'maplibre-gl';
import { MapService } from '../map/map.service';
Expand Down Expand Up @@ -65,20 +66,24 @@ export class ControlComponent<T extends IControl>

control: T | CustomControl;

constructor(private mapService: MapService) {}
constructor(private mapService: MapService) {
afterNextRender(() => {
if (this.content.nativeElement.childNodes.length) {
this.control = new CustomControl(this.content.nativeElement);
this.mapService.mapCreated$.subscribe(() => {
this.mapService.addControl(this.control!, this.position);
});
}
})
}

ngAfterContentInit() {
if (this.content.nativeElement.childNodes.length) {
this.control = new CustomControl(this.content.nativeElement);
this.mapService.mapCreated$.subscribe(() => {
this.mapService.addControl(this.control!, this.position);
});
}

}

ngOnDestroy() {
if (this.mapService.mapInstance.hasControl(this.control)) {
this.mapService.removeControl(this.control);
}
// if (this.mapService.mapInstance.hasControl(this.control)) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess you could use "?" here so that if this is not initialized, then it won't do anything, can't you?
I'm not very worried about SSR memory leaks for this lib, right?

Copy link
Member Author

@birkskyum birkskyum Mar 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm more concerned about leaks on the client actually, where the objects are created, because the SSR apps today often do soft navigation where possible, so they won't necessarily clean up the client until a proper page refresh is performed. I can see this when debugging the examples - I had to go into each one and only when i hit refresh they'd trigger some error. Initializing on the client, and cleaning up on the server, or vice-versa, looks to me like asking for trouble.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, yea I get your point here.
I'll see if I can look into it in the next few days...
Documentation is so lacking that it makes this whole process very painful...

Copy link
Collaborator

@marcjulian marcjulian Apr 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about injecting the platform to check if its the browser and then do the clean up?

private readonly _isBrowser: boolean = inject(Platform).isBrowser;

ngOnDestroy() {
  if(this._isBrowser) {
    ...
  }
}

It's used by the angular team in the components repo too e.g.

https://github.com/angular/components/blob/5a327b6741701a00cd3a409236e3f98a470c3820/src/material/datepicker/calendar-body.ts#L274-L277

// this.mapService.removeControl(this.control);
// }
}
}
139 changes: 73 additions & 66 deletions projects/ngx-maplibre-gl/src/lib/map/map.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
Output,
SimpleChanges,
ViewChild,
afterNextRender,
} from '@angular/core';
import {
AnimationOptions,
Expand Down Expand Up @@ -277,74 +278,80 @@ export class MapComponent

@ViewChild('container', { static: true }) mapContainer: ElementRef;

constructor(private mapService: MapService, private elementRef: ElementRef) {}
constructor(private mapService: MapService, private elementRef: ElementRef) {

afterNextRender(()=>{
if (this.preserveDrawingBuffer) { // This is to allow better interaction with the map state
const htmlElement: HTMLElement = this.elementRef.nativeElement;
htmlElement.setAttribute('data-cy', 'map');
this.subscriptions.push(this.mapLoad.subscribe(() => {
htmlElement.setAttribute('data-loaded', 'true');
}));
this.subscriptions.push(this.idle.subscribe(() => {
htmlElement.setAttribute('data-idle', 'true');
}));
this.subscriptions.push(this.render.subscribe(() => {
htmlElement.removeAttribute('data-idle');
}));
}

this.mapService.setup({
mapOptions: {
collectResourceTiming: this.collectResourceTiming,
container: this.mapContainer.nativeElement,
crossSourceCollisions: this.crossSourceCollisions,
fadeDuration: this.fadeDuration,
minZoom: this.minZoom,
maxZoom: this.maxZoom,
minPitch: this.minPitch,
maxPitch: this.maxPitch,
style: this.style,
hash: this.hash,
interactive: this.interactive,
bearingSnap: this.bearingSnap,
pitchWithRotate: this.pitchWithRotate,
clickTolerance: this.clickTolerance,
attributionControl: this.attributionControl,
logoPosition: this.logoPosition,
failIfMajorPerformanceCaveat: this.failIfMajorPerformanceCaveat,
preserveDrawingBuffer: this.preserveDrawingBuffer,
refreshExpiredTiles: this.refreshExpiredTiles,
maxBounds: this.maxBounds,
scrollZoom: this.scrollZoom,
boxZoom: this.boxZoom,
dragRotate: this.dragRotate,
dragPan: this.dragPan,
keyboard: this.keyboard,
doubleClickZoom: this.doubleClickZoom,
touchPitch: this.touchPitch,
touchZoomRotate: this.touchZoomRotate,
trackResize: this.trackResize,
center: this.center,
zoom: this.zoom,
bearing: this.bearing,
pitch: this.pitch,
renderWorldCopies: this.renderWorldCopies,
maxTileCacheSize: this.maxTileCacheSize,
localIdeographFontFamily: this.localIdeographFontFamily,
transformRequest: this.transformRequest,
bounds: this.bounds ? this.bounds : this.fitBounds,
fitBoundsOptions: this.fitBoundsOptions,
antialias: this.antialias,
locale: this.locale,
cooperativeGestures: this.cooperativeGestures,
terrain: this.terrain,
},
mapEvents: this,
});
if (this.cursorStyle) {
this.mapService.changeCanvasCursor(this.cursorStyle);
}
})

}

ngAfterViewInit() {
if (this.preserveDrawingBuffer) { // This is to allow better interaction with the map state
const htmlElement: HTMLElement = this.elementRef.nativeElement;
htmlElement.setAttribute('data-cy', 'map');
this.subscriptions.push(this.mapLoad.subscribe(() => {
htmlElement.setAttribute('data-loaded', 'true');
}));
this.subscriptions.push(this.idle.subscribe(() => {
htmlElement.setAttribute('data-idle', 'true');
}));
this.subscriptions.push(this.render.subscribe(() => {
htmlElement.removeAttribute('data-idle');
}));
}

this.mapService.setup({
mapOptions: {
collectResourceTiming: this.collectResourceTiming,
container: this.mapContainer.nativeElement,
crossSourceCollisions: this.crossSourceCollisions,
fadeDuration: this.fadeDuration,
minZoom: this.minZoom,
maxZoom: this.maxZoom,
minPitch: this.minPitch,
maxPitch: this.maxPitch,
style: this.style,
hash: this.hash,
interactive: this.interactive,
bearingSnap: this.bearingSnap,
pitchWithRotate: this.pitchWithRotate,
clickTolerance: this.clickTolerance,
attributionControl: this.attributionControl,
logoPosition: this.logoPosition,
failIfMajorPerformanceCaveat: this.failIfMajorPerformanceCaveat,
preserveDrawingBuffer: this.preserveDrawingBuffer,
refreshExpiredTiles: this.refreshExpiredTiles,
maxBounds: this.maxBounds,
scrollZoom: this.scrollZoom,
boxZoom: this.boxZoom,
dragRotate: this.dragRotate,
dragPan: this.dragPan,
keyboard: this.keyboard,
doubleClickZoom: this.doubleClickZoom,
touchPitch: this.touchPitch,
touchZoomRotate: this.touchZoomRotate,
trackResize: this.trackResize,
center: this.center,
zoom: this.zoom,
bearing: this.bearing,
pitch: this.pitch,
renderWorldCopies: this.renderWorldCopies,
maxTileCacheSize: this.maxTileCacheSize,
localIdeographFontFamily: this.localIdeographFontFamily,
transformRequest: this.transformRequest,
bounds: this.bounds ? this.bounds : this.fitBounds,
fitBoundsOptions: this.fitBoundsOptions,
antialias: this.antialias,
locale: this.locale,
cooperativeGestures: this.cooperativeGestures,
terrain: this.terrain,
},
mapEvents: this,
});
if (this.cursorStyle) {
this.mapService.changeCanvasCursor(this.cursorStyle);
}

}

ngOnDestroy() {
Expand Down
56 changes: 56 additions & 0 deletions projects/showcase/server.ts
Original file line number Diff line number Diff line change
@@ -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['SSR_PORT'] || 4000;

// Start up the Node server
const server = app();
server.listen(port, () => {
console.log(`Node Express server listening on http://localhost:${port}`);
});
}

run();
11 changes: 11 additions & 0 deletions projects/showcase/src/app/app.config.server.ts
Original file line number Diff line number Diff line change
@@ -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);
4 changes: 3 additions & 1 deletion projects/showcase/src/app/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import { provideHttpClient } from '@angular/common/http';
import { provideAnimations } from '@angular/platform-browser/animations';
import { provideClientHydration } from '@angular/platform-browser';

export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(),
provideAnimations(),
provideAnimations(),
provideClientHydration()
],
};
7 changes: 6 additions & 1 deletion projects/showcase/src/app/demo/demo-index.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
OnInit,
QueryList,
ViewChildren,
afterNextRender,
} from '@angular/core';
import {
MatSlideToggleChange,
Expand Down Expand Up @@ -90,14 +91,18 @@ export class DemoIndexComponent implements OnInit, AfterViewInit {
Category.CONTROLS_AND_OVERLAYS,
Category.TERRAIN,
];

afterNextRender(() => {
this.scrollInToActiveExampleLink();
})
}

ngOnInit() {
this.routes = this.originalRoutes;
}

ngAfterViewInit() {
this.scrollInToActiveExampleLink();

}

toggleSidenav() {
Expand Down
Loading