10

I'm building a web component with stenciljs and i want to be able to load the css theme file based on the proprety called theme.

@Component({
  tag: 'pm-header',
  styleUrl: 'pm-header.scss',
  shadow: true
})

export class PmHeader {

  @Prop() theme: string = 'default';

  ...

  render() {
    return (<nav class={this.theme}></nav>)
  }
}
malek yahyaoui
  • 379
  • 3
  • 17

3 Answers3

21

I know this is late, but this should hopefully help others as it is not documented and I just spent a lot of time figuring it out. The following code allows your custom components to use a "mode" attribute to determine which style is loaded.

Step 1: Define multiple styleUrls (aka. "modes") in your component definition. I am using "dark" and "default" - but you can define as many as you want. For example, ionic uses "ios" and "md".

@Component({
    tag: 'my-component',
    styleUrls: {
        default: 'my-component.default.pcss',
        dark: 'my-component.dark.pcss',
    },
})
export class MyComponent { ... }

Step 2: Create the style files (usually with shared common styles):

  • my-component.common.css

      :host { display: block }
    
  • my-component.default.css

      @import './my-component.common.css';
      :host { color: black }
    
  • my-component.dark.css

      @import './my-component.common.css';
      :host { background: black; color: white }
    

Step 3: Update your stencil.config.ts to use a globalScript:

export const config: Config = {
    namespace: 'mycomponent',
    globalScript: './src/globals/global.ts',
    ...
}

Step 4: Create the global script and define a "setMode" function like this:

import { setMode } from '@stencil/core';

setMode(elm => {
    // NOTE: you can write whatever you want here - it's up to you. This
    // function must return one of the style "modes" defined in step 1.
    return (elm as any).mode || elm.getAttribute('mode') || 'default';
});

Step 5: Use your custom component like this:

<!-- default mode -->
<my-component />
<my-component mode="default" />

<!-- dark mode -->
<my-component mode="dark" />

You can customize the setMode function for determining the mode - for example you could look for a property on the window, or check for a className on the element. It's up to you. The above is just a simple example which allows you to use a "mode" attribute on the element... falling back to "default" if nothing is specified.

Ryan Wheale
  • 26,022
  • 8
  • 76
  • 96
  • I followed these instructions but the style isn't applied . I can see the – Dr. Goulu Nov 20 '20 at 07:23
  • 1
    I'd have to see your setup, but the `setMode` function is likely your problem. Every time stencil mounts a component, it's going to call `setMode` and pass the DOM element. The `setMode` function is responsible for saying _"here is the stylesheet to use for this component"_. In your case, you need to make sure that `setMode` returns _"mystyle"_ when it's supposed to. (ex: `if (elm.classList.contains('mystyle')) { return 'mystyle' }` Does that make sense? – Ryan Wheale Nov 23 '20 at 19:38
  • 1
    Just an hint, if you need to know the current mode used on a given component, there is a simple to do so: `getMode(elementReference)`, where the `elementReference` is the DOM reference to the Stencil's element – gblaise Apr 29 '21 at 11:28
  • Have you got it working when calling setMode on runtime though? I've been toying around with calling `setMode` after the components have connected but it doesn't do anything. I'm calling setMode initially on the `connectedCallback` of a theme component and then again when clicking a button, but it only works the first time. – Osman Cea Jul 22 '21 at 21:05
  • @OsmanCea I'm sorry I missed your comment from years ago. The `setMode` method should only be called once. The function you pass to `setMode` will be called for every component, with `elm` referencing the root node of the component. I've never tested calling `setMode` twice, but you shouldn't need to. – Ryan Wheale Jan 25 '23 at 21:04
3

Please have a look at the below example. It will work even when shadow is set to true

@Component({
  tag: 'my-component',
  styleUrls: [
    'my-component.scss'
  ],
  shadow: true
})

export class MyComponent{

  @State() theme: string // when theme changes render needs to be called again

  render () {
      return (
        <div>

          { 
            this.theme &&
            <link rel="stylesheet" href={`path_to_css/${this.theme}.css`} />
          }

          <p>{this.text}</p>
        </div>
      )
    }
}
Lalit Kushwah
  • 3,861
  • 5
  • 25
  • 40
1

In this case, your component will always load the styles found in pm-header.scss.

To customize styling based on theme, I think you have two options:

1 - Place all themes inside pm-header.scss and use Sass to guard each one:

nav.foo {
  // foo theme goes in here
}

nav.bar {
  // bar theme goes in here
}

2 - Stencil does provide a styleUrls prop which can ostensibly let you choose between multiple Sass files at load time. This is the approach Ionic 4 is taking (between ios and material designs; here's an example), but I don't believe it's well documented on how to implement it. You'll probably have to dig around in the Ionic code to go this route.

matthewsteele
  • 1,797
  • 1
  • 15
  • 31
  • I already implemented the first solution but I wanted a more clean one, thank you anyway. The second one on the other hand needs a bit of time of digging, time that I don't have for now. – malek yahyaoui Sep 28 '18 at 13:40