Reputation: 2702
I recently started getting into WebComponents and I must say I have found it ground breaking so far! To get a better grasp of the concept, I am trying to adopt designs from my earlier project into reusable web components. I started by trying to create a Table element with some added features.
First of, I declared a minimal template for the component:
<template id="data-table-template">
<table>
<thead></thead>
<tbody></tbody>
<tfoot></tfoot>
</table>
</template>
I use this template to create tables by taking table header values and row values as attributes. The element is used in the following manner:
<eur-data-table
headers="getHeaders()",
rowdata="getRowData()"
host="#data-table-host"
template="#data-table-template"
></eur-data-table>
In the component prototype connectedCallback
method, I add the headers, rows and set style for the component. The generated shadow DOM fragment from Chrome Element inspector is as follows:
<eur-data-table headers="getHeaders()" ,="" rowdata="getRowData()" host="#data-table-host" template="#data-table-template" theme="firebrick">
<table>
<style id="styleBlock"></style>
<thead>
<th>Name</th>
<th>Age</th>
</thead>
<tbody>
<tr>
<td>Babu</td>
<td>60</td>
</tr>
<tr>
<td>Shyam</td>
<td>35</td>
</tr>
</tbody>
<tfoot></tfoot>
</table>
</eur-data-table>
The problem lies in adding style rules. In the component class, I create a style
element and I append it to the shadow DOM before adding rules to the style.sheet
property. I checked the console output of style.sheet
and it looks like all the rules were added. However, after inserting the style element to the shadow DOM, there are no rules as can be seen from the rendered output above.
In the component class, style is added in the following way:
connectedCallback() {
let template = document.querySelector(DataTable._parseId(this.template));
if(!template) throw new DOMException('Host and/or template ids are undefined');
if(!this.hasAttribute('theme')) this.setAttribute('theme', 'firebrick');
let style = document.createElement('style');
style.setAttribute('id', 'styleBlock');
// WebKit Hack
style.appendChild(document.createTextNode(""));
this.shadowRoot.appendChild(style);
this._table = template.content.cloneNode(true);
this._setupHeaders();
this._setupBody();
this._setupStyle(style.sheet); // style is not being applied!!
console.dir(style.sheet); // logs a CSSStyleSheet object
this.shadowRoot.appendChild(this._table);
}
In the connectedCallback
method, _setupStyle
is passed a reference to the style sheet where, it adds the rules in the following manner (the rules are expressed as javascript object):
_setupStyle(sheet) {
if(!sheet) throw new DOMException('sheet is undefined', 'StylesheetNotFoundException');
let rules = window.themes[this.theme];
if(!rules) throw new DOMException('Theme is undefined');
Object.keys(rules)
.sort((r1, r2) => rules[r1].order - rules[r2].order)
.forEach(rule => {
let o = _.omit(rules[rule], 'order');
let i = rule.order;
let s = this._makeRule(o, rule);
sheet.insertRule(s, i);
});
}
The order
value is an integer number used for sorting the style rules in order. Once sorted, the order
key and its value is omitted from the object before converting it to a DOMString:
_makeRule(value, rule) {
return `${rule} ${JSON.stringify(value).replace(new RegExp(/,/g), ';')}`;
}
Logging the style.sheet
property prints:
CSSStyleSheet
cssRules: CSSRuleList
0: CSSStyleRule {selectorText: "tfoot", style: CSSStyleDeclaration, styleMap: StylePropertyMap, type: 1, cssText: "tfoot { }", …}
1: CSSStyleRule {selectorText: "tr:last-child > td", style: CSSStyleDeclaration, styleMap: StylePropertyMap, type: 1, cssText: "tr:last-child > td { }", …}
2: CSSStyleRule {selectorText: "td:last-child", style: CSSStyleDeclaration, styleMap: StylePropertyMap, type: 1, cssText: "td:last-child { }", …}
3: CSSStyleRule {selectorText: "td", style: CSSStyleDeclaration, styleMap: StylePropertyMap, type: 1, cssText: "td { }", …}
4: CSSStyleRule {selectorText: "tr:first-child", style: CSSStyleDeclaration, styleMap: StylePropertyMap, type: 1, cssText: "tr:first-child { }", …}
5: CSSStyleRule {selectorText: "tr:last-child", style: CSSStyleDeclaration, styleMap: StylePropertyMap, type: 1, cssText: "tr:last-child { }", …}
6: CSSStyleRule {selectorText: "tr", style: CSSStyleDeclaration, styleMap: StylePropertyMap, type: 1, cssText: "tr { }", …}
7: CSSStyleRule {selectorText: "th:last-child", style: CSSStyleDeclaration, styleMap: StylePropertyMap, type: 1, cssText: "th:last-child { }", …}
8: CSSStyleRule {selectorText: "th", style: CSSStyleDeclaration, styleMap: StylePropertyMap, type: 1, cssText: "th { }", …}
9: CSSStyleRule {selectorText: "thead", style: CSSStyleDeclaration, styleMap: StylePropertyMap, type: 1, cssText: "thead { }", …}
10: CSSStyleRule {selectorText: "thead tbody tfoot", style: CSSStyleDeclaration, styleMap: StylePropertyMap, type: 1, cssText: "thead tbody tfoot { }", …}
11: CSSStyleRule {selectorText: "table", style: CSSStyleDeclaration, styleMap: StylePropertyMap, type: 1, cssText: "table { }", …}
My question is: since the component gets rendered without error and since, I am adding all the rules as expected and attaching both, the style and table elements to the shadow DOM renders an element as expected then, why aren't my CSS rules being applied to the rendered content?
Upvotes: 0
Views: 1249
Reputation: 2702
Okay I finally found a working solution. it is much easier to add the style in the innerHTML
property of the style tag. To do this, I needed to change the _setupStyle
and _makeRule
method (posted in the question) to the following:
_setupStyle(style) {
if(!style) throw new DOMException('sheet is undefined', 'StylesheetNotFoundException');
let rules = window.themes[this.theme];
if(!rules) throw new DOMException('Theme is undefined');
style.innerHTML = "";
Object.keys(rules)
.sort((r1, r2) => rules[r1].order - rules[r2].order)
.forEach(rule => {
let o = _.omit(rules[rule], 'order');
let i = rule.order;
let s = this._makeRule(o, rule);
//sheet.insertRule(s, i);
style.innerHTML += s + ' \n';
});
}
I am no longer using CSSStyleSheet
class for adding rules (commented out); instead, I am just adding each rule on a new line. The _makeRule
method is as follws:
_makeRule(value, rule) {
return `${rule} ${JSON.stringify(value).replace(this._cssParseRule, ';').replace(/\'|\"/g, "")}`;
}
I modified the method to converted object into string and then replacing ,
with ;
followed by replacing all '
or "
. The rendered output is as follows:
<style id="styleBlock">
table {border:1px solid firebrick;border-collapse:collapse;border-spacing:0;padding:0}
thead tbody tfoot {margin:0;padding:0}
thead {border-bottom:1px solid firebrick;background-color:firebrick;color:azure}
th {border-right:1px solid firebrick;border-bottom:1px solid firebrick}
th:last-child {border-bottom:none}
tr {margin:0;border-bottom:1px solid firebrick}
tr:last-child {border-bottom:none}
tr:first-child {border-left:none}
td {margin:0;padding:10px;text-align:center;border-left:1px solid firebrick;border-right:1px solid firebrick;border-bottom:1px solid firebrick}
td:last-child {border-right:none}
tr:last-child > td {border-bottom:none}
tfoot {margin:0;border-top:1px solid firebrick}
</style>
Note: Although the CSS is working, it is not correct; there should be a ;
at the end of the last property for each rule.
Upvotes: 0
Reputation: 7681
for the time being, until the shadow parts api becomes available (which will allow us to declare whole elements as styleable by consumers)
i recommend using lots of custom css properties (style hooks) to provide a means for a consumer to influence the styling of your web components
there might be some polyfill support for a proposed property adoptedStyleSheets
for the purposes of theming, but i've only seen it casually mentioned in some threads all rumor-like
Upvotes: 0