3

I am developing a Dashboard in Angular with a Plugin Architecture to keep it easily extensible. The dashboard consists of pre-defined visualizations like scatter plots. I also want to allow adding visualizations from external sources, i.e., an angular component that is capable of drawing 3D plots. I have successfully implemented the Plugin Architecture on the Angular and Typescript side. However, I'm facing challenges in making the SCSS styles easily extensible and ensuring a seamless look and feel when integrating visualizations from external sources.

Specifically, I want each component to have its own styles, such as font color and size. Additionally, I want to define a global-styles.scss file where I can overwrite these styles. When styles are defined in global-styles.scss, they should overwrite the local styles.

Currently, I have come up with the following (working) approach:

/* global-styles.scss */
:root {
  --header-color: hotpink;
}
/* scatter-plot.scss */
:host {
  --default-header-color: blue;
}

.header {
  color: var(--header-color, var(--default-header-color));
}

While this approach seems to work, it involves lots of repetition because I always need to use var(--XY, var(--default-XY)) for each custom property usage. So I'm wondering whether there's a cleaner and more efficient way to achieve the desired behavior? I have attempted to directly overwrite the custom properties, but I couldn't get it working as the "outer CSS" would need to overwrite the "inner CSS" (i.e., global-styles.scss should overwrite scatter-plot.scss).

EDIT

Ideally, it should also support theming (light and dark mode). Thus, I guess it would be easier to stick to CSS custom properties rather than SCSS, because they can be easily overwritten at runtime.

Highnoon
  • 64
  • 5

5 Answers5

1

Your approach is correct and fits the CSS custom properties specifications, but I understand that it's somewhat verbose due to the need to provide default values each time a variable is used.

You can create a SCSS mixin (utility mixin) that encapsulates the fallback logic. This could reduce the amount of code you write each time you need to use a variable:

@mixin var($property, $varName, $default) {
  #{$property}: var(--#{$varName}, var(--default-#{$varName}, #{$default}));
}

You can use this mixin in your component styles:

.header {
  @include var(color, 'header-color', blue);
}

This approach still uses CSS variables and fallbacks, but it makes your SCSS code a little cleaner and easier to manage.

OR since you're using Angular, you could take advantage of the ::ng-deep pseudo-class to enforce styles on child components. With this approach, you could define default styles in your components, then use ::ng-deep in your global styles to override them. However, ::ng-deep is deprecated and is not a recommended long-term solution.

OR This is a more Angular-oriented way to handle theming, you can create a ThemeService, which allows you to switch themes dynamically at runtime. Each theme could be a separate SCSS file that's loaded based on the current theme. For theming, you would use Angular's [ngStyle] or [ngClass] directives to apply styles based on the current theme.

Given your requirements, the utility mixin method might be the most suitable. It still adheres to the CSS custom properties methodology and doesn't require much refactoring from your current approach.

Joey
  • 613
  • 1
  • 6
  • 17
0

You can utilize CSS custom properties. By organizing your styles and leveraging cascading nature of CSS, you can achieve the desired behavior without the repetition of var(--XY, var(--default-XY)) for each custom property usage.

Here's an improved approach that builds upon your existing setup:

/* global-styles.scss */
:root {
--header-color: hotpink;
}

/* scatter-plot.scss */
:host {
--header-color: blue;
}

.header {
color: var(--header-color);
}

By using the var(--header-color) directly in the .header class, the pattern for each usage and rest will be taken care of with the help of CSS Cascading.

Regarding theming (light and dark mode), you can create different sets of custom properties for each theme, and swap them out dynamically. For example:

/* global-styles.scss */
:root {
--header-color-light: hotpink;
--header-color-dark: #333;
}

/* scatter-plot.scss */
:host {
--header-color: var(--header-color-light);
}

To switch to the dark theme, you can programmatically update the values of the custom properties to their corresponding dark mode values.

With this setup, you can easily extend and override styles in individual components while maintaining a seamless look and feel across your dashboard.

Try this and let me know.

Ankit
  • 86
  • 2
  • 12
  • 1
    Thanks for your answer. However, as I explicitly stated, I want to "reverse" the cascading. I want the variables in `global-styles.scss` to overwrite those defined in `scatter-plot.scss`. While this might seem counter-intuitive, my goal is to make the components usable individually and well as being able to integrate them into large projects. Within large projects, all of those components however should share uniform styles (= variables of `global-styles.scss` should take over). – Gykonik Jun 08 '23 at 12:40
0

You can change the .css variables using javascript.

Imagine a .json like

{
  "--background-color": "#212a2e",
  "--text-color": "#F7F8F8",
  "--link-color": "red"
}

You can to have a service that change the css variables based in this file

@Injectable({
  providedIn: 'root',
})
export class CssService {
  json: any;
  constructor(private httpCient: HttpClient) {}
  setCss(el: ElementRef) {
    const obs = !this.json
      ? this.httpCient
          .get('/assets/css.json')
          .pipe(tap((res) => (this.json = res)))
      : of(this.json);

    return obs.pipe(
      tap((res: any) => {
        Object.keys(res).forEach((key) => {
          if (window.getComputedStyle(el.nativeElement).getPropertyValue(key))
            el.nativeElement.style.setProperty(key, res[key]);
        });
      }),
      map((_) => true)
    );
   }
 }

Now in each component you can in constructor inject as public

constructor(public cssService: CssService, public el:ElementRef) {}

And put all under a div

<div *ngIf="cssService.setCss(el)|async">
  ...
</div>

stackblitz

Eliseo
  • 50,109
  • 4
  • 29
  • 67
0

You can try change the angular.json to change the styles

    "styles": [
          "src/styles.css",
          {
            "input":"src/css/global.css",
            "bundleName": "global"
          }
        ],

See the docs

As Angular create an index with the two styles

<style="style.***.css">
<style="global.***.css">

The only is "global" should override the style.css

To override you need your global.css looks like, e.g.

//my-app is the selector
my-app.custom{
  --background-color: #212a2e,
  --text-color: #F7F8F8,
  --link-color: red
}
//hello is the selector
hello.custom{
  --text-color:yellow;
}

your component like

@Component({
  selector: 'my-app',
  ...
  host: {class: 'custom'}
})

Or use

<hello class="custom" ...></hello>

NOTE: You can also include manually the global.css in the .html and, in this way, you can simply edit the global.css without to create again the app.

a stackbliz

Eliseo
  • 50,109
  • 4
  • 29
  • 67
  • Thanks for the answer! This implies that I have to know all components I want to include in advance to use their selector, right? Ideally, I would like the rules to apply to everything automatically. One way would be to use `* { --text-color: yellow}`, but I guess it's not really a good style :D So is there a good way to overwrite the styles of `hello` without directly knowing about `hello` :) – Highnoon Jun 09 '23 at 15:08
  • If all your css variables are related to `:root` I imagine it's all more easy. Really I don't know about your requirements. The idea is create some . css "more specific" – Eliseo Jun 09 '23 at 20:43
0

There's one more option you could play with: style encapsulation. I see two possibilities:

  • Go full on ViewEncapsulation.None (at least for the customizable components)

It's more risky, since it could lead to unintentional styles overrides, especially with more people working on the project. I see this approach taken more on libraries.

  • Separate customizable tokens from components encapsulated styles

I think this approach could work well for your case! Separate customizable CSS variables from your components scss files. You could even keep them on the same folders if the variables are closelly related to each component and import these files on a main file, which could also contain global theming variables. These global variables could be used as initial default values for the component ones, creating a hierarchy...the possibilities are endless!

You could import the tokens stylesheet at the start of your global styles file, so variables overrides done after would work properly!

Hope this helps a little! Cheers!

Lincoln Alves
  • 546
  • 5
  • 13