希望是最好懂的 Angular 組件樣式介紹

Ray
23 min readAug 5, 2023
ImgSrc

本文內容

  1. #元數據介紹 (@Component、Metadata)
  2. #組件樣式介紹 (樣式模組化範例)
  3. #樣式封裝 (Emulated , ShadowDom ,None)
  4. #特殊選擇器 (:host, ::ng-deep, :host-context)

#元數據介紹

透過指令建立一個組件,觀察 @Component(裝飾器) 中的內容 (Metadata)

組件的元數據告訴 Angular 從哪裡獲取創建和呈現組件及其視圖所需的資料,將組件及其範本組合起來形成畫面上我們看到的視圖。

ng g c styleExample
// 會建立出四個檔案分別是 :
// 1.style-example.component.html
// 2.style-example.component.scss
// 3.style-example.component.spec.ts
// 4.style-example.component.ts
// style-example.component.ts

import { Component } from '@angular/core';

@Component({
selector: 'app-style-example',
templateUrl: './style-example.component.html',
styleUrls: ['./style-example.component.scss']
})
export class StyleExampleComponent {

}

透過 Angular指令建立出 Component 後,會發現 Angular 已經自己幫你輸入 HTML 範本的相對位置(./style-example.component.html)以及樣式表的相對位置(./style-example.component.scss)

@Component(裝飾器)中的內容可以理解為:當其他 HTML 範本上使用 <app-style-example> 這個選擇器,Angular 就會給他對應到selector的組件 (StyleExampleComponent),也就是結合以下三個檔案的內容:

templateUrl 上的範本 (style-example.component.html)

stylesUrls上的樣式表 (style-example.component.scss)

組件所需要的相關邏輯(style-example.component.ts)

接著我們來聊聊 stylesUrls,也就是負責導出樣式表的部分。 stylesUrls 的格式是 Array,那是否意味著可以導入多個 scss 檔案?答案是沒錯,一個視圖允許多個 scss 檔案來源。另外,樣式也不一定需要透過指定 scss 檔案的相對路徑來導入,除了預設的輸入 scss 檔案來源外,以下有幾種其他方式:

  • 透過 styles 加在 metadata 中
import { Component } from '@angular/core';

@Component({
selector: 'app-style-example',
templateUrl: './style-example.component.html',
styles: ['h1 {color:red}']
})

export class StyleExampleComponent {

}
  • 寫在 template 中
import { Component } from '@angular/core';

@Component({
selector: 'app-style-example',
template: '<style> h1 {color:red} </style> '<h1>h1</h1>',
styleUrls: ['./style-example.component.scss']
})
export class StyleExampleComponent {

}
  • template link tags
import { Component } from '@angular/core';

@Component({
selector: 'app-style-example',
template: `
<link rel="stylesheet" href="../assets/example.scss">
<h1>h1</h1>
`,
styleUrls: ['./style-example.component.scss']
})

export class StyleExampleComponent {

}
  • 寫在 scss 檔案中使用 import
@import './header.css';

#組件樣式介紹

稍微介紹完透過 Angular 指令產生 component 中裝飾器的原數據後讓我們回到主題:組件樣式元件的樣式表 ( 元數據下的styles )

在 ​@Component​ 中指定的樣式只會對該元件的範本生效,也就是 父元件 與 子元件的樣式表不會互相汙染

舉例來說,將父元件的 <H1> 字樣顏色設為紅色,將子元件的 <H1> 字樣設為藍色;在雙方為父子關係且都有對 <H1> 的樣式做調整,觀察輸出結果

// 父元件

import { Component } from '@angular/core';

@Component({
selector: 'app-style-example',
template: `
<h1>Parent H1</h1>
<app-style-example-child></app-style-example-child>
`
,
styles: ['h1 {color: red;}']
})
export class StyleExampleComponent {

}
// 子元件

import { Component } from '@angular/core';

@Component({
selector: 'app-style-example-child',
template: '<h1>Child H1</h1>',
styles: ['h1 {color: blue;}']
})

export class StyleExampleChildComponent {

}

會發現父子元件的樣式表並不會互相影響,也不會影響專案內其他使用到的 <H1> 的樣式。

專案中可能在多個組件中都會對同一個元素或是 class 的樣式做調整,但是我們不會希望在 A 組件調整了一個樣式結果不小心連 B 組件也一起動到,這會造成維護不易,樣式模組化的好處之一就是樣式表隔離。

那 Angular 是如何做到樣式表隔離的呢?首先打開網頁檢查觀察元素的內容

看下 <h1> 元素上被加上了 "_ngcontent-uyu-c21",但於我們的程式中並沒有加上這個屬性,為什麼會多出這個屬性?這是因為 Angular 會替每個元件加上獨有的屬性,讓選擇器只會選到當前元件的元素,用以防止互相汙染。

#樣式封裝

上面組件樣式介紹中提到樣式模組化的特性,對於不同樣式表對同一個元素進行修改,為何能在不同 Component 中不互相影響?那若想讓子層繼承父親的樣式又應該如何設定?這邊就是接下來要講的樣式封裝

Angular 總共提供了三種樣式封裝的選項在裝飾器中可選,分別為:Emulated、ShawdowDom、None

  1. Emulated (預設) [ Emulated 可以理解為:只進 & 不出]

只進 ( 在全局樣式styles.scss中修改 h1的樣式,會影響到 Component )

// styles.scss
h1 {
color: green
}
// component
import { Component, ViewEncapsulation } from '@angular/core';

@Component({
selector: 'app-style-example',
template: '<h1>H1</h1>',
styles: [],
encapsulation: ViewEncapsulation.Emulated
})
export class StyleExampleComponent {

}

不出 ( parent 將 <h1> 樣式設為了紅色,不影響其他組件的 <h1> 樣式顏色 )

// parent
import { Component, ViewEncapsulation } from '@angular/core';

@Component({
selector: 'app-style-example',
template: '<h1>Parent H1</h1> <app-style-example-child></app-style-example-child>',
styles: ['h1 {color: red;}'],
encapsulation: ViewEncapsulation.Emulated
})
export class StyleExampleComponent {

}

// child
import { Component, ViewEncapsulation } from '@angular/core';

@Component({
selector: 'app-style-example-child',
template: '<h1>Child H1</h1>',
styles: [],
encapsulation: ViewEncapsulation.Emulated
})
export class StyleExampleChildComponent {

}

小結:Emulated 會吃到全局樣式,並且不將樣式傳給子層、兄弟層。

2. ShadowDom [ ShadowDom 可以理解為:不進 & 不出]

不進 ( 在全局樣式styles.scss中修改 h1的樣式,不會影響到 Component )

// styles.scss
h1 {
color: green
}
import { Component, ViewEncapsulation } from '@angular/core';

@Component({
selector: 'app-style-example',
template: '<h1>H1</h1>',
styles: [],
encapsulation: ViewEncapsulation.ShadowDom
})
export class StyleExampleComponent {

}
組件不受到全局樣式 (styles.scss) 的影響

不出 ( 將 <h1> 樣式設為了紅色,不影響其他同層級組件的 <h1> 樣式顏色 )

// showdowDom 
import { Component, ViewEncapsulation } from '@angular/core';

@Component({
selector: 'app-style-example',
template: '<h1>H1</h1>',
styles: ['h1 {color: red;}'],
encapsulation: ViewEncapsulation.ShadowDom
})
export class StyleExampleComponent {

}
// 同層級元素 (無設置 h1 樣式)
import { Component } from '@angular/core';

@Component({
selector: 'app-style-example-brother',
template: '<h1>Brother H1</h1>',
styles: []
})

export class StyleExampleBrotherComponent {

}
// app.component.html
<app-style-example></app-style-example>
<app-style-example-brother></app-style-example-brother>
不影響同層級樣式

但是,ShadowDom雖然不影響同層級的組件,卻會影響該組件下所有子層級樣式。舉例來說,目前的組件有四個,分別是:祖父、父親、子孫、曾子孫。在父親層使用 ShadowDom,則往下所有組件都會繼承父親層的樣式表

// 父親層

import { Component, ViewEncapsulation } from '@angular/core';

@Component({
selector: 'app-style-example',
template: '<h1>Parent H1</h1> <app-style-example-child></app-style-example-child>',
styles: ['h1 {color: red;}'],
encapsulation: ViewEncapsulation.ShadowDom
})
export class StyleExampleComponent {

}
// 祖父
@Component({
selector: 'app-style-example-grandparent',
template: '<h1>GrandParent H1</h1> <app-style-example></app-style-example>',
styleUrls: ['./style-example-grandparent.component.scss']
})

// 子孫
@Component({
selector: 'app-style-example-child',
template: '<h1>Child H1</h1> <app-style-example-grandson></app-style-example-grandson>',
styles: []
})

// 曾子孫
@Component({
selector: 'app-style-example-grandson',
template: '<h1>Grandson H1</h1> ',
styleUrls: ['./style-example-grandson.component.scss']
})
Shawdow Dom

小結:Shawdow Dom 不會吃到全局樣式,並且不將樣式傳給兄弟層,但所有子層皆會繼承樣式。

不過!若是子層的樣式表同樣有對父層的調整過的元素重複設定,那麼子層的樣式仍然會以子層的設定為主。

// 父層
import { Component, ViewEncapsulation } from '@angular/core';

@Component({
selector: 'app-style-example',
template: '<h1>Parent H1</h1> <app-style-example-child></app-style-example-child>',
styles: ['h1 {color: red;}'],
encapsulation: ViewEncapsulation.ShadowDom
})
export class StyleExampleComponent {

}

// 子層
import { Component } from '@angular/core';

@Component({
selector: 'app-style-example-child',
template: '<h1>Child H1</h1>',
styles: ['h1 {color: blue;}']
})
export class StyleExampleChildComponent {

}
雖父層啟用 shadowDom 但子層仍會以子層樣式表為優先

3. None [ None 可以理解為:能進 & 能出]

能進 ( 在全局樣式styles.scss中修改 h1的樣式,會影響到 Component )

h1 {
color: green
}
import { Component, ViewEncapsulation } from '@angular/core';

@Component({
selector: 'app-style-example',
template: '<h1>H1</h1>',
styles: [],
encapsulation: ViewEncapsulation.None
})
export class StyleExampleComponent {

}
受到全局樣式 (Styles.scss) 影響

會出 ( 不論兄弟層或子層,皆會受到本樣式表影響 )

// 父親

import { Component, ViewEncapsulation } from '@angular/core';

@Component({
selector: 'app-style-example',
template: '<h1>Parent H1</h1> <app-style-example-child></app-style-example-child>',
styles: ['h1 {color: red;}'],
encapsulation: ViewEncapsulation.None
})
export class StyleExampleComponent {

}
// app.component.html
<app-style-example></app-style-example>
<app-style-example-brother></app-style-example-brother>
兄弟層與子層都受到 encapsulation為ViewEncapsulation.None 的組件樣式影響

#特殊選擇器

:host

(表示選擇當前的組件)

// 沒有使用 :host

import { Component, ViewEncapsulation } from '@angular/core';

@Component({
selector: 'app-style-example',
template: `
<h1>Parent H1</h1>
`
,
styles: [`
h1{ color:red; }
`
]
})
export class StyleExampleComponent {

}
透過網頁檢查查看屬性樣式
// 使用 :host
import { Component, ViewEncapsulation } from '@angular/core';

@Component({
selector: 'app-style-example',
template: `
<h1>Parent H1</h1>
`
,
styles: [`
:host h1{ color:red; }
`
]
})
export class StyleExampleComponent {

}

可以發現加上 :host 後,屬性上從原先的

h1[_ngcontent-qid-c20]

被修改為

[_nghost-qid-c20] h1[_ngcontent-qid-c20]

這表示只選取當前組件的 DOM,那這樣有什麼用意?我們接著看ng-deep

::ng-deep

(忽略的層級關係,直接找到並修改所有符合的 className)

現在有兩個組件分別是父組件以及子組件,分別有一個 <h1>;觀察子組件修改 h1 會發生什麼事

// 父組件
import { Component, ViewEncapsulation } from '@angular/core';

@Component({
selector: 'app-style-example',
template: `
<h1>Parent H1</h1>
<app-style-example-child></app-style-example-child>
`
,
styles: [`
`
]
})
export class StyleExampleComponent {

}

// 子組件
import { Component } from '@angular/core';

@Component({
selector: 'app-style-example-child',
template: '<h1>Child H1</h1>',
styles: [`
::ng-deep h1{
color:blue;
}
`
]
})
export class StyleExampleChildComponent {

}
會發現子組件修改h1 但不影響父層的h1

接著加上 ng-deep 再修改一次 <h1> 後觀察結果

// Child

import { Component } from '@angular/core';

@Component({
selector: 'app-style-example-child',
template: '<h1>Child H1</h1>',
styles: [`
::ng-deep h1{
color:blue;
}
`]
})
export class StyleExampleChildComponent {

}
會發現父組件的 <h1> 也受到影響

我們再加上一個 Brother 組件,是與 Child同層 以及一個 Grandson 組件是包在 Child 組件底下,看 Child 組件的 ng-deep 會不會影響到 Brother 組件 與 Grandson 組件

// Child 組件

import { Component } from '@angular/core';

@Component({
selector: 'app-style-example-child',
template: '<h1>Child H1</h1> <app-style-example-grandson></app-style-example-grandson>',
styles: [`
::ng-deep h1{
color:blue;
}
`]
})
export class StyleExampleChildComponent {

}
<app-style-example></app-style-example> // 包含parent , child , grandson
<app-style-example-brother></app-style-example-brother> // brother
ng-deep會影響全局的樣式

那若是只想要修改使用 ng-deep 的組件(Child) 以及其子組件 (Grandson) ,應該如何設定?這時候就要透過先前的 :host 選擇器,就可以達到這個效果

import { Component } from '@angular/core';

@Component({
selector: 'app-style-example-child',
template: '<h1>Child H1</h1> <app-style-example-grandson></app-style-example-grandson>',
styles: [`
:host ::ng-deep h1{
color:blue;
}
`]
})
export class StyleExampleChildComponent {

}

小結:僅使用 ::ng-deep 會使該對應到該 className 的所有元素喪失屬性選擇器,使得所有包含該 className 的元素受到影響 (使該樣式失去樣式封裝,會影響所有兄弟組件),通常會在 ng-deep 前加上 :host,將 ng-deep 的設定留在組件樹內部。另外現 ng-deep 已棄用。

::host-content

( 與上層DOM匹配時才會觸發 )

舉例來說,目前有一個父層與一個子層。子層有一個 <h1>,且子層僅當父層於子層外有 <div> 時才觸發,則可以寫:

// 子層
import { Component } from '@angular/core';

@Component({
selector: 'app-style-example-child',
template: '<h1>Child H1</h1>',
styles: [`
:host-context(div) h1 { color: red }
`
]
})
export class StyleExampleChildComponent {

}

// 父層
import { Component, ViewEncapsulation } from '@angular/core';

@Component({
selector: 'app-style-example',
template: `
<div>
<app-style-example-child></app-style-example-child>
</div>
`
,
styles: [``]
})
export class StyleExampleComponent {

}
觸發子層的 host-context 選擇器

若父層外沒有符合子層要的 div 時,如

// 父層

import { Component, ViewEncapsulation } from '@angular/core';

@Component({
selector: 'app-style-example',
template: `
<app-style-example-child></app-style-example-child>
`
,
styles: [`
`
]
})
export class StyleExampleComponent {

}
則子層不會觸發 host-context 的內容

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

No responses yet

Write a response