Skip to content

Commit 6dff463

Browse files
committed
Add global styles properly in the SSR context
Fixes #18 #48 #96 #407
1 parent fc1f73e commit 6dff463

File tree

16 files changed

+331
-10
lines changed

16 files changed

+331
-10
lines changed

.github/workflows/check.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ jobs:
2525
- run: yarn test:demo
2626
- run: yarn add -D chromedriver@~`google-chrome --version | awk '{print $3}' | awk -F. '{print $1}'`
2727
- run: yarn test:integration
28+
- run: yarn test:integration:ssr
2829
check6:
2930
name: Font Awesome 6
3031
runs-on: ubuntu-latest
@@ -48,3 +49,4 @@ jobs:
4849
- run: yarn test:demo
4950
- run: yarn add -D chromedriver@~`google-chrome --version | awk '{print $3}' | awk -F. '{print $1}'`
5051
- run: yarn test:integration
52+
- run: yarn test:integration:ssr

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,4 @@ Thumbs.db
5252
!/.yarn/plugins
5353
!/.yarn/sdks
5454
!/.yarn/versions
55+
!/.yarn/patches
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
diff --git a/src/builders/protractor/index.js b/src/builders/protractor/index.js
2+
index 34d8f76bac7ece1fcb6d7afd722ec99fad4efccc..1817fe4f635b191b1e3ece423056411e8c0038f0 100755
3+
--- a/src/builders/protractor/index.js
4+
+++ b/src/builders/protractor/index.js
5+
@@ -108,17 +108,7 @@ async function execute(options, context) {
6+
const serverOptions = await context.getTargetOptions(target);
7+
const overrides = {
8+
watch: false,
9+
- liveReload: false,
10+
};
11+
- if (options.host !== undefined) {
12+
- overrides.host = options.host;
13+
- }
14+
- else if (typeof serverOptions.host === 'string') {
15+
- options.host = serverOptions.host;
16+
- }
17+
- else {
18+
- options.host = overrides.host = 'localhost';
19+
- }
20+
server = await context.scheduleTarget(target, overrides);
21+
const result = await server.result;
22+
if (!result.success) {

angular.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,9 @@
141141
"devServerTarget": "demo:serve:production",
142142
"webdriverUpdate": false
143143
},
144-
"development": {
145-
"devServerTarget": "demo:serve:development"
144+
"ssr": {
145+
"devServerTarget": "demo:serve-ssr:production",
146+
"webdriverUpdate": false
146147
}
147148
},
148149
"defaultConfiguration": "production"

docs/guide/adding-css.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Adding CSS
2+
3+
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.
4+
5+
If you have issues with this approach, you can disable it by setting `FaConfig.autoAddCss` to `false`:
6+
7+
```typescript
8+
import { FaConfig } from '@fortawesome/angular-fontawesome';
9+
10+
export class AppComponent {
11+
constructor(faConfig: FaConfig) {
12+
faConfig.autoAddCss = false;
13+
}
14+
}
15+
```
16+
17+
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:
18+
19+
```json
20+
{
21+
"projects": {
22+
"your-project-name": {
23+
"architect": {
24+
"build": {
25+
"options": {
26+
"styles": [
27+
"node_modules/@fortawesome/fontawesome-svg-core/styles.css",
28+
"src/styles.css"
29+
]
30+
}
31+
}
32+
}
33+
}
34+
}
35+
}
36+
```
37+
38+
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.
39+
40+
## Size concerns
41+
42+
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.

docs/upgrading/0.14.0-0.15.0.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,19 @@ Dynamic animation can be achieved by binding the `animation` input to `undefined
1414
## Remove usage of the `styles` and `classes` inputs
1515

1616
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).
17+
18+
## Styles are correctly added in the SSR context
19+
20+
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`:
21+
22+
```typescript
23+
import { FaConfig } from '@fortawesome/angular-fontawesome';
24+
25+
export class AppComponent {
26+
constructor(faConfig: FaConfig) {
27+
faConfig.autoAddCss = false;
28+
}
29+
}
30+
```
31+
32+
Not doing this should not cause any issues, but it will lead to same styles being added twice to the page.

docs/usage.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,4 @@ Guides on specific topics or use cases.
5757
* [Storybook](./guide/storybook.md)
5858
* [Advanced uses](./guide/advanced-uses.md)
5959
* [Styling icon internals](./guide/styling-icon-internals.md)
60+
* [Adding CSS](./guide/adding-css.md)

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"test:schematics": "ts-node --project projects/schematics/tsconfig.json node_modules/.bin/jasmine projects/schematics/src/**/*.spec.ts",
88
"test:demo": "ng test demo --watch=false --browsers=ChromeCI",
99
"test:integration": "ng e2e demo",
10+
"test:integration:ssr": "ng e2e demo --configuration ssr",
1011
"lint": "ng lint",
1112
"start": "ng serve demo",
1213
"start:ssr": "ng run demo:serve-ssr",
@@ -26,7 +27,7 @@
2627
},
2728
"homepage": "https://github.com/FortAwesome/angular-fontawesome",
2829
"devDependencies": {
29-
"@angular-devkit/build-angular": "^17.3.7",
30+
"@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",
3031
"@angular-devkit/core": "^17.3.7",
3132
"@angular-devkit/schematics": "^17.3.7",
3233
"@angular-eslint/builder": "^17.0.0",

projects/demo/e2e/src/app.e2e-spec.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,43 @@
1-
import { browser, logging } from 'protractor';
1+
import { browser, ElementFinder, logging } from 'protractor';
22
import { appPage } from './app.page';
33

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

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

16+
it('should only add styles once', async () => {
17+
const styles: string[] = await appPage.styles.map((style: ElementFinder) => style.getAttribute('innerHTML'));
18+
const fontAwesomeStyles = styles.filter((style) => style.includes('.svg-inline--fa'));
19+
20+
expect(fontAwesomeStyles.length).toBe(1);
21+
});
22+
23+
it('should include styles in the server-side-rendered page', async () => {
24+
const context = await appPage.appRoot.getAttribute('ng-server-context');
25+
if (context !== 'ssr') {
26+
// Skip the test if the page is not server-side rendered.
27+
return;
28+
}
29+
30+
const render1 = await fetch(browser.baseUrl);
31+
const text1 = await render1.text();
32+
expect(text1).toContain('.svg-inline--fa');
33+
34+
// Repeated second time to make sure that second render also includes the styles.
35+
// To achieve it we use WeakSet instead of a simple global variable.
36+
const render2 = await fetch(browser.baseUrl);
37+
const text2 = await render2.text();
38+
expect(text2).toContain('.svg-inline--fa');
39+
});
40+
1541
afterEach(async () => {
1642
// Assert that there are no errors emitted from the browser
1743
const logs = await browser.manage().logs().get(logging.Type.BROWSER);

projects/demo/e2e/src/app.page.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
import { $$, browser } from 'protractor';
1+
import { $, $$, browser } from 'protractor';
22

33
export class AppPage {
44
readonly icons = $$('svg');
5+
readonly styles = $$('style');
6+
7+
readonly appRoot = $('app-root');
58

69
async navigateTo() {
710
await browser.get(browser.baseUrl);

src/lib/config.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Injectable } from '@angular/core';
2+
import { config } from '@fortawesome/fontawesome-svg-core';
23
import { IconDefinition, IconPrefix } from './types';
34

45
@Injectable({ providedIn: 'root' })
@@ -26,4 +27,25 @@ export class FaConfig {
2627
* @default false
2728
*/
2829
fixedWidth?: boolean;
30+
31+
/**
32+
* Automatically add Font Awesome styles to the document when icon is rendered.
33+
*
34+
* For the majority of the cases the automatically added CSS is sufficient,
35+
* please refer to the linked guide for more information on when to disable
36+
* this feature.
37+
*
38+
* @see {@link: https://github.com/FortAwesome/angular-fontawesome/blob/main/docs/guide/adding-css.md}
39+
* @default true
40+
*/
41+
set autoAddCss(value: boolean) {
42+
config.autoAddCss = value;
43+
this._autoAddCss = value;
44+
}
45+
46+
get autoAddCss() {
47+
return this._autoAddCss;
48+
}
49+
50+
private _autoAddCss = true;
2951
}

src/lib/icon/icon.component.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { Component, HostBinding, Input, OnChanges, Optional, SimpleChanges } from '@angular/core';
1+
import { DOCUMENT } from '@angular/common';
2+
import { Component, HostBinding, inject, Input, OnChanges, Optional, SimpleChanges } from '@angular/core';
23
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
34
import {
45
FaSymbol,
@@ -18,6 +19,7 @@ import { faWarnIfIconDefinitionMissing } from '../shared/errors/warn-if-icon-htm
1819
import { faWarnIfIconSpecMissing } from '../shared/errors/warn-if-icon-spec-missing';
1920
import { AnimationProp, FaProps } from '../shared/models/props.model';
2021
import { faClassList } from '../shared/utils/classlist.util';
22+
import { ensureCss } from '../shared/utils/css';
2123
import { faNormalizeIconSpec } from '../shared/utils/normalize-icon-spec.util';
2224
import { FaStackItemSizeDirective } from '../stack/stack-item-size.directive';
2325
import { FaStackComponent } from '../stack/stack.component';
@@ -71,6 +73,8 @@ export class FaIconComponent implements OnChanges {
7173

7274
@HostBinding('innerHTML') renderedIconHTML: SafeHtml;
7375

76+
private document = inject(DOCUMENT);
77+
7478
constructor(
7579
private sanitizer: DomSanitizer,
7680
private config: FaConfig,
@@ -96,6 +100,7 @@ export class FaIconComponent implements OnChanges {
96100
const iconDefinition = this.findIconDefinition(this.icon ?? this.config.fallbackIcon);
97101
if (iconDefinition != null) {
98102
const params = this.buildParams();
103+
ensureCss(this.document);
99104
const renderedIcon = icon(iconDefinition, params);
100105
this.renderedIconHTML = this.sanitizer.bypassSecurityTrustHtml(renderedIcon.html.join('\n'));
101106
}

src/lib/layers/layers-counter.component.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import { Component, HostBinding, Input, OnChanges, Optional, SimpleChanges } from '@angular/core';
1+
import { DOCUMENT } from '@angular/common';
2+
import { Component, HostBinding, inject, Input, OnChanges, Optional, SimpleChanges } from '@angular/core';
23
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
34
import { counter, CounterParams } from '@fortawesome/fontawesome-svg-core';
45
import { faWarnIfParentNotExist } from '../shared/errors/warn-if-parent-not-exist';
6+
import { ensureCss } from '../shared/utils/css';
57
import { FaLayersComponent } from './layers.component';
68

79
@Component({
@@ -19,6 +21,8 @@ export class FaLayersCounterComponent implements OnChanges {
1921

2022
@HostBinding('innerHTML') renderedHTML: SafeHtml;
2123

24+
private document = inject(DOCUMENT);
25+
2226
constructor(
2327
@Optional() private parent: FaLayersComponent,
2428
private sanitizer: DomSanitizer,
@@ -41,6 +45,7 @@ export class FaLayersCounterComponent implements OnChanges {
4145
}
4246

4347
private updateContent(params: CounterParams) {
48+
ensureCss(this.document);
4449
this.renderedHTML = this.sanitizer.bypassSecurityTrustHtml(counter(this.content || '', params).html.join(''));
4550
}
4651
}

src/lib/layers/layers-text.component.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { Component, HostBinding, Input, OnChanges, Optional, SimpleChanges } from '@angular/core';
1+
import { DOCUMENT } from '@angular/common';
2+
import { Component, HostBinding, inject, Input, OnChanges, Optional, SimpleChanges } from '@angular/core';
23
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
34
import {
45
FlipProp,
@@ -13,6 +14,7 @@ import {
1314
import { faWarnIfParentNotExist } from '../shared/errors/warn-if-parent-not-exist';
1415
import { FaProps } from '../shared/models/props.model';
1516
import { faClassList } from '../shared/utils/classlist.util';
17+
import { ensureCss } from '../shared/utils/css';
1618
import { FaLayersComponent } from './layers.component';
1719

1820
@Component({
@@ -37,6 +39,8 @@ export class FaLayersTextComponent implements OnChanges {
3739

3840
@HostBinding('innerHTML') renderedHTML: SafeHtml;
3941

42+
private document = inject(DOCUMENT);
43+
4044
constructor(
4145
@Optional() private parent: FaLayersComponent,
4246
private sanitizer: DomSanitizer,
@@ -75,6 +79,7 @@ export class FaLayersTextComponent implements OnChanges {
7579
}
7680

7781
private updateContent(params: TextParams) {
82+
ensureCss(this.document);
7883
this.renderedHTML = this.sanitizer.bypassSecurityTrustHtml(text(this.content || '', params).html.join('\n'));
7984
}
8085
}

src/lib/shared/utils/css.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { config, dom } from '@fortawesome/fontawesome-svg-core';
2+
3+
const cssInserted = new WeakSet();
4+
export const autoCssId = 'fa-auto-css';
5+
6+
/**
7+
* Ensure that Font Awesome CSS is inserted into the page.
8+
*
9+
* SVG Core has the same logic to insert the same styles into the page, however
10+
* it's not aware of Angular SSR and therefore styles won't be added in that
11+
* context leading to https://github.com/FortAwesome/angular-fontawesome/issues/48.
12+
* That's why the same logic is duplicated here.
13+
*
14+
* @param document - Document.
15+
*/
16+
export function ensureCss(document: Document): void {
17+
if (cssInserted.has(document)) {
18+
return;
19+
}
20+
21+
// Prevent adding the same styles again after hydration.
22+
if (document.getElementById(autoCssId) != null) {
23+
config.autoAddCss = false;
24+
cssInserted.add(document);
25+
return;
26+
}
27+
28+
const style = document.createElement('style');
29+
style.setAttribute('type', 'text/css');
30+
style.setAttribute('id', autoCssId);
31+
style.innerHTML = dom.css();
32+
const headChildren = document.head.childNodes;
33+
let beforeChild = null;
34+
35+
for (let i = headChildren.length - 1; i > -1; i--) {
36+
const child = headChildren[i];
37+
const tagName = child.nodeName.toUpperCase();
38+
39+
if (['STYLE', 'LINK'].indexOf(tagName) > -1) {
40+
beforeChild = child;
41+
}
42+
}
43+
44+
document.head.insertBefore(style, beforeChild);
45+
46+
// Prevent SVG Core from adding the same styles.
47+
//
48+
// As the logic is present in two places and SVG Core is not aware about
49+
// this library, it may lead to styles being added twice. This can only
50+
// occur when icon is rendered by SVG Core before the Angular component
51+
// and should not have any significant negative impact. This is a rare
52+
// use case, and it's tricky to prevent, so we accept this behavior. Consumer
53+
// can choose to disable `FaConfig.autoAddCss` and add styles manually to
54+
// prevent this from happening.
55+
config.autoAddCss = false;
56+
cssInserted.add(document);
57+
}

0 commit comments

Comments
 (0)