8

Let's say there is a simple framework to display popups:

@Component(
  selector: 'popup-host',
  template: '''
      <div class="popup-container">
        <ng-template #popupRef></ng-template>
      </div>
  ''',
  styles: ['.popup-container { position: absolute; top: 100; left: 100; z-index: 100; }'],
)
class PopupContainerComponent {
  final PopupController _controller;
  final ComponentLoader _loader;

  PopupContainerComponent(this._controller, this._loader);

  void ngOnInit() {
    _controller.container = this;
  }

  @ViewChild('popupRef', read: ComponentRef)
  ComponentRef popupRef;

  void render(PopupConfig config) {
    final componentRef = _loader.loadNextTo(config.factory, popupRef);
    if (componentRef.instance is HasValueSetter) {
      componentRef.instance.value = config.value;
    }
  }
}

@Injectable()
class PopupController {
  PopupContainerComponent _container;
  set container(PopupContainerComponent container) => _container = container;

  void showPopup(PopupConfig config) {
    container.render(config);
  }
  ...
}

class PopupConfig {
  final ComponentFactory factory;
  final dynamic value;
  PopupConfig(this.factory, [this.value]);
}

abstract class HasValueSetter {
  set value(dynamic value);
}

This can then be used like this:

// Somewhere in the root template
<popup-host></popup-host>

// In popup.dart
@Component(
  selector: 'happy-popup',
  template: '''
      <div class="header">This is the popup content.</div>
      <div class="main">The value is {{value}}.</div>
      <div class="footer">I am happy!</div>
  ''',
)
class HappyPopupComponent implements HasValueSetter {
  @override
  dynamic value;
}

// In some_other.dart
@Component(
  ...
  styles: [
    '.header { font-weight: bold }',
    '.main { color: red }',
    '.footer { color: green; font-style: italic }',
  ],
  ...
)
class SomeOtherComponent {
  final PopupController _popupController;
  ...
  SomeOtherComponent(this._popupController, ...) ...;

  void displayPopup() {
    _popupController.showPopup(HappyPopupComponentNgFactory, 42);
  }
}
...

Is there a way to forward styles from <some-other-component> to <happy-popup> without having to define them at the root of the app?

Dino
  • 7,779
  • 12
  • 46
  • 85
Sergiy Belozorov
  • 5,856
  • 7
  • 40
  • 73

5 Answers5

6

You could achieve this by splitting your components code into separate files - or separate CSS file in your case.

Instead of writing the style straight in the component's - styles, you would import the CSS file by using the styleUrls. That way you can pass a file(s) with your styles, and the file can be shared amongst multiple components.

@Component(
      styleUrls: ['./hero1.css', './folder/hero2.css'],
)

Bear in mind that the URLs in styleUrls are relative to the component.

Another benefit of importing the css with styleUrls is that it also grants you the ability to use imports within that file.

hero1.css

@import './folder/hero2.css';

FYI: It's a common practice to split your components code into separate files.

  • hero.dart
  • hero.css
  • hero.html

And then in your dart file reference them as:

@Component(
      templateUrl: './hero.html',
      styleUrls: ['./hero.css'],
)

Please refer to AngularDart - Component Styles for brief information about this.

Dino
  • 7,779
  • 12
  • 46
  • 85
  • Thank you. I was aware of the fact that I could separate stylesheets into a separate file, but didn't quite think about reusing them across multiple components. I am not sure if this solution is universal enough though, e.g. a popup component could be part of the framework and importing parts of the app into the framework may be contrary to good software practices. Ideally, framework would provide a way to import an arbitrary css file provided via the `PopupConfig`. Is it possible to customize the `styleUrls` property on a component at run-time? – Sergiy Belozorov Dec 03 '19 at 10:19
  • Also what if there are multiple components that want to open the same popup, but style it differently? – Sergiy Belozorov Dec 03 '19 at 10:27
  • 1
    Yes you can set the `styleUrls` dynamically at the run-time. There are multiple ways to do it. I suggest you to search for `Angular dynamic styleUrls` to get some ideas and see which one fits you the best. Regarding the second question. If that's the case, I would import the shared popup styling file + the styling file where I overwrite the styles I want to change for a specific component. – Dino Dec 03 '19 at 10:34
  • I an not sure I understand your suggestion on how to style two popups differently. Both popups would use the same component, so the only way I can overwrite the styles from a shared file is by appending a new entry to `styleUrls` at run-time, isn't it? What if two popups appear at the same time, e.g. one opens another, and need to by styled differently? If I modify the `styleUrls`, wouldn't it style both? Finally, I couldn't find a good article on how to dynamically modify `styleUrls` without hacks. It seems to be not supported by Angular natively. – Sergiy Belozorov Dec 05 '19 at 09:49
1

As your popup isn't a child of the component that opened it you cannot use ::ng-deep

the only thing I think that will work is to remove view encapsulation from the host, the popup and the component that opens the popup (try only the popup and component that opens the popup first, if that doesn't work, remove the host's encapsulation as well.)

@Component(
  selector: 'happy-popup',
  template: '''
      <div class="header">This is the popup content.</div>
      <div class="main">The value is {{value}}.</div>
      <div class="footer">I am happy!</div>
  ''',
  encapsulation: ViewEncapsulation.None // <=== no encapsulation at all
)
class HappyPopupComponent implements HasValueSetter {
Ron
  • 844
  • 7
  • 8
  • Can you please explain what you mean by "remove view encapsulation"? – Sergiy Belozorov Dec 03 '19 at 10:23
  • @SergiyBelozorov Done :). let me know if it helped you – Ron Dec 03 '19 at 10:38
  • It certainly helped me learn something new about Angular and how one can configure components :-), but I am not sure this is the best approach. With generic class names such as `header`, `main`, and `footer` one can easily style such components incidentally. Perhaps one can try to use more specific names to avoid conflicts, but I would prefer a solution that does keep the encapsulation. – Sergiy Belozorov Dec 03 '19 at 10:51
  • I understand your concern but that’s the only solution to your requirements. As you said one can try to use more specific name and classes, maybe use the BEM approach – Ron Dec 03 '19 at 10:55
  • @SergiyBelozorov If it feels to you like this is the most accurate answer to your question, dont forget to mark it as the accepted one, I would love to have that bounty ;) – Ron Dec 03 '19 at 11:58
  • 1
    Disabling `ViewEncapsulation` is a big no-no for mid or large sized applications. It takes special care or your whole styling will get messed up in a long run. (ex: generic class names). And I disagree that it's the only solution to his requirements. – Dino Dec 03 '19 at 13:26
  • @Dino it’s not for the entire app only a small module which he can encapsulate with different approach. – Ron Dec 03 '19 at 13:29
  • @Dino also angular material team use this approach in their CDK library – Ron Dec 03 '19 at 13:43
  • Thank you @Ron for offering your answer, but I will wait a bit more before deciding which one is best. And I do get email reminder to assign the bounty before it expires, so no worries :-). – Sergiy Belozorov Dec 05 '19 at 09:36
0

You can combine scss with your code

First you need to seperate the scss file you want to share accross the app

For example:

In style1.scss

.header { font-weight: bold }

Then in style2.scss

@import "style1"

Or you can combine list of scss file in your component code by define in the list of array

styleUrls: ['./style1.scss', './style2.scss'],
  • Note: Please change the path acordingly with your file path and also this only work when you use scss not css

This is how you can manual config scss support for angular cli

In angular.json add

"projects": {
        "app": {
            "root": "",
            "sourceRoot": "src",
            "projectType": "application",
            "prefix": "app",
            "schematics": { // add this config
                "@schematics/angular:component": {
                    "style": "scss"
                }
            },
Tony Ngo
  • 19,166
  • 4
  • 38
  • 60
  • Yes, we do indeed use scss and `@import` statements. But just like the answer from @Dino, I wonder if it's possible to not have to hard-code the style URL into the popup component, but read it from the `PopupConfig` and somehow assign it at run-time? – Sergiy Belozorov Dec 03 '19 at 10:21
0

You can send prop value to component, which can be custom class and put it in your popop html. And then in scss file add extra css overrides for specific class. So for each custom component you can have custom css code.

PS: And yes, i would suggest to import scss file like:

@Component(
      styleUrls: ['./hero1.css'],
)

It is just better to seperate css from js + your css code then can be much longer, containing all styling cases.

Andris
  • 3,895
  • 2
  • 24
  • 27
0

In addition to methods already mentioned I would suggest two more avenues to explore:

  1. since Angular scopes CSS to components and we'd like to keep it this way your crossing component boundary can be done by finding out what scope Angular assigned to it and adding manually scoped CSS into global <style> tag on the page:

    @ViewChild('popupRef') popupRef; ngAfterViewInit() { this.popupRef.nativeElement.attributes[0].name // this will have a value similar to _ngcontent-tro-c1 which you will need to scope all your custom CSS with. }

one apparent drawback of this approach is that Angular conceals CSS management and therefore you'll have to resort to plain JS to manage it. (one example here)

  1. you can try defining the CSS in @Component before creating it by utilising custom decorator factory like in this answer

Personally I'd explore the second option as it seems to be less hacky

timur
  • 14,239
  • 2
  • 11
  • 32