├── .gitignore ├── LICENSE ├── README.MD ├── adopt-a-style.png ├── chrome-nopolyfill.png ├── demo.css ├── demo.html ├── firefox-withpolyfill.png ├── package.json └── style-shelter.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Ben Farrell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # Style Shelter 2 | ##### An adoption shelter for your style sheets 3 | 4 | ![alt text](adopt-a-style.png "Adopt-a-Style") 5 | 6 | Edit: Aug 2020 7 | -------------- 8 | Uh-oh! Keal Jones kindly pointed out in this repo's issues, that CSS's @import no longer works 9 | with Constructable Stylesheets. I substituted a good ol' `fetch` call in it's place. 10 | As this logic was pretty centralized, the replacement was a snap. I didn't test much though, 11 | but given that they both do about the same thing with the URL (load it asynchronously), it seems 12 | like a pretty decent alternative. I just popped a demo.html and demo.css file in this repo to demonstrate 13 | for anyone interested. 14 | 15 | The hope is that this project is no longer needed once we can properly import CSS modules with JS. It's too bad 16 | the combo of @import and Constructable StyleSheets died first! 17 | 18 | Great for 19 | + Design Systems 20 | + The Shadow DOM 21 | + Web Components 22 | + CSS Modules (probably... once they are supported in the browser) 23 | 24 | Uses 25 | + Constructable Stylesheets (Chrome only, but can be polyfilled) 26 | + ES6 Module feature 27 | + ES6 WeakMaps 28 | 29 | When using the Shadow DOM (commonly in Web Components), outside style cannot pierce 30 | the Shadow Boundary and make its way in. Awesome, right? Not so if you need to use a design 31 | system or similar. 32 | 33 | Now, Web Component users can adopt a stylesheet that can live happily inside the safe comfort 34 | of their very own Shadow DOM. The stylesheet is also not a clone of the original - it is a reference, which 35 | means that entire design systems aren't recreated in every component instance. 36 | 37 | ### Main usage 38 | 39 | **StyleShelter.adopt** accepts an array of URLs, as well as a default scope. 40 | Internally, your URLs will be turned into proper StyleSheet objects using 41 | 42 | ```javascript 43 | new CSSStyleSheet(); 44 | ``` 45 | 46 | Once constructed, your URL will be loaded internally via `fetch` api: 47 | 48 | ```javascript 49 | const sheet = new CSSStyleSheet(); 50 | fetch(url) 51 | .then(response => response.text()) 52 | .then(data => { 53 | sheet.replace(data) .... 54 | ```` 55 | 56 | As Style Shelter is a globally used module, it will cache loaded stylesheets using 57 | a WeakMap. Subsequent calls to load the same CSS file will return the cached sheet object. 58 | 59 | ```javascript 60 | import StyleShelter from './style-shelter.js'; 61 | const styles = ['./css/docs.css', './components/app/app.css']; 62 | StyleShelter.adopt(styles, this.shadowRoot); 63 | ``` 64 | If no scope is specified (```this.shadowRoot``` in the example above), unadopted CSSStyleSheets are returned 65 | to take further action. This can be great for a solution like LitElement where the static style getter 66 | performs a single adoption all at once and needs to use a single array containing CSS template literals and your stylesheets. 67 | 68 | The following, when your component scope is not specified, will allow the document to adopt stylesheets intended for it 69 | but simply return the rest of the stylesheets as unadopted 70 | 71 | ```javascript 72 | import StyleShelter from './style-shelter.js'; 73 | 74 | static get styles() { 75 | const styles = ['./css/docs.css', './components/app/app.css']; 76 | // note the below, we're wrapping stylesheets in another object to be 77 | // compatible with what LitElement expects 78 | const unadopted = StyleShelter.adopt(styles).map( sheet => ({ styleSheet: sheet })); 79 | return unadopted.concat(css`:host{ ... }`); 80 | } 81 | ``` 82 | 83 | ### Mixed Adoption Scopes 84 | Especially in the case of stylesheets containing CSS Vars or even broad container styles that 85 | break through the Shadow DOM because they are so generic, it may be desired to not adopt every style 86 | to the same scope (like the shadowRoot). This is why the first parameter of the adopt method accepts 87 | an array of Objects as well as string URLs. 88 | 89 | An Object as part of this array can have keys of "url", and "scope". The following will adopt 90 | styles to the desired scopes for each CSS URL. Style Shelter users may mix URL strings and objects 91 | in this array. 92 | 93 | 94 | ```javascript 95 | import StyleShelter from './style-shelter.js'; 96 | const styles = [ 97 | './css/docs.css', 98 | './components/app/app.css', 99 | { url: './css/vars.css', scope: document }]; 100 | StyleShelter.adopt(styles, this.shadowRoot); 101 | ``` 102 | 103 | ### Configuration 104 | The **adopt** method takes and optional third parameter, which is a configuration object. The default 105 | is in the module and is the following: 106 | 107 | ```javascript 108 | { 109 | append: [document], 110 | onSuccess: null, 111 | onError: function (err, message) { 112 | console.warn(err, err.message); 113 | } 114 | } 115 | ``` 116 | 117 | The **append** key is an array containing scopes. Style Shelter, by default, replaces 118 | the current array of stylesheets with the one you specify using the new "adoptedStyleSheets" method. 119 | 120 | ```javascript 121 | scope.adoptedStyleSheets = [someStyleSheets]; 122 | ``` 123 | 124 | However, especially in the case of the page document object, you likely don't want to 125 | replace the entire stylesheet array from an individual component, since it does affect the entire page. 126 | 127 | This is why the **append** array contains the **document** object by default. Instead of replacing, stylesheets on the **document** 128 | scope, it will add on, instead: 129 | 130 | ```javascript 131 | scope.adoptedStyleSheets = scope.adoptedStyleSheets.concat([someStyleSheets]); 132 | ``` 133 | 134 | The **onSuccess** key is a success callback when the stylesheet has been successfully loaded. 135 | With Constructable Style Sheets, CSS can be loaded synchronously, however, Style Shelter loads them 136 | asynchronously because it uses the `fetch` api for loading CSS as text. Though the newly constructed sheet comes back synchronously, 137 | the CSS takes time to load. This callback allows a user to take action on load. One todo for Style Shelter could be to 138 | wait until load until the style sheet is adopted, however right now there is no option to do so. 139 | 140 | The **onError** key is what you'd expect. If your stylesheet fails to load, it will produce an error. Also, if you fail 141 | to specify scope (either as default or in an object), **onError** will fire. By default, **onError** will console.warn with your error. 142 | 143 | ### Polyfilling 144 | Currently, Chrome is the only browser to support Constructable Stylesheets, the underlying feature behind Style Shelter. However, there is 145 | a polyfill that simulates the desired results. 146 | 147 | https://www.npmjs.com/package/construct-style-sheets-polyfill 148 | 149 | Unfortunately Constructable Stylesheets cannot be completely replicated with a polyfill. 150 | 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. 151 | 152 | ![alt text](chrome-nopolyfill.png "Chrome without polyfill usage") 153 | 154 | Notice that there are no stylesheets visible here - they are all adopted as intended. 155 | 156 | This next screen capture is from Firefox, where Constructed Stylesheets aren't supported, and the polyfill is being used. 157 | 158 | ![alt text](firefox-withpolyfill.png "Firefox with polyfill usage") 159 | 160 | Notice that these CSS are created inside the Shadow DOM, and imagine a large number 161 | of component instances where we are constantly replicating the same stylesheets over and over again 162 | for a design system. Obviously, not polyfilling will be better - so hopefully Firefox and Safari support Constructable Stylesheets soon. 163 | 164 | ### Using CSS Modules in the Future 165 | 166 | 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 167 | the HTML page from having to know what CSS files are required throughout the application. Instead, each Web Component can be responsible for 168 | loading exactly the CSS it needs. 169 | 170 | 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 171 | CSS in a JS file. 172 | 173 | ```javascript 174 | css() { 175 | return `