Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support passing a {Function} (options.insertInto) #279

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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 All @@ -43,9 +55,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
Original file line number Diff line number Diff line change
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 (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
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@
"insertAt": {
"type": ["string", "object"]
},
"insertInto": {
"type": "string"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like we need new keyword in ajv for this

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, a newer version of schema-utils is needed

},
"singleton": {
"type": "boolean"
},
Expand Down
20 changes: 19 additions & 1 deletion test/basicTest.js
Original file line number Diff line number Diff line change
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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really want to test with shadow root at first, but jsdom not working with shadow dom. Maybe some day we can use smth like headless-chrome for testing

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"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
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = function() {
return document.querySelector("#test-shadow");
};