Skip to content

Commit 5d26d6d

Browse files
committed
Add global styles properly in the SSR context
Fixes #18 #48 #96 #407
1 parent 551ccb2 commit 5d26d6d

File tree

17 files changed

+357
-11
lines changed

17 files changed

+357
-11
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..6ec016ee9d1a04ba0b1dc4742340a2cbe2b9b80f 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+
if (options.port !== undefined) {
21+
overrides.port = options.port;
22+
}

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, this.config);
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: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
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';
5+
import { FaConfig } from '../config';
46
import { faWarnIfParentNotExist } from '../shared/errors/warn-if-parent-not-exist';
7+
import { ensureCss } from '../shared/utils/css';
58
import { FaLayersComponent } from './layers.component';
69

710
@Component({
@@ -19,6 +22,9 @@ export class FaLayersCounterComponent implements OnChanges {
1922

2023
@HostBinding('innerHTML') renderedHTML: SafeHtml;
2124

25+
private document = inject(DOCUMENT);
26+
private config = inject(FaConfig);
27+
2228
constructor(
2329
@Optional() private parent: FaLayersComponent,
2430
private sanitizer: DomSanitizer,
@@ -41,6 +47,7 @@ export class FaLayersCounterComponent implements OnChanges {
4147
}
4248

4349
private updateContent(params: CounterParams) {
50+
ensureCss(this.document, this.config);
4451
this.renderedHTML = this.sanitizer.bypassSecurityTrustHtml(counter(this.content || '', params).html.join(''));
4552
}
4653
}

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

Lines changed: 8 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,
@@ -10,9 +11,11 @@ import {
1011
TextParams,
1112
Transform,
1213
} from '@fortawesome/fontawesome-svg-core';
14+
import { FaConfig } from '../config';
1315
import { faWarnIfParentNotExist } from '../shared/errors/warn-if-parent-not-exist';
1416
import { FaProps } from '../shared/models/props.model';
1517
import { faClassList } from '../shared/utils/classlist.util';
18+
import { ensureCss } from '../shared/utils/css';
1619
import { FaLayersComponent } from './layers.component';
1720

1821
@Component({
@@ -37,6 +40,9 @@ export class FaLayersTextComponent implements OnChanges {
3740

3841
@HostBinding('innerHTML') renderedHTML: SafeHtml;
3942

43+
private document = inject(DOCUMENT);
44+
private config = inject(FaConfig);
45+
4046
constructor(
4147
@Optional() private parent: FaLayersComponent,
4248
private sanitizer: DomSanitizer,
@@ -75,6 +81,7 @@ export class FaLayersTextComponent implements OnChanges {
7581
}
7682

7783
private updateContent(params: TextParams) {
84+
ensureCss(this.document, this.config);
7885
this.renderedHTML = this.sanitizer.bypassSecurityTrustHtml(text(this.content || '', params).html.join('\n'));
7986
}
8087
}

src/lib/layers/layers.component.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
1-
import { Component, ElementRef, HostBinding, Input, OnChanges, OnInit, Renderer2, SimpleChanges } from '@angular/core';
1+
import { DOCUMENT } from '@angular/common';
2+
import {
3+
Component,
4+
ElementRef,
5+
HostBinding,
6+
inject,
7+
Input,
8+
OnChanges,
9+
OnInit,
10+
Renderer2,
11+
SimpleChanges,
12+
} from '@angular/core';
213
import { SizeProp } from '@fortawesome/fontawesome-svg-core';
314
import { FaConfig } from '../config';
15+
import { ensureCss } from '../shared/utils/css';
416

517
/**
618
* Fontawesome layers.
@@ -15,6 +27,8 @@ export class FaLayersComponent implements OnInit, OnChanges {
1527

1628
@Input() @HostBinding('class.fa-fw') fixedWidth?: boolean;
1729

30+
private document = inject(DOCUMENT);
31+
1832
constructor(
1933
private renderer: Renderer2,
2034
private elementRef: ElementRef,
@@ -23,6 +37,7 @@ export class FaLayersComponent implements OnInit, OnChanges {
2337

2438
ngOnInit() {
2539
this.renderer.addClass(this.elementRef.nativeElement, 'fa-layers');
40+
ensureCss(this.document, this.config);
2641
this.fixedWidth = typeof this.fixedWidth === 'boolean' ? this.fixedWidth : this.config.fixedWidth;
2742
}
2843

0 commit comments

Comments
 (0)