
Перевод (а точнее оригинал) моей статьи опубликованной здесь
Многие 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 использует этот файл стилей.