Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/bengfarrell/style-shelter

An ES6 Module based shelter to adopt CSS Stylesheets from
https://github.com/bengfarrell/style-shelter

Last synced: about 2 months ago
JSON representation

An ES6 Module based shelter to adopt CSS Stylesheets from

Awesome Lists containing this project

README

        

# Style Shelter
##### An adoption shelter for your style sheets

![alt text](adopt-a-style.png "Adopt-a-Style")

Edit: Aug 2020
--------------
Uh-oh! Keal Jones kindly pointed out in this repo's issues, that CSS's @import no longer works
with Constructable Stylesheets. I substituted a good ol' `fetch` call in it's place.
As this logic was pretty centralized, the replacement was a snap. I didn't test much though,
but given that they both do about the same thing with the URL (load it asynchronously), it seems
like a pretty decent alternative. I just popped a demo.html and demo.css file in this repo to demonstrate
for anyone interested.

The hope is that this project is no longer needed once we can properly import CSS modules with JS. It's too bad
the combo of @import and Constructable StyleSheets died first!

Great for
+ Design Systems
+ The Shadow DOM
+ Web Components
+ CSS Modules (probably... once they are supported in the browser)

Uses
+ Constructable Stylesheets (Chrome only, but can be polyfilled)
+ ES6 Module feature
+ ES6 WeakMaps

When using the Shadow DOM (commonly in Web Components), outside style cannot pierce
the Shadow Boundary and make its way in. Awesome, right? Not so if you need to use a design
system or similar.

Now, Web Component users can adopt a stylesheet that can live happily inside the safe comfort
of their very own Shadow DOM. The stylesheet is also not a clone of the original - it is a reference, which
means that entire design systems aren't recreated in every component instance.

### Main usage

**StyleShelter.adopt** accepts an array of URLs, as well as a default scope.
Internally, your URLs will be turned into proper StyleSheet objects using

```javascript
new CSSStyleSheet();
```

Once constructed, your URL will be loaded internally via `fetch` api:

```javascript
const sheet = new CSSStyleSheet();
fetch(url)
.then(response => response.text())
.then(data => {
sheet.replace(data) ....
````

As Style Shelter is a globally used module, it will cache loaded stylesheets using
a WeakMap. Subsequent calls to load the same CSS file will return the cached sheet object.

```javascript
import StyleShelter from './style-shelter.js';
const styles = ['./css/docs.css', './components/app/app.css'];
StyleShelter.adopt(styles, this.shadowRoot);
```
If no scope is specified (```this.shadowRoot``` in the example above), unadopted CSSStyleSheets are returned
to take further action. This can be great for a solution like LitElement where the static style getter
performs a single adoption all at once and needs to use a single array containing CSS template literals and your stylesheets.

The following, when your component scope is not specified, will allow the document to adopt stylesheets intended for it
but simply return the rest of the stylesheets as unadopted

```javascript
import StyleShelter from './style-shelter.js';

static get styles() {
const styles = ['./css/docs.css', './components/app/app.css'];
// note the below, we're wrapping stylesheets in another object to be
// compatible with what LitElement expects
const unadopted = StyleShelter.adopt(styles).map( sheet => ({ styleSheet: sheet }));
return unadopted.concat(css`:host{ ... }`);
}
```

### Mixed Adoption Scopes
Especially in the case of stylesheets containing CSS Vars or even broad container styles that
break through the Shadow DOM because they are so generic, it may be desired to not adopt every style
to the same scope (like the shadowRoot). This is why the first parameter of the adopt method accepts
an array of Objects as well as string URLs.

An Object as part of this array can have keys of "url", and "scope". The following will adopt
styles to the desired scopes for each CSS URL. Style Shelter users may mix URL strings and objects
in this array.


```javascript
import StyleShelter from './style-shelter.js';
const styles = [
'./css/docs.css',
'./components/app/app.css',
{ url: './css/vars.css', scope: document }];
StyleShelter.adopt(styles, this.shadowRoot);
```

### Configuration
The **adopt** method takes and optional third parameter, which is a configuration object. The default
is in the module and is the following:

```javascript
{
append: [document],
onSuccess: null,
onError: function (err, message) {
console.warn(err, err.message);
}
}
```

The **append** key is an array containing scopes. Style Shelter, by default, replaces
the current array of stylesheets with the one you specify using the new "adoptedStyleSheets" method.

```javascript
scope.adoptedStyleSheets = [someStyleSheets];
```

However, especially in the case of the page document object, you likely don't want to
replace the entire stylesheet array from an individual component, since it does affect the entire page.

This is why the **append** array contains the **document** object by default. Instead of replacing, stylesheets on the **document**
scope, it will add on, instead:

```javascript
scope.adoptedStyleSheets = scope.adoptedStyleSheets.concat([someStyleSheets]);
```

The **onSuccess** key is a success callback when the stylesheet has been successfully loaded.
With Constructable Style Sheets, CSS can be loaded synchronously, however, Style Shelter loads them
asynchronously because it uses the `fetch` api for loading CSS as text. Though the newly constructed sheet comes back synchronously,
the CSS takes time to load. This callback allows a user to take action on load. One todo for Style Shelter could be to
wait until load until the style sheet is adopted, however right now there is no option to do so.

The **onError** key is what you'd expect. If your stylesheet fails to load, it will produce an error. Also, if you fail
to specify scope (either as default or in an object), **onError** will fire. By default, **onError** will console.warn with your error.

### Polyfilling
Currently, Chrome is the only browser to support Constructable Stylesheets, the underlying feature behind Style Shelter. However, there is
a polyfill that simulates the desired results.

https://www.npmjs.com/package/construct-style-sheets-polyfill

Unfortunately Constructable Stylesheets cannot be completely replicated with a polyfill.
Shown below is a screen capture of a Shadow DOM enabled Web Component in Chrome that uses Style Shelter to adopt a number of CSS files.

![alt text](chrome-nopolyfill.png "Chrome without polyfill usage")

Notice that there are no stylesheets visible here - they are all adopted as intended.

This next screen capture is from Firefox, where Constructed Stylesheets aren't supported, and the polyfill is being used.

![alt text](firefox-withpolyfill.png "Firefox with polyfill usage")

Notice that these CSS are created inside the Shadow DOM, and imagine a large number
of component instances where we are constantly replicating the same stylesheets over and over again
for a design system. Obviously, not polyfilling will be better - so hopefully Firefox and Safari support Constructable Stylesheets soon.

### Using CSS Modules in the Future

Constructable Stylesheets solve a big problem and allow Web Component developers to adopt CSS from a design system or a common folder. They also unburden
the HTML page from having to know what CSS files are required throughout the application. Instead, each Web Component can be responsible for
loading exactly the CSS it needs.

An issue that isn't exactly solved yet is CSS specific to a Web Component. A common practice is to use strings (likely template literals) to store
CSS in a JS file.

```javascript
css() {
return `
selector {
display: flex;
position: relative;
align-items: center;
border-radius: 5px;
background-color: #ffffff;
}
...
```

An obvious thought is to use Constructable Stylesheets to take the CSS here and
put it in an actual CSS file. Ideally, a developer would place it inside their component folder.
The problem however, is that CSS URLs are relative to your index.html page, so loading the CSS from inside your componenent would look like the following:

```javascript
StyleShelter.adopt(['./components/navigation/navigation.css'], this.shadowRoot);
```

The above assumes the component knows your entire application structure, where "navigation" is the name of the component. Web Components should be more self-reliant than this.
They should not be dependent on a defined application structure to function. A developer should just pop them in wherever they need
to be and just work.

Though CSS Modules aren't available in any browsers yet, they may solve this problem (assuming they function like JS modules do).

```javascript
import Navigation from './navigation.css';
StyleShelter.adopt([Navigation], this.shadowRoot);
```

In fact - if CSS Modules work like JS modules do, where browser network requests aren't made for the same module imported from multiple places,
it might negate the need for most of what Style Shelter does with caching the style sheets.

### A Note on Using This Module

While I will make this module available on NPM and Github, I'm more focused on the workflow approach rather than
the code. I'm also not a huge fan of npm installing tiny scripts in my projects - and this one is a tiny ~130 lines of code.
Please, feel free to copy the JS and tweak it to your liking!