Importing styles into a web component - html

What is the canonical way to import styles into a web component?
The following gives me an error HTML element <link> is ignored in shadow tree:
<template>
<link rel="style" href="foo.css" />
<h1>foo</h1>
</template>
I am inserting this using shadow DOM using the following:
var importDoc, navBarProto;
importDoc = document.currentScript.ownerDocument;
navBarProto = Object.create(HTMLElement.prototype);
navBarProto.createdCallback = function() {
var template, templateClone, shadow;
template = importDoc.querySelector('template');
templateClone = document.importNode(template.content, true);
shadow = this.createShadowRoot();
shadow.appendChild(templateClone);
};
document.registerElement('my-nav-bar', {
prototype: navBarProto
});

Now direct <link> tag is supported in shadow dom.
One can directly use:
<link rel="stylesheet" href="yourcss1.css">
<link href="yourcss2.css" rel="stylesheet" type="text/css">
It has been approved by both whatwg and W3C.
Useful links for using css in shadow dom:
https://w3c.github.io/webcomponents/spec/shadow/#inertness-of-html-elements-in-a-shadow-tree
https://github.com/whatwg/html/commit/43c57866c2bbc20dc0deb15a721a28cbaad2140c
https://github.com/w3c/webcomponents/issues/628
Direct css link can be used in shadow dom.

If you need to place external styles inside the <template> tag you could try
<style> #import "../my/path/style.css"; </style>
however I have a feeling this will start importing after the element has been created.

Answer no longer valid
The #import syntax was removed from CSSStyleSheet.replace()
Chrome
Mozilla
Constructable Stylesheets
This is a new feature that allows for the construction of CSSStyleSheet objects. These can have their contents set or imported from a css file using JavaScript and be applied to both documents and web components' shadow roots. It will be available in Chrome with version 73 and probably in the near future for Firefox.
There's a good writeup on the Google developers site but I'll summarize it briefly below with an example at the bottom.
Creating a style sheet
You create a new sheet by calling the constructor:
const sheet = new CSSStyleSheet();
Setting and replacing the style:
A style can be applied by calling the methods replace or replaceSync.
replaceSync is synchronous, and can't use any external resources:
sheet.replaceSync(`.redText { color: red }`);
replace is asynchronous and can accept #import statements referencing external resources. Note that replace returns a Promise which needs to be handled accordingly.
sheet.replace('#import url("myStyle.css")')
.then(sheet => {
console.log('Styles loaded successfully');
})
.catch(err => {
console.error('Failed to load:', err);
});
Applying the style to a document or shadow DOM
The style can be applied by setting the adoptedStyleSheets attribute of either the document or a shadow DOM.
document.adoptedStyleSheets = [sheet]
The array in adoptedStyleSheets is frozen and can't be mutated with push(), but you can concatenate by combining with its existing value:
document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet];
Inheriting from the document
A shadow DOM can inherit constructed styles from the document's adoptedStyleSheets in the same way:
// in the custom element class:
this.shadowRoot.adoptedStyleSheets = [...document.adoptedStyleSheets, myCustomSheet];
Note that if this is run in the constructor, the component will only inherit the style sheets that were adopted prior to its creation. Setting adoptedStyleSheets in the connectedCallback will inherit for each instance when it is connected. Notably, this will not cause an FOUC.
Example with Web Components
Let's create a component called x-card that wraps text in a nicely styled div.
// Create the component inside of an IIFE
(function() {
// template used for improved performance
const template = document.createElement('template');
template.innerHTML = `
<div id='card'></div>
`;
// create the stylesheet
const sheet = new CSSStyleSheet();
// set its contents by referencing a file
sheet.replace('#import url("xCardStyle.css")')
.then(sheet => {
console.log('Styles loaded successfully');
})
.catch(err => {
console.error('Failed to load:', err);
});
customElements.define('x-card', class extends HTMLElement {
constructor() {
super();
this.attachShadow({
mode: 'open'
});
// apply the HTML template to the shadow DOM
this.shadowRoot.appendChild(
template.content.cloneNode(true)
);
// apply the stylesheet to the shadow DOM
this.shadowRoot.adoptedStyleSheets = [sheet];
}
connectedCallback() {
const card = this.shadowRoot.getElementById('card');
card.textContent = this.textContent;
}
});
})();
<x-card>Example Text</x-card>
<x-card>More Text</x-card>

NB!!!
THIS ANSWER IS OUTDATED
PLEASE CHECK THE ANSWER BY Himanshu Sharma
Up-to-date answer: https://stackoverflow.com/a/48202206/2035262
According to Polymer documentation:
Polymer allows you to include stylesheets in your <polymer-element> definitions, a feature not supported natively by Shadow DOM.
This is a bit weird reference, but I could not google the straight one. It looks like at the moment there is no rumors about supporting links inside templates.
That said, whether you want to use vanilla web component, you should either inline your css with <style> tag, or load and apply your css manually in javascript.

The above answers show how to import stylesheets into a web component, but importing a single style to a shadow DOM can be done (kind-of) programmatically. This is the technique I developed recently.
First - make sure that you embed your component-local styles directly in a template with the HTML code. This is to make sure that the shadow DOM will have a stylesheet in your element constructor. (importing other stylesheets should be ok, but you must have one ready in the constructor)
Second - use a css-variable to point at a css rule to import.
#rule-to-import {
background-color: #ffff00;
}
my-element {
--my-import: #rule-to-import;
}
Third - In the component constructor, read the CSS variable and locate the pointed to style in the document stylesheets. When found, copy the string but rewrite the selector to match the internal element(s) you wish to style. I use a helper function for this.
importVarStyle(shadow,cssvar,target) {
// Get the value of the specified CSS variable
const varstyle=getComputedStyle(this).getPropertyValue(cssvar).trim();
if(varstyle!="") varstyle: {
const ownstyle=shadow.styleSheets[0];
for(let ssheet of document.styleSheets) { // Walk through all CSS rules looking for a matching rule
for(let cssrule of ssheet.cssRules) {
if(cssrule.selectorText==varstyle) { // If a match is found, re-target and clone the rule into the component-local stylesheet
ownstyle.insertRule(
cssrule.cssText.replace(/^[^{]*/,target),
ownstyle.cssRules.length
);
break varstyle;
}
}
}
}
}

Try the <style> element inside of <template>:
<template>
<style>
h1 {
color: red;
font-family: sans-serif;
}
</style>
<h1>foo</h1>
</template>

Related

How do you generate dynamic <style> tag content in Angular template at runtime?

I have an Angular component that generates mat-checkbox dynamically at runtime and I need to change the individual background of each checkbox differently with different color and I don't (won't) have the information before hand, only available at runtime.
I have the following ng-template for the checkboxes:
<ng-template #renderCheckbox let-id="id" let-attr="attr">
<mat-checkbox
[checked]="attr.show"
[color]="'custom-' + id"
(change)="onChange($event.checked, attr)">
{{attr.name}}
</mat-checkbox>
</ng-template>
where, attr in the template has the following interface type, these infomation are pulled from Highcharts' series and I didn't want to hardcode the color.
interface LinkedSeriesAttributes {
id: string;
name: string;
index: number;
color: string;
checked: boolean;
}
Since there is no way to create css classes before hand and there is no way to directly apply color to the mat-checkbox, I could only generate the <style>...</style> right at the beginning of my template.
In my component, I have code that will generate the style which would give me something like this:
.mat-checkbox.mat-custom-hello.mat-checkbox-checked .mat-checkbox-background::before {
color: #6E8BC3 !important;
}
.mat-checkbox.mat-custom-world.mat-checkbox-checked .mat-checkbox-background::before {
color: #9ED6F2 !important;
}
...
However, I tried various ways to dump it inside <style> without success. I tried:
<style>{{ dynamicCSSStyles }}</style>
Which, my IDE shows that's an error with the curly braces, although it compiled fine and ran without errors, I got nothing, can't even see the <style> tag.
I also tried to include <style> inside my dynamicCSSStyles variable, and angular just dumped the whole thing out as text...
What's the correct way to generate a <style> in Angular.
I've found a REALLY dirty way of "making this work" but it causes Angular to keep adding the <style> back into the DOM.
First, set encapsulation to ViewEncapsulation.None.
Second, create a function to generate the <style> tag the old fashion way with an id:
updateDynsmicStyleNode() {
const id = 'dynamic-css-styles';
const nativeElm = this.elmRef.nativeElement;
const existing = nativeElm.querySelector(`style#${id}`);
if (!existing) {
const styleTag = document.createElement('style');
styleTag.setAttribute('id', id);
styleTag.innerHTML = this.dynamicCSSStyles;
nativeElm.prepend(styleTag);
} else {
existing.innerHTML = this.dynamicCSSStyles;
}
}
Third, call our function in ngAfterViewChecked:
ngAfterViewChecked() {
this.updateDynsmicStyleNode();
}
I mean while this worked, it is really bad, since moving the mouse around the screen would cause Angular to just continuously reinsert the <style> tag.
Does anyone know some other way more legit to archive this? LOL
You can use ngClass or [class] attribute. Since you can have the styles ready from the component.ts file.
You can do something like this:
Way 1: If you already know what the dynamic ids might be, (like if it always will be 'hello' and 'world')
let dynamicClasses = {};
// Once you get some classes from your logic, you can add them to the object above
dynamicClasses['hello'] = 'custom-hello';
dynamicClasses['world'] = 'custom-world';
// Then in HTML
<mat-checkbox [ngClass]="dynamicClasses"></mat-checkbox>
Way 2: If you dont know what the classes also might be, like if its not always be hello or world, then create a method and call it where required, you might need to do something similar to #codenamezero said.

html5 template tag: fallback for content access

I am using a template tag in a webkit browser (JavaFX WebView 2.2) to store elements that I may clone and append on the main part of the document.
However, I can't access its content using templateElement.content (the HTML5 standard). Instead, I use jQuery to get the elements inside the template TAG with the selector "#templateElement div".
Seems the template tag is not yet fully supported (inner scripts also run), although its contents are not rendered.
My fear is that, when the template tag becomes supported, the way to get its contents will break and my page will stop working.
What is the recommended way of getting template contents regardless future implementation changes?
HTML:
<template id="templateElement">
<div>Clone Me!</div>
</template>
JavaScript:
function getContentsCurrent() {
var toBeCloned = $("#templateElement div")[0];
//append where needed...
}
function getContentsFuture() {
var toBeCloned = templateElement.content.getElementsByTagName("div")[0];
//append where needed...
}
EDIT
I think jQuery won't be able to handle this automatically, even in the future, because the template "innerHTML" is purposely routed to content so that it becomes inaccessible to the DOM (so no selector touches it accidentally).
You could test if the content feature exists before:
function getContents() {
var toBeCloned;
if ( templateElement.content )
toBeCloned = templateElement.content.getElementsByTagName("div")[0];
else
toBeCloned = templateElement.querySelector("div");
//append where needed...
}
Another way:
var content = templateElement.content || templateElement
var toBeCloned = content.querySelector( "div" )
//...

Cannot select elements inside "auto-binding" template

I have created some custom elements, now I'm writing tests for them.
I wanted to use "auto-binding" because I have plenty of attributes that needs to be bound among my elements.
Unfortunately I cannot query any element inside the template.
Here is some code.
<template id="root" is="auto-binding">
<dalilak-data-service id="dds" regions="{{regions}}"></dalilak-data-service>
<dalilak-regions-handler id="drh" regions="{{regions}}" flattendedRegions="{{flattendRegions}}" descendantsRegionNames="{{descendantsRegionNames}}" regionsByNameId="{{regionsByNameId}}"></dalilak-regions-handler>
</template>
In the test script I have tried the following
drh = document.querySelector('#drh');
suite('dalilak-regions-handler', function() {
test('handler initialized', function() {
assert.ok(drh);
});
});
Also tried this:
drh = document.querySelector('* /deep/ #drh'); // or '#root /deep/ #drh'
suite('dalilak-regions-handler', function() {
test('handler initialized', function() {
assert.ok(drh);
});
});
But none of them worked.
Note without the template I can query my custom elements.
auto-binding templates stamp asynchronously, I expect your problem is that you need to wait for the template to stamp before querying for elements.
The template fires a template-bound event when this happens, so you can use code like this:
addEventListener('template-bound', function() {
drh = document.querySelector('#drh');
...
});
Of course, this means your testing infrastructure will need to understand how to handle asynchrony, which can be a concern.
Where possible, it is best to avoid the /deep/ selector. That is a nuclear option and can return unexpected results because it pierces all shadow DOMs. It also won't work for your auto-binding template because its contents are inside a #document-fragment, not a #shadow-root. Instead, try querying the #document-fragment itself. This preferable because you are limiting your query to the scope of your template, which is much more precise.
var template = document.querySelector('#root');
var drh = template.content.querySelector('#drh');

Is there a way to specify an HTML5 custom element should be used only once per document?

I searched around on Google, here on StackOverflow, probably-now-outdated HTML5 spec's, and have not found an answer. I feel as though I'm missing something obvious.
I'm wondering if there is a way to specify when creating an HTML5 custom element, that users of that new element should (or must, to be 'valid' to the element's spec) only use it once per document?
For example with HTML's elements, 'head', 'body', 'main', etc., should only be used once within a document. I have not been able to find a way to do this with custom elements. Is this possible, either with vanilla HTML5, Polymer, or some other means?
Thanks to any who can help.
Use built-in callbacks to track the usage of the custom element:
var MyElementPrototype = Object.create(HTMLElement.prototype);
MyElementPrototype.len = 0;
MyElementPrototype.attachedCallback = function() {
MyElementPrototype.len++;
if (MyElementPrototype.len > 1) {
alert('The Document is not Valid'); // Do Something
}
};
MyElementPrototype.detachedCallback = function() {
MyElementPrototype.len--;
};
document.registerElement(
'my-element',
{
prototype: MyElementPrototype
}
);
If you just want to validate the document, you can do it easily with JavaScript.
JsFiddle: http://jsfiddle.net/4AXaS/
HTML:
<div>Lorem</div>
<div>Ipsum</div>
JavaScript:
$(function () {
if ($('div').length > 1) {
alert("Can't use this element more than once in the document");
}
});

How to select element inside open Shadow DOM from Document?

Say I have a DOM that looks like this in my Document:
<body>
<div id="outer">
<custom-web-component>
#shadow-root (open)
<div id="inner">Select Me</div>
</custom-web-component>
</div>
</body>
Is it possible to select the inner div inside the shadow root using a single querySelector argument on document? If so, how is it constructed?
For example, something like document.querySelector('custom-web-component > #inner')
You can do it like this:
document.querySelector("custom-web-component").shadowRoot.querySelector("#inner")
In short, not quite. The TL:DR is that, depending on how the component is set up, you might be able to do something like this:
document.querySelector('custom-web-component').div.innerHTML = 'Hello world!';
Do do this - if you have access to where the web component is created, you can add an interface there to access inner content. You can do this the same way you would make any JavaScript class variable/method public. Something like:
/**
* Example web component
*/
class MyComponent extends HTMLElement {
constructor() {
super();
// Create shadow DOM
this._shadowRoot = this.attachShadow({mode: 'open'});
// Create mock div - this will be directly accessible from outside the component
this.div = document.createElement('div');
// And this span will not
let span = document.createElement('span');
// Append div and span to shadowRoot
this._shadowRoot.appendChild(span);
this._shadowRoot.appendChild(this.div);
}
}
// Register component
window.customElements.define('custom-web-component', MyComponent);
// You can now access the component 'div' from outside of a web component, like so:
(function() {
let component = document.querySelector('custom-web-component');
// Edit div
component.div.innerHTML = 'EDITED';
// Edit span
component._shadowRoot.querySelector('span').innerHTML = 'EDITED 2';
})();
<custom-web-component></custom-web-component>
In this instance, you can access the div from outside of the component, but the span is not accessible.
To add: As web components are encapsulated, I don't think you can otherwise select internal parts of the component - you have to explicitly set a way of selecting them using this, as above.
EDIT:
Saying that, if you know what the shadow root key is, you can do this: component._shadowRoot.querySelector() (added to demo above). But then that is quite a weird thing to do, as it sorta goes against the idea of encapsulation.
EDIT 2
The above method will only work is the shadow root is set using the this keyword. If the shadow root is set as let shadowRoot = this.attachShadow({mode: 'open'}) then I don't think you will be able to search for the span - may be wrong there though.
This code will behave like query selector and work on nested shadowDoms:
const querySelectorAll = (node,selector) => {
const nodes = [...node.querySelectorAll(selector)],
nodeIterator = document.createNodeIterator(node, Node.ELEMENT_NODE);
let currentNode;
while (currentNode = nodeIterator.nextNode()) {
if(currentNode.shadowRoot) {
nodes.push(...querySelectorAll(currentNode.shadowRoot,selector));
}
}
return nodes;
}