Skip to content

Commit

Permalink
feat: support passing a {Function} (options.insertInto) (#279)
Browse files Browse the repository at this point in the history
  • Loading branch information
savelichalex authored and michael-ciniawsky committed Jan 26, 2018
1 parent 3a4cb53 commit 0eb8fe7
Show file tree
Hide file tree
Showing 6 changed files with 76 additions and 20 deletions.
19 changes: 16 additions & 3 deletions README.md
Expand Up @@ -139,7 +139,7 @@ Styles are not added on `import/require()`, but instead on call to `use`/`ref`.
|**`attrs`**|`{Object}`|`{}`|Add custom attrs to `<style></style>`|
|**`transform`** |`{Function}`|`false`|Transform/Conditionally load CSS by passing a transform/condition function|
|**`insertAt`**|`{String\|Object}`|`bottom`|Inserts `<style></style>` at the given position|
|**`insertInto`**|`{String}`|`<head>`|Inserts `<style></style>` into the given position|
|**`insertInto`**|`{String|Function}`|`<head>`|Inserts `<style></style>` into the given position|
|**`singleton`**|`{Boolean}`|`undefined`|Reuses a single `<style></style>` element, instead of adding/removing individual elements for each required module.|
|**`sourceMap`**|`{Boolean}`|`false`|Enable/Disable Sourcemaps|
|**`convertToAbsoluteUrls`**|`{Boolean}`|`false`|Converts relative URLs to absolute urls, when source maps are enabled|
Expand Down Expand Up @@ -318,14 +318,27 @@ A new `<style>` element can be inserted before a specific element by passing an

### `insertInto`
By default, the style-loader inserts the `<style>` elements into the `<head>` tag of the page. If you want the tags to be inserted somewhere else you can specify a CSS selector for that element here. If you target an [IFrame](https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement) make sure you have sufficient access rights, the styles will be injected into the content document head.
You can also insert the styles into a [ShadowRoot](https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot), e.g

You can also pass function to override default behavior and insert styles in your container, e.g

**webpack.config.js**
```js
{
loader: 'style-loader',
options: {
insertInto: () => document.querySelector("#root"),
}
}
```

Using function you can insert the styles into a [ShadowRoot](https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot), e.g

**webpack.config.js**
```js
{
loader: 'style-loader',
options: {
insertInto: '#host::shadow>#root'
insertInto: () => document.querySelector("#root").shadowRoot,
}
}
```
Expand Down
23 changes: 19 additions & 4 deletions index.js
Expand Up @@ -17,7 +17,19 @@ module.exports.pitch = function (request) {

validateOptions(require('./options.json'), options, 'Style Loader')

options.hmr = typeof options.hmr === 'undefined' ? true : options.hmr;
options.hmr = typeof options.hmr === 'undefined' ? true : options.hmr;

// need to use this variable, because function should be inlined
// if just store it in options, then after JSON.stringify
// function will be quoted and then in runtime will be just string
var insertInto;
if (typeof options.insertInto === "function") {
insertInto = options.insertInto.toString();
}
// we need to check if it string, or variable will be "undefined" and loader crash then
if (typeof options.insertInto === "string") {
insertInto = '"' + options.insertInto + '"';
}

var hmrCode = [
"// Hot Module Replacement",
Expand Down Expand Up @@ -52,9 +64,12 @@ module.exports.pitch = function (request) {
"if(typeof content === 'string') content = [[module.id, content, '']];",
"// Prepare cssTransformation",
"var transform;",
options.transform ? "transform = require(" + loaderUtils.stringifyRequest(this, "!" + path.resolve(options.transform)) + ");" : "",
"var options = " + JSON.stringify(options),
"options.transform = transform",
"var insertInto;",
options.transform ? "transform = require(" + loaderUtils.stringifyRequest(this, "!" + path.resolve(options.transform)) + ");" : "",
"insertInto = " + insertInto + ";" ,
"var options = " + JSON.stringify(options),
"options.transform = transform",
"options.insertInto = insertInto;",
"// add the styles to the DOM",
"var update = require(" + loaderUtils.stringifyRequest(this, "!" + path.join(__dirname, "lib", "addStyles.js")) + ")(content, options);",
"if(content.locals) module.exports = content.locals;",
Expand Down
28 changes: 19 additions & 9 deletions lib/addStyles.js
Expand Up @@ -23,12 +23,24 @@ var isOldIE = memoize(function () {
return window && document && document.all && !window.atob;
});

var getTarget = function (target) {
return document.querySelector(target);
};

var getElement = (function (fn) {
var memo = {};

return function(selector) {
if (typeof memo[selector] === "undefined") {
var styleTarget = fn.call(this, selector);
return function(target) {
// If passing function in options, then use it for resolve "head" element.
// Useful for Shadow Root style i.e
// {
// insertInto: function () { return document.querySelector("#foo").shadowRoot }
// }
if (typeof target === 'function') {
return target();
}
if (typeof memo[target] === "undefined") {
var styleTarget = getTarget.call(this, target);
// Special case to return head of iframe instead of iframe itself
if (window.HTMLIFrameElement && styleTarget instanceof window.HTMLIFrameElement) {
try {
Expand All @@ -39,13 +51,11 @@ var getElement = (function (fn) {
styleTarget = null;
}
}
memo[selector] = styleTarget;
memo[target] = styleTarget;
}
return memo[selector]
return memo[target]
};
})(function (target) {
return document.querySelector(target)
});
})();

var singleton = null;
var singletonCounter = 0;
Expand All @@ -67,7 +77,7 @@ module.exports = function(list, options) {
if (!options.singleton && typeof options.singleton !== "boolean") options.singleton = isOldIE();

// By default, add <style> tags to the <head> element
if (!options.insertInto) options.insertInto = "head";
if (!options.insertInto) options.insertInto = "head";

// By default, add <style> tags to the bottom of the target
if (!options.insertAt) options.insertAt = "bottom";
Expand Down
3 changes: 0 additions & 3 deletions options.json
Expand Up @@ -14,9 +14,6 @@
"insertAt": {
"type": ["string", "object"]
},
"insertInto": {
"type": "string"
},
"singleton": {
"type": "boolean"
},
Expand Down
20 changes: 19 additions & 1 deletion test/basicTest.js
Expand Up @@ -35,6 +35,13 @@ describe("basic tests", function() {
"<iframe class='iframeTarget'/>",
"</body>",
"</html>"
].join("\n"),
requiredJS = [
"var el = document.createElement('div');",
"el.id = \"test-shadow\";",
// "var shadow = el.attachShadow({ mode: 'open' })", // sadly shadow dom not working in jsdom
"document.body.appendChild(el)",
"var css = require('./style.css');",
].join("\n");

var styleLoaderOptions = {};
Expand Down Expand Up @@ -66,7 +73,7 @@ describe("basic tests", function() {

// Create a tiny file system. rootDir is used because loaders are referring to absolute paths.
fs.mkdirpSync(rootDir);
fs.writeFileSync(rootDir + "main.js", "var css = require('./style.css');");
fs.writeFileSync(rootDir + "main.js", requiredJS);
fs.writeFileSync(rootDir + "style.css", requiredCss);
fs.writeFileSync(rootDir + "styleTwo.css", requiredCssTwo);
fs.writeFileSync(rootDir + "localScoped.css", localScopedCss);
Expand Down Expand Up @@ -140,6 +147,17 @@ describe("basic tests", function() {
}, selector);
}); // it insert into

it("insert into custom element by function", function(done) {
const selector = "#test-shadow";
styleLoaderOptions.insertInto = () => document.querySelector("#test-shadow");

let expected = requiredStyle;

runCompilerTest(expected, done, function() {
return this.document.querySelector(selector).innerHTML;
}, selector);
});

it("singleton (true)", function(done) {
// Setup
styleLoaderOptions.singleton = true;
Expand Down
3 changes: 3 additions & 0 deletions test/insert/into.js
@@ -0,0 +1,3 @@
module.exports = function() {
return document.querySelector("#test-shadow");
};

0 comments on commit 0eb8fe7

Please sign in to comment.