Manage Themes and StyleSheets - nikolaimueller/my-wc GitHub Wiki

Mange Themes and StyleSheets

With custom webcomponents using shadow-root the internal component's CSS styles can be encapsulated - at least to some extend. In addition, CSS styles defined inside a webcomponent will not bleed into the surrounding document. This does not only apply to inline CSS styles but also applies to CSS style-sheets defined in separate *.css file(s). Also there are ways to globally (from inside a document) style webcomponent internals, these techniques are not "realy smooth" and, further more, also cuting off the power of Cascading Style Sheets within custom webcomponents.

For that reasons my-wc uses CSS Style-Sheet files to implement basic styling for webcomponents, theming and individual style overwrites for all their webcomponents. If you'll like to take a look into my-wc webcomponents sources, you'll find, that for every Javascript file (*.js) implementing a webcomponent, there is a "sister" file with the exact same name but having the extension .css. Under the hood then, in the constructor of the webcomponent, you can see, that the componenent loads a CSS style-sheet file, which is the "sister" file mentioned.

As a convention in my-wc implementation, a custom webcoponent resides in a subfolder having the exact name as the tag-name of the custom webcomponent. While inside that subfolder, the Javascript source code file has the class's name as it's file-name. With the style-sheet file mentioned before, we ending up with a subfolder having (at least) two files in it.

Styling your own Custom Webcomponent

As an example how to use CSS stylesheet file in your own custom component implementation, I show you the my-wc WCSelectBox (shortened source):

Folder and Files

  • wc-select-box/
    • WCSelectBox.js
    • WCSelectBox.css
// file: wc-select-box/WCSelectBox.js
/* (1.) */ 
import { applyTheme } from '../theme-manager/theme-manager.js'
export default class WCSelectBox extends HTMLElement {
    static get tag() { return 'wc-select-box' }
    static get styleSheet_url() {
/* (2.) */
        if (import.meta.url.endsWith('.js'))  { return import.meta.url.replace('.js',  '.css') }
    }
    constructor() {
        super()
        
/* (3.) */
        // Add Component StyleSheet-Link
        this.refLinkStyle = document.createElement('link')
        this.refLinkStyle.setAttribute('rel', 'stylesheet')
        this.refLinkStyle.setAttribute('href', WCSelectBox.styleSheet_url)
        shadow.appendChild(this.refLinkStyle)
        
/* (4.) */
        // Apply theme
        applyTheme(shadow, WCSelectBox.styleSheet_url)
    }
}
// Register custom element.
customElements.define(WCSelectBox.tag, WCSelectBox)

(1.) Import the function applyTheme from the theme-manager module.
(2.) Calculate the CSS stylesheet ("sister" file) file name.
(3.) Add the stylesheet link-tag. It adds the stylesheet "sister" file as the first child element to you component's shadowroot - as a result this stylesheet becomes the styling base of the component.
(4.) Calling the applyTheme() function, will link another stylesheet file which overrides the base-style.

Theming (your's and my-wc's custom webcomponents)

How theming works:

The idea is simple: There is a separate theming base folder below your project root. At the first subfolder level there is a sub-folder for every theme you want to implement, in the example sources you will find light-theme and dark-theme.
Below the theme starting folders (light-theme and dark-theme) there should be a mirrored folder structure of your client or src folder of your project that reflects the subfolder-tree where your components reside. As a convention I place 3rd-Party components along my-wc itself in a vendor subfolder (example/vendor/). Than the theme CSS stylesheet file name has to be the same name as the custom components stylesheet file name.

With these simple rules, the applyTheme() from the theme-manager module, can calculate the path of every custom webcomponent contained in your project

Remember: One of the design principles of my-wc is, not to use any bundling or what so ever. Thus providing 3rd-party modules to your project, is a matter of copying files/folders from node_modules into a vendor folder below your client project root. (If you are interested, take a look into package.json and look out for the build scripts - it is quite simple to build with npm's run-scripts and tools like rimraf, mkdirp and ncp).

Folders and Files:

  • examples - the my-wc example folder can bee seen as an equivalent to the client or src folder in your project
    • themes - the theming root folder somewhere below your project root
      • dark-theme - the dark-theme start folder
        • components - the example components
        • styles - document's main style sheet
        • vendor - "foreign sources" normaly copied from node_modules (in case of example copied from /components)
          • my-wc
            • components
              • wc-select-box
                • WCSelectBox.css - uff, finally we got the CSS stylesheet file
              • ...
        • views - the example main views, pages so to speek
        • ...
      • light-theme - the light-theme start folder
        • ...
      • ... - more themes of your desire

TODO: describe initialising theming

Applaying individual CSS stylesheets

In addition to the base stylesheet and the theming stylesheet you can also applay individual stylesheet to my-wc components or to your own webcomponents.
Ok, but what is that good for? You can insert stylesheets into a webcomponent from outside the component!

Take a look to the examples: If you start the my-wc examples in your browser, there is a "Style Sheet" menu entry (at the top of the page/view) which looks somewhow different than the other menu entries. And indeed this "Style SHeet" menu entry has an individual stylesheet applied to it, which the other menu entries don't have (the other menu entries have only their components base stylesheet and the cuirrently selected theming stylesheet applied to them).

How to applay individual stylesheets to my-wc components:

The menu is assembled inside the MainApp component in the my-wc example (examples/components/main-app/MainApp.js) and the menu entries are made up by <route-link> (class RouteLink) custom webcomponents.

Inside the MainApp.js file there is this line of code, defining the url of the private stylesheet to be applied to one menu entry:

let privatRouteLinkUrl = import.meta.url.replace('MainApp.js', 'private-RouteLink.css')

In modern ECMAScript import.meta.url returns the url of the loaded javascript module (in this case it might be: http://127.0.0.1:5501/examples/components/main-app/MainApp.js - the server and port may be different on your machine, but the path will remain the same.)

Some lines later, inside the block of HTMLTemplate definition, there is this line of HTML5. It defines the "Style Sheet" menu entry component and it has the attribute style-sheet set to the private stylesheet's url (.../private-RouteLink.css). Setting this attribute applies the individual stylesheet file:

<${RouteLink.tag} title="Style Sheet" url="/style-sheet" style-sheet="${privatRouteLinkUrl}"></${RouteLink.tag}>

There is also an programmatically way to applay individual themes to my-wc components: applyStyleSheet(url)

let privatRouteLinkUrl = import.meta.url.replace('MainApp.js', 'private-RouteLink.css')

let refRouteLink = document.createElement(RouteLink.tag)
refRouteLink.applyStyleSheet(url)
// do some more things with refRouteLink before appending it to your shadowroot.
shadow.appendChild(refRouteLink)

And if we take a look into RouteLink.js we find that it is quiet simple to implement applyStyleSheet(url) and observing the style-sheet attribute within your own webcomponent:

import { 
    applyTheme, 
    applyStyleSheet as themeManager_applyStyleSheet } from '../../vendor/my-wc/components/theme-manager/theme-manager.js' 

class YourComponent extends HTMLElement {
    static get tag() { return 'your-component' }
    // The 'style-sheet' attribute is the declarative switch to apply individual stylesheets.
    static get observedAttributes() { return ['style-sheet'] }
    static get styleSheet_url() {
        if (import.meta.url.endsWith('.js'))  { return import.meta.url.replace('.js',  '.css') }
    }
    constructor() {
        super()
        let shadow = this.attachShadow({ mode:'open' })
        
        // Add Component StyleSheet-Link
        this.refLinkStyle = document.createElement('link')
        this.refLinkStyle.setAttribute('rel', 'stylesheet')
        this.refLinkStyle.setAttribute('href', RouteLink.styleSheet_url)
        shadow.appendChild(this.refLinkStyle)

        // Applay theme
        applyTheme(shadow, RouteLink.styleSheet_url);
    }
    applyStyleSheet(url) {
        // Optional: Apply individual stylesheet programmatically.
        themeManager_applyStyleSheet(this, url)
    }
    attributeChangedCallback(name, oldValue, newValue) {
    switch (name) {
        case 'style-sheet':
            // Optional: Apply individual stylesheet declarative.
            themeManager_applyStyleSheet(this.shadowRoot, newValue)
            break
    }
}
customElements.define(YourComponent.tag, YourComponent)

^ Wiki Home

⚠️ **GitHub.com Fallback** ⚠️