Manage Themes and StyleSheets - nikolaimueller/my-wc GitHub Wiki
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.
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.
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 fromnode_modules
into a vendor folder below your client project root. (If you are interested, take a look intopackage.json
and look out for the build scripts - it is quite simple to build withnpm
's run-scripts and tools likerimraf
,mkdirp
andncp
).
Folders and Files:
-
examples
- themy-wc
example folder can bee seen as an equivalent to theclient
orsrc
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 fromnode_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
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).
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)