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

Многие 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 использует этот файл стилей.

Комментарии (1)


  1. vsezol
    13.05.2022 03:29

    Спасибо за статью!

    Возник вопрос к примеру, разве стили label - не ответственность label? Быть может сделать компонент обертку для него или директиву, тогда не нужно будет ставить ViewEncapsulation.None или использовать ng-deep.

    Насколько актуально использовать в стилях селектор компонента, современные средства рефакторинга изменять селектор в файле стилей при изменении названия компонента?