Перевод (а точнее оригинал) моей статьи опубликованной здесь

Многие Angular разработчики и верстальщики, пишущие CSS/SCSS код в Angular-приложениях сталкивались с ситуацией, когда надо применить стили к компоненту вложенному в текущий и не до конца разобравшись как это работает, выключали инкапсуляцию стилей или добавляли ng-deep, при этом не учитывая некоторых нюансов, что в последствии приводит к проблемам. В данной статье я попытаюсь максимально просто и сжато изложить все детали.

Когда у компонента включена инкапсуляция стилей (по умолчанию она включена и в большинстве случаев стоит оставить ее включенной), ст��ли содержащиеся в файле\файлах стилей компонента будут применяться только к элементам этого компонента. Это очень удобно, вам не нужно следить за уникальностью селекторов, не нужно использовать БЭМ или придумывать длинные имена классов и следить за их уникальностью, хотя вы по-прежнему можете это делать, если хотите. Во время компиляции проекта Angular сам добавит к каждому элементу уникальный атрибут, например, _ngcontent-ool-c142 и заменит ваш класс .my-class на .my-class[_ngcontent-ool-c142] (это в случае ViewEncapsulation.Emulated, которая включена по умолчанию, если вы укажете `ViewEncapsulation.ShadowDom`, поведение будет другое, но результат тот же).

Теперь давайте представим, что у нас есть компонент ComponentA

<div class="checkbox-container">
  <mat-checkbox>Check me</mat-checkbox>
</div>

в который вложен mat-checkbox из Angular material (это может быть и ваш собственный компонент, не обязательно компоненты из библиотек).

Внутри компонента mat-checkbox есть label,

<mat-checkbox>
	<label>...
</mat-checkbox>

к которому мы хотим добавить border. Если мы напишем в файле стилей компонента

mat-checkbox label {
  border: 1px solid #aabbcc;
}

то после применения ViewEncapsulation.Emulated селектор будет примерно таким

mat-checkbox[_ngcontent-uiq-c101]   label[_ngcontent-uiq-c101] {
  border: 1px solid #aabbcc;
}

т. е. border применится к label с атрибутом _ngcontent-uiq-c101, но у всех дочерних элементов внутри mat-checkbox будет другой атрибут, т. к. label находится внутри другого компонента, и у него либо будет атрибут с другим ID (id компонента mat-checkbox), либо его не будет вообще, если у компонента в свою очередь отключена инкапсуляция. В данном случае атрибута не будет совсем, т. к. у mat-checkbox как и у других компонентов из Angular Material ViewEncapsulation.None.

Таким образом стили, ограниченные атрибутом компонента ComponentA применяются только к элементам находящемся непосредственно внутри этого компонента. Если в компоненте находится другой компонент, то на его элементы эти стили уже не распространяются.

Если вам интересно, как именно работает Emulated инкапсуляция в Angular, вы можете найти множество подробных статей на эту тему, здесь же я приведу очень краткое описание, чтобы не раздувать статью. Итак, если у компонента есть инкапсуляция, то к самому компоненту добавится атрибут _nghost-ID, а к каждому вложенному элементу добавится атрибут _ngcontent-ID и ко всем стилям в этом компоненте в селектор добавится [_ngcontent-ID]. Таким образом все стили будут применяться ТОЛЬКО к элементам расположенным непосредственно внутри этого компонента.

Как же быть если нам надо применить стили к элементам внутри вложенного компонента (т. е. в нашем примере к label внутри mat-checkbox)

Для того чтобы применить стили, у нас есть три варианта:

  • отключить инкапсуляцию стилей в ComponentA

  • использовать ng-deep

  • поместить css код в глобальные стили, т.е. стили в styles.(s)css или в других файлах указанных в секции styles в angular.json

Давайте рассмотрим их подробнее

Отключение инкапсуляции

В этом случае все стили внутри компонента станут «глобальными», причем это произойдет только после того, как компонент будет создан, т. е. после того как пользователь посетил тот р��здел приложения, где используется данный компонент, что сильно затрудняет выявление данной проблемы. Давайте отключим инкапсуляцию стилей у нашего компонента

@Component({
  selector: 'app-component-a',
  templateUrl: './component-a.component.html',
  styleUrls: ['./component-a.component.scss'],
  encapsulation: ViewEncapsulation.None
})

вспомним, что в файле стилей у нас

mat-checkbox label {   
  border: 1px solid #aabbcc; 
}

до тех пор пока пользователь не открыл страницу, где используется компонент ComponentА, все остальные mat- checkbox в приложении выглядят без рамки, но после того, как ComponentА создан, css код приведенный выше динамически добавится в секцию <style> в DOM дерево и после этого все mat-checkbox станут использовать эти стили.

Для того, чтобы предотвратить такой явно нежелательный эффект, мы можем ограничить область д��йствия стилей, применив более специфичный селектор. Например, добавим класс checkbox-container к элементу родителю mat-checkbox,

<div class="checkbox-container">   
   <mat-checkbox>Check me</mat-checkbox> 
</div>

и исправим селектор на такой

.checkbox-container mat-checkbox label {   
	border: 1px solid #aabbcc; 
}

теперь рамку получат только чекбоксы расположенные внутри элемента с классом checkbox-container. Но вместо того, чтобы добавлять класс с уникальным именем и следить за их неповторяемостью, гораздо проще использовать селектор компонента, т.к. он будет заведомо уникальным

app-component-a mat-checkbox label {   
  border: 1px solid #aabbcc; 
}

Вывод: если вы отключаете инкапсуляцию, не забывайте добавлять селектор компонента ко ВСЕМ стилям внутри компонента, в случае SCSS\SASS, просто оборачивайте весь код в:

app-component-a {   
  ... 
}

Псевдо-класс ng-deep

Теперь давайте включим инкапсуляцию обратно, убрав encapsulation: ViewEncapsulation.None из декоратора @Component, и добавим в css селектор ::ng-deep

::ng-deep mat-checkbox label {   
  border: 1px solid #aabbcc; 
}

ng-deep заставит фреймворк сгенерировать стили без добавления к ним атрибутов , в результате в DOM добавится код:

mat-checkbox label{border:1px solid #aabbcc}

который будет влиять на все mat-checkbox приложения, точно так же как если бы мы добавили это в глобальные стили или отключили инкапсуляцию, как мы делали ранее. Чтобы избежать этого поведения, мы можем опять ограничить область распространения селектором компонента

::ng-deep app-component-a mat-checkbox label {   
  border: 1px solid #aabbcc; 
}

или поступить еще проще и использовать псевдо-класс :host

:host ::ng-deep mat-checkbox label {   
  border: 1px solid #aabbcc; 
}

что гораздо удобнее и надежнее (представьте, что вы переименовали селектор компонента и забыли изменить его в коде css).

Как это работает? Очень просто - Angular сгенерирует в данном случае вот такие стили

[_nghost-qud-c101] mat-checkbox label{border:1px solid #aabbcc}

где _nghost-qud-c101 это атрибут добавленный к нашему ComponentA, т. е. border применится ко всем label внутри любого mat-checkbox, лежащего внутри элемента с атрибутом _nghost-qud-c101, который есть ТОЛЬКО у ComponentA.

<app-component-a _ngcontent-qud-c102 _nghost-qud-c101>

Вывод: если используете ::ng-deep ОБЯЗАТЕЛЬНО добавляйте :host или создайте mixin и везде используйте его

@mixin ng-deep {
  :host ::ng-deep {
    @content;
  }
}

@include ng-deep {
  mat-checkbox label {
    border: 1px solid #aabbcc;
  }
}

Многих смущает тот факт, что ng-deep уже давно помечен как deprecated. У команды Angular были планы отказаться от использования этого псевдо-класса, но позже это решение было отложено на неопределенный срок, по крайней мере до тех пор, пока не появятся новые альтернативы. Если сравнивать ng-deep и ViewEncapsulation.None, то в первом случае мы по крайней мере отключаем инкапсуляцию не для всех стилей компонента, а только для тех, которые нам нужны. Даже если у вас есть компонент, где все стили, предназначены для дочерних компонентов, ng-deep кажется более выигрышным, т. к. вы в последствии можете добавить стили для собственных элементов компонента, и в этом случае вы их просто напишете выше\ниже кода вложенного в :host ::ng-deep {} и они будут работать как обычно, а при отключенной инкапсуляции у вас уже нет такой возможности.

Напоследок хочу добавить пару слов о том, как «стилить» компоненты из библиотек. Если вам нужно изменить вид по умолчанию для, скажем, всех mat-select в вашем приложении, чаше всего лучше сделать это в глобальных стилях. Иногда, некоторые разработчики предпочитают поместить эти стили в отдельный SCSS файл и импортировать его везде где нужно, но в этом случае при сборке проекта, эти стили продублируются в каждом chank-е (скомпилированный js файл для каждого lazy- или shared-модуля), где хотя бы один из компонентов, попавших в этот chank использует этот файл стилей.