I have created generic svelte components which I want to add to an existing project. The components make use of third party libraries which use global operator for some of the styles.
When I add the component to the existing project, the styles of the existing project are affected by the child component.
Example:
In the image above, there is an outer element with * { color: green } and an inner element with * { color: red }. It works as expected, no style is leaked inside or outside.
However, if I use :global(), that style is applied to parent components as well. I'm using UI libraries that do this, so I have no control over their use of global. For example, if I add a material button using a library, the parent styles go haywire:
I used a simple example which could actually be solved by redefining a few css styles on the parent component, but I have many components with many layers of nesting using multiple third party components which attempt to change a lot of global styles so redifining styles in parent would be impractical.
Is there a way to tell svelte to scope all of a components styles to itself?
ps: In the example images above, the child element is converted to web-component. So the global styles are leaking out not only in svelte apps, but even leaking outside the shadow dom
Related
A lot of our site is structured as such:
component
component CSS
component template
child component
We often need to style the child component differently based on which parent component it was in. This is annoying, of course, but was manageable when we were using global CSS as we could simply target the specific instance with unique ID and class CSS selectors.
We're trying to now stop using global styles and encapsulating our CSS within each component. The problem with this is that, from what I see, encapsulate angular CSS will scope your CSS so that it only applies to that component...which means it also will not apply to any child components.
One solution appears to be to use ::ng-deep selectors in our component CSS to target the child. This is meeting some resistance, though, as I'm being told this also breaks encapsulation in general and could affect other parts of the site in different components.
This is where I'm confused. Some questions:
Does any style targeted with ::ng-deep become descoped so that it is global application-wide? Or is it simply 'global' within that one parent component?
If it's the former, would it be an OK practice to still use ::ng-deep, but also make sure you are using additional unique css selectors so it only applies to the child component you are targeting?
Or is there a more proper way to add styles to a particular child component instance?
:ng-deep is global. No matter where you put it, it applies to all components. Not just children.
If you use :host :ng-deep, then it will work from that component down (Into the children, grand children etc).
The main issue working with ng-deep is that styles in Angular are lazy loaded. So in some cases, you can be viewing the site perfectly fine, then you view a particular page that uses ng-deep, then you can go back to previous pages that were fine that are now broken because the ng-deep style is applied site wide. e.x. https://tutorialsforangular.com/2020/04/13/the-dangers-of-ng-deep-bleeding/
Generally speaking, if I need to style a child component slightly differently depending on where it's placed, then I create an input variable for the child, make the parent set it, then make it a class somewhere in the child component HTML. The child component can then style that class how it see's fit and you haven't had to break encapsulation.
::ng-deep by itself does make the style global, but when combined with :host may do what you need, but as #eldar alluded to with this link, ::ng-deep is deprecated (and has been for some time).
In my app.component.html I have the following, working class assignment.
<body>
<span class="test">SHABOO</span>
<div class="container">
<router-outlet></router-outlet>
</div>
</body>
In the outlet, a component is rendered and it contains the following.
<span class="test">HAZAA</span>
I expected it to get the same style as the first one but it seems that the style doesn't cascade down to the component. It made me uncertain if I'm mistaken about how the styles are supposed to behave between parent and child components in Angular.
I made sure that the name of the class isn't overriden (to exclude the risk of collision). At the moment, I'm putting similar chunks of SCSS code in each component and that's obviously bad practice.
If I #import "../app.component.scss", the styles kicks in. But I was under the impression that even without the import, the style is supposed to cascade.
Angular components use view encapsulation. This is by design so that components are reusable across applications. To treat a component's styles as global, set the view encapsulation mode to none (not recommended).
Instead of using none, register global style files in the Angular CLI styles section, which is pre-configured with the styles.css file.
Component CSS styles are encapsulated into the component's view and don't affect the rest of the application.
To control how this encapsulation happens on a per component basis, you can set the view encapsulation mode in the component metadata. Choose from the following modes:
ShadowDom view encapsulation uses the browser's native shadow DOM implementation (see Shadow DOM on the MDN site) to attach a shadow DOM to the component's host element, and then puts the component view inside that shadow DOM. The component's styles are included within the shadow DOM.
Native view encapsulation uses a now deprecated version of the browser's native shadow DOM implementation - learn about the changes.
Emulated view encapsulation (the default) emulates the behavior of shadow DOM by preprocessing (and renaming) the CSS code to effectively scope the CSS to the component's view. For details, see Appendix 1.
None means that Angular does no view encapsulation. Angular adds the CSS to the global styles. The scoping rules, isolations, and protections discussed earlier don't apply. This is essentially the same as pasting the component's styles into the HTML.
To set the components encapsulation mode, use the encapsulation property in the component metadata:
src/app/quest-summary.component.ts
// warning: few browsers support shadow DOM encapsulation at this time
encapsulation: ViewEncapsulation.Native
ShadowDom view encapsulation only works on browsers that have native support for shadow DOM (see Shadow DOM v1 on the Can I use site). The support is still limited, which is why Emulated view encapsulation is the default mode and recommended in most cases.
External and global style files
When building with the CLI, you must configure the angular.json to include all external assets, including external style files.
Register global style files in the styles section which, by default, is pre-configured with the global styles.css file.
See the CLI wiki to learn more.
This is how angular works. Every component is separated with it's CSS, even if it is child component of a parent. They can't inherit their parent CSS.
Haza span component is child of your app component, but it is separated component. So, you can't get those Shaboo styling to Haza.
Solution:
if you want to maintain same CSS, there are multiple ways.
Recommended way:
a) if it is only for onle class, copy the class to the child component.
b) if it is for huge number of classes, create a css file for them and import them in both parent and child component css.
Example:
#import '../span.css';
you can add this css to styles.css. then you will get it anywhere in the app. if you want this style many places, you can do it.
As #Christopher Peisert mentioned, by using
encapsulation: ViewEncapsulation.Native
end of the #component decorator in child.component.ts. It is not recommended way
The global CSS files are those that are listed in angular.json. All others CSS files are encapsulated by default (this can be removed but is not recommended.)
Keeping your global CSS organised is important as your application grows. What I like to do is create a global styles folder and then add new files for different widgets that I might be altering. Since removal of ::ng-deep these types of changes have to be done in the global scope.
It works like that. If you are in need of having a common style then add it in the global styles.scss file.
If you want a global style put it in the global file styles.css or styles.[Your css preprocessor]
See this example https://stackblitz.com/edit/angular-a5ntm6?file=src%2Fstyles.css
I put test class at the end
.test{
color:red;
}
The Angular documentation for :host does not mention that we can style all host elements at once. I have added this CSS at a “global” level:
:host {
width: 100%;
}
but there was no effect. However, this works fine in the CSS at the component level:
:host(app-search) {
width: 100%;
}
where app-search is a component, app-search.component.ts.
Is is possible to write a :host selector for all components or must this be declared multiple times at the component level?
By its very definition (or specification), :host can never be used at a global level. It is created to use from component level and to select the parent component (which is called shadow host) from the children (which is called shadow tree).
For more clarification, Angular's :host selector is a special selectors from the world of shadow DOM style scoping (described in the CSS Scoping Module Level 1 page on the W3C site).
The angular documentation clearly specifies that you should use this selector to select the parent component from within the child component. But it may seem unclear to you if you don't have any idea how shadow tree works. see the documentation.
If you need to style any component from a global stylesheet, there is a style.css file automatically added at a global scope in Angular. Just put your CSS in that file and you can find it available globally in all components.
Component level CSS files make your CSS modular. This is a great feature because:
You can use the CSS class names and selectors that make the most
sense in the context of each component.
Class names and selectors are local to the component and don't collide with classes and selectors
used elsewhere in the application.
Changes to styles elsewhere in the
application don't affect the component's styles.
You can co-locate
the CSS code of each component with the TypeScript and HTML code of
the component, which leads to a neat and tidy project structure.
You
can change or remove component CSS code without searching through the
whole application to find where else the code is used.
Although it is configurable, I strongly recommend not to use ViewEncapsulation.None. It will make kill all your CSS modularity which you can avail easily using global CSS files without affecting the scoping restriction.
In your app-search.component.ts file, you could set:
encapsulation: ViewEncapsulation.None
like this
#Component({
templateUrl: 'app-search.component.html',
styleUrls: ['app-search.component.css'],
encapsulation: ViewEncapsulation.None
})
This prevents you from having to rewrite styles and enables styles on a global level.
You could also try applying the styles directly into the index.html file. These styles will also be global, preventing you from rewriting styles at the component level.
I'd like to change the styles of a component outside the scope of my current component. The markup in my app.component.html is as follows:
<app-nav></app-nav>
<div id='content-container'>
<router-outlet></router-outlet>
</div>
I'm looking to change the styles of a class main-nav in the app-nav component from the 'home-component' which is loaded from a route. I use routerLink's in the nav and I've found if I add encapsulation: ViewEncapsulation.None to the home.component.ts I can alter the styles of the the class in the app-nav component however the changes are only applied once and if I go to other routes using the routerLink's I cannot revert the changes (seems like the css is only loaded/applied once - when the component is initialized/comes into view)
Special selectors are designed to access and apply css outside the encapsulation without having to change encapsulation scope. In your case I think :host or :host-context will help.
See https://angular.io/guide/component-styles#special-selectors
Use the :host pseudo-class selector to target styles in the element
that hosts the component (as opposed to targeting elements inside the
component's template).
and
Sometimes it's useful to apply styles based on some condition outside
of a component's view. For example, a CSS theme class could be applied
to the document element, and you want to change how your
component looks based on that.
Use the :host-context() pseudo-class selector, which works just like
the function form of :host(). The :host-context() selector looks for a
CSS class in any ancestor of the component host element, up to the
document root. The :host-context() selector is useful when combined
with another selector.
change the styles of a component outside the scope of my current component
Before I say anything else, this is highly discouraged. Components are supposed to be self-contained entities, which can be reusable across your project, or even other projects. By creating a component that changes something else in the system only through CSS, you're creating a tight coupling which is difficult to see immediately from code, leading to difficult-to-debug bugs and other errors.
If you want to change your app-nav (or main-nav as you refer to it later, I'm not sure if this is my mistake) based on the current route, you should do so by injecting the ActivatedRoute and querying it to determine the specific version of header which you want.
Another option is to simply move the header below the router outlet, so you always direct access to it from the component which rests across the whole page.
the changes are only applied once and if I go to other routes using the routerLink's
This is expected, since the styles are injected into DOM only when you visit the actual component, no need for them to appear before there's need for it. Angular expects that the styles you define for a component and used only on that component. You're breaking this rule of thumb.
I cannot revert the changes (seems like the css is only loaded/applied once - when the component is initialized/comes into view)
Your observation is correct.
Consider the following hierarchy in some Angular 2/4 project:
<parent-cmp [ngClass]="{ 'parent': condition }">
<child-cmp class="child"></child-cmp>
</parent-cmp>
Now in the child component's CSS file I would like to say:
.parent .child {
background-color: red;
}
In this scenario, I'm basing the child's design on parent's logic without knowing what that logic is at child level. But the problem is that, this is not going to work. And that's because, Angular compiles the the child CSS selectors to this:
.parent[_ngcontent-c9] .child[_ngcontent-c9] {
background-color: red;
}
And the parent part of the selector is not going to work anymore. So How can I pull this off? Also please bear in mind that I simplfied this example and the two components are not necessarily one after another (there might be arbitrary number of components in between).
In Angular this is called "view encapsulation" where the JavaScript, CSS styles, and HTML templates are all managed by Angular. There are a lot of advantages to this approach as it allows you to easily tree-shake a project and drop components that are not being used. You not only drop the Javascript code, but all the styles and HTML with it.
When it's turned on the styles are injected into the DOM as embedded styles. Angular keeps track of what styles are required on the document and adds or removes styles as needed. These styles can have strange names at run-time like those in your question.
You need to read up on the https://angular.io/guide/component-styles styles guide to see how to define a :host style. This is the style assigned to a component when view encapsulation is turned on. When using :host you can refer to the parent selector using a :host-context path, and you can also style inside other child components using the ::ng-deep selectors.
Keep in mind. This is all optional. It's turned on by default, but if you don't want to use it. You can turn it off.
You can change the view encapsulation mode when you define your component. To disable this feature just change the encapsulation option to native.
See the guide:
https://angular.io/guide/component-styles#view-encapsulation