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

Add global styles properly in the SSR context #443

Merged
merged 3 commits into from
May 15, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ jobs:
- run: yarn test:demo
- run: yarn add -D chromedriver@~`google-chrome --version | awk '{print $3}' | awk -F. '{print $1}'`
- run: yarn test:integration
- run: yarn test:integration:ssr
check6:
name: Font Awesome 6
runs-on: ubuntu-latest
Expand All @@ -48,3 +49,4 @@ jobs:
- run: yarn test:demo
- run: yarn add -D chromedriver@~`google-chrome --version | awk '{print $3}' | awk -F. '{print $1}'`
- run: yarn test:integration
- run: yarn test:integration:ssr
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,4 @@ Thumbs.db
!/.yarn/plugins
!/.yarn/sdks
!/.yarn/versions
!/.yarn/patches
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
diff --git a/src/builders/protractor/index.js b/src/builders/protractor/index.js
index 34d8f76bac7ece1fcb6d7afd722ec99fad4efccc..6ec016ee9d1a04ba0b1dc4742340a2cbe2b9b80f 100755
--- a/src/builders/protractor/index.js
+++ b/src/builders/protractor/index.js
@@ -108,17 +108,7 @@ async function execute(options, context) {
const serverOptions = await context.getTargetOptions(target);
const overrides = {
watch: false,
- liveReload: false,
};
- if (options.host !== undefined) {
- overrides.host = options.host;
- }
- else if (typeof serverOptions.host === 'string') {
- options.host = serverOptions.host;
- }
- else {
- options.host = overrides.host = 'localhost';
- }
if (options.port !== undefined) {
overrides.port = options.port;
}
61 changes: 59 additions & 2 deletions angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/demo",
"outputPath": "dist/demo/browser",
"index": "projects/demo/src/index.html",
"main": "projects/demo/src/main.ts",
"polyfills": ["zone.js"],
Expand Down Expand Up @@ -141,8 +141,65 @@
"devServerTarget": "demo:serve:production",
"webdriverUpdate": false
},
"ssr": {
"devServerTarget": "demo:serve-ssr:production",
"webdriverUpdate": false
}
},
"defaultConfiguration": "production"
},
"server": {
"builder": "@angular-devkit/build-angular:server",
"options": {
"outputPath": "dist/demo/server",
"main": "projects/demo/server.ts",
"tsConfig": "projects/demo/tsconfig.server.json",
"buildOptimizer": false,
"optimization": false,
"sourceMap": true,
"extractLicenses": false,
"vendorChunk": true
},
"configurations": {
"production": {
"buildOptimizer": true,
"outputHashing": "media",
"optimization": true,
"sourceMap": false,
"extractLicenses": true,
"vendorChunk": false
},
"development": {}
},
"defaultConfiguration": "production"
},
"serve-ssr": {
"builder": "@angular-devkit/build-angular:ssr-dev-server",
"configurations": {
"development": {
"browserTarget": "demo:build:development",
"serverTarget": "demo:server:development"
},
"production": {
"browserTarget": "demo:build:production",
"serverTarget": "demo:server:production"
}
},
"defaultConfiguration": "development"
},
"prerender": {
"builder": "@angular-devkit/build-angular:prerender",
"options": {
"routes": ["/"]
},
"configurations": {
"production": {
"browserTarget": "demo:build:production",
"serverTarget": "demo:server:production"
},
"development": {
"devServerTarget": "demo:serve:development"
"browserTarget": "demo:build:development",
"serverTarget": "demo:server:development"
}
},
"defaultConfiguration": "production"
Expand Down
42 changes: 42 additions & 0 deletions docs/guide/adding-css.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Adding CSS

For Font Awesome icon to render properly, it needs global Font Awesome styles to be added to the page. By default, the library will automatically add the necessary styles to the page before rendering an icon.

If you have issues with this approach, you can disable it by setting `FaConfig.autoAddCss` to `false`:

```typescript
import { FaConfig } from '@fortawesome/angular-fontawesome';

export class AppComponent {
constructor(faConfig: FaConfig) {
faConfig.autoAddCss = false;
}
}
```

And instead add the styles manually to your application. You can find the necessary styles in the `node_modules/@fortawesome/fontawesome-svg-core/styles.css` file. Then add them to the application global styles in the `angular.json` file:

```json
{
"projects": {
"your-project-name": {
"architect": {
"build": {
"options": {
"styles": [
"node_modules/@fortawesome/fontawesome-svg-core/styles.css",
"src/styles.css"
]
}
}
}
}
}
}
```

One common case when this is necessary is when using Shadow DOM. Angular includes [certain non-trivial logic](https://angular.io/guide/view-encapsulation#mixing-encapsulation-modes) to ensure that global styles work as expected inside the shadow root which can't be applied when styles are added automatically.

## Size concerns

If you are concerned about the size of the Font Awesome global styles, you may extract only the necessary styles from the `node_modules/@fortawesome/fontawesome-svg-core/styles.css` file and add them instead. This way, you can reduce the size of the global styles to only what is necessary for your application. But be aware that this is not officially supported and may break with future updates to Font Awesome. Make sure to revisit the manually extracted styles every time the library is updated or a new Font Awesome feature is used.
16 changes: 16 additions & 0 deletions docs/upgrading/0.14.0-0.15.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,19 @@ Dynamic animation can be achieved by binding the `animation` input to `undefined
## Remove usage of the `styles` and `classes` inputs

Previously deprecated `styles` and `classes` inputs in all components were removed. These inputs don't work the way one would expect and cause a lot of confusion. For the majority of the cases, one should use regular [class and style bindings](https://angular.io/guide/class-binding) provided by Angular. For those rare cases, when it is not enough, there is a guide on how one can style component's internal elements at their own risk - [Styling icon internals](https://github.com/FortAwesome/angular-fontawesome/blob/master/docs/guide/styling-icon-internals.md).

## Styles are correctly added in the SSR context

Previously, the library didn't correctly add global styles in the SSR context. If you have added global styles to your application to work around issues like [#407](https://github.com/FortAwesome/angular-fontawesome/issues/407), [#18](https://github.com/FortAwesome/angular-fontawesome/issues/18) or [#48](https://github.com/FortAwesome/angular-fontawesome/issues/48), you can either remove the workaround or alternatively, disable automatic styles injection by setting `FaConfig.autoAddCss` to `false`:

```typescript
import { FaConfig } from '@fortawesome/angular-fontawesome';

export class AppComponent {
constructor(faConfig: FaConfig) {
faConfig.autoAddCss = false;
}
}
```

Not doing this should not cause any issues, but it will lead to same styles being added twice to the page.
1 change: 1 addition & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,4 @@ Guides on specific topics or use cases.
* [Storybook](./guide/storybook.md)
* [Advanced uses](./guide/advanced-uses.md)
* [Styling icon internals](./guide/styling-icon-internals.md)
* [Adding CSS](./guide/adding-css.md)
31 changes: 20 additions & 11 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
"test:schematics": "ts-node --project projects/schematics/tsconfig.json node_modules/.bin/jasmine projects/schematics/src/**/*.spec.ts",
"test:demo": "ng test demo --watch=false --browsers=ChromeCI",
"test:integration": "ng e2e demo",
"test:integration:ssr": "ng e2e demo --configuration ssr",
"lint": "ng lint",
"start": "ng serve demo",
"start:ssr": "ng run demo:serve-ssr",
"build": "ng build angular-fontawesome",
"build:watch": "ng build angular-fontawesome -c development --watch",
"build:schematics": "tsc -p projects/schematics/tsconfig.json && ts-node -O '{\"module\":\"commonjs\"}' tasks/build-schematics.ts",
Expand All @@ -25,35 +27,42 @@
},
"homepage": "https://github.com/FortAwesome/angular-fontawesome",
"devDependencies": {
"@angular-devkit/build-angular": "^17.0.0",
"@angular-devkit/core": "^17.0.0",
"@angular-devkit/schematics": "^17.0.0",
"@angular-devkit/build-angular": "patch:@angular-devkit/build-angular@npm%3A17.3.7#~/.yarn/patches/@angular-devkit-build-angular-npm-17.3.7-60e65bd832.patch",
"@angular-devkit/core": "^17.3.7",
"@angular-devkit/schematics": "^17.3.7",
"@angular-eslint/builder": "^17.0.0",
"@angular-eslint/eslint-plugin": "^17.0.0",
"@angular-eslint/eslint-plugin-template": "^17.0.0",
"@angular-eslint/schematics": "17.0.0",
"@angular-eslint/template-parser": "^17.0.0",
"@angular/cli": "^17.0.0",
"@angular/common": "^17.0.0",
"@angular/compiler": "^17.0.0",
"@angular/compiler-cli": "^17.0.0",
"@angular/core": "^17.0.0",
"@angular/language-service": "^17.0.0",
"@angular/platform-browser": "^17.0.0",
"@angular/platform-browser-dynamic": "^17.0.0",
"@angular/animations": "^17.3.8",
"@angular/cli": "^17.3.7",
"@angular/common": "^17.3.8",
"@angular/compiler": "^17.3.8",
"@angular/compiler-cli": "^17.3.8",
"@angular/core": "^17.3.8",
"@angular/language-service": "^17.3.8",
"@angular/platform-browser": "^17.3.8",
"@angular/platform-browser-dynamic": "^17.3.8",
"@angular/platform-server": "^17.0.0",
"@angular/router": "^17.0.0",
"@angular/ssr": "^17.0.0",
"@fortawesome/fontawesome-svg-core": "^6.4.2",
"@fortawesome/free-regular-svg-icons": "^6.4.2",
"@fortawesome/free-solid-svg-icons": "^6.4.2",
"@types/express": "^4.17.17",
"@types/jasmine": "~4.3.0",
"@types/node": "~20.9.0",
"@typescript-eslint/eslint-plugin": "^6.10.0",
"@typescript-eslint/parser": "^6.10.0",
"browser-sync": "^3.0.0",
"chromedriver": "~123.0.1",
"eslint": "^8.53.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-import": "2.29.0",
"eslint-plugin-jsdoc": "^46.8.2",
"eslint-plugin-prefer-arrow": "1.2.3",
"express": "^4.18.2",
"jasmine-core": "~4.5.0",
"jasmine-spec-reporter": "~7.0.0",
"karma": "~6.4.2",
Expand Down
1 change: 1 addition & 0 deletions projects/demo/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"parserOptions": {
"project": [
"projects/demo/tsconfig.app.json",
"projects/demo/tsconfig.server.json",
"projects/demo/tsconfig.spec.json",
"projects/demo/e2e/tsconfig.json"
],
Expand Down
28 changes: 27 additions & 1 deletion projects/demo/e2e/src/app.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,43 @@
import { browser, logging } from 'protractor';
import { browser, ElementFinder, logging } from 'protractor';
import { appPage } from './app.page';

describe('Angular FontAwesome demo', () => {
beforeEach(async () => {
// TODO: Migrate off Protractor as wait for Angular does not seem to work in the standalone mode
browser.waitForAngularEnabled(false);
await appPage.navigateTo();
await browser.sleep(1000);
});

it('should render all icons', async () => {
expect(await appPage.icons.count()).toBe(46);
});

it('should only add styles once', async () => {
const styles: string[] = await appPage.styles.map((style: ElementFinder) => style.getAttribute('innerHTML'));
const fontAwesomeStyles = styles.filter((style) => style.includes('.svg-inline--fa'));

expect(fontAwesomeStyles.length).toBe(1);
});

it('should include styles in the server-side-rendered page', async () => {
const context = await appPage.appRoot.getAttribute('ng-server-context');
if (context !== 'ssr') {
// Skip the test if the page is not server-side rendered.
return;
}

const render1 = await fetch(browser.baseUrl);
const text1 = await render1.text();
expect(text1).toContain('.svg-inline--fa');

// Repeated second time to make sure that second render also includes the styles.
// To achieve it we use WeakSet instead of a simple global variable.
const render2 = await fetch(browser.baseUrl);
const text2 = await render2.text();
expect(text2).toContain('.svg-inline--fa');
});

afterEach(async () => {
// Assert that there are no errors emitted from the browser
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
Expand Down
5 changes: 4 additions & 1 deletion projects/demo/e2e/src/app.page.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { $$, browser } from 'protractor';
import { $, $$, browser } from 'protractor';

export class AppPage {
readonly icons = $$('svg');
readonly styles = $$('style');

readonly appRoot = $('app-root');

async navigateTo() {
await browser.get(browser.baseUrl);
Expand Down
72 changes: 72 additions & 0 deletions projects/demo/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import 'zone.js/node';

import { APP_BASE_HREF } from '@angular/common';
import { CommonEngine } from '@angular/ssr';
import * as express from 'express';
import { existsSync } from 'node:fs';
import { join } 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 distFolder = join(process.cwd(), 'dist/demo/browser');
const indexHtml = existsSync(join(distFolder, 'index.original.html'))
? join(distFolder, 'index.original.html')
: join(distFolder, 'index.html');

const commonEngine = new CommonEngine();

server.set('view engine', 'html');
server.set('views', distFolder);

// Example Express Rest API endpoints
// server.get('/api/**', (req, res) => { });
// Serve static files from /browser
server.get(
'*.*',
express.static(distFolder, {
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: distFolder,
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}`);
});
}

// Webpack will replace 'require' with '__webpack_require__'
// '__non_webpack_require__' is a proxy to Node 'require'
// The below code is to ensure that the server is run only when not requiring the bundle.
declare const __non_webpack_require__: NodeRequire;
const mainModule = __non_webpack_require__.main;
const moduleFilename = (mainModule && mainModule.filename) || '';
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
run();
}

export default bootstrap;
9 changes: 9 additions & 0 deletions projects/demo/src/app/app.config.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
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);
Loading
Loading