For a Single Page Application I have registered some custom elements to the customElementRegistry which are rendered from string literals via insertAdjacentHTML(). Nesting these elements is no problem when done in html. But when I try to instantiate a parent custom element programmatically starting from customElements.get('entry-page') or document.createElement('div', {is: 'entry-page'}), the nested custom elements do not render as if not defined at all. Even customElements.whenDefined('nested-element').then(...) doesn't help.
function registerCustomElement(name, tpl) {
class cls extends HTMLDivElement {
connectedCallback() {
this.insertAdjacentHTML('afterbegin', tpl)
}
};
customElements.define(name, cls, {
extends: 'div'
})
}
let tpl1 = '<p class="box">inner HTML</p>'
let tpl2 = '<article class="outer">outer HTML<inner-html></inner-html></article>'
let tpl3 = '<article class="outer">revised outer HTML<div is="inner-html"></div></article>'
registerCustomElement('inner-html', tpl1)
registerCustomElement('outer-html', tpl2)
registerCustomElement('outer-html-revised', tpl3)
document.querySelector('main')
.insertAdjacentHTML('beforeend', '<div is="outer-html"></div>')
document.querySelector('main')
.insertAdjacentHTML('beforeend', '<div is="outer-html-revised"></div>')
body {
font-family: 'sans serif';
font-size: 0.8em
}
.box {
padding: 1em;
color: white;
background-color: olive;
}
.outer {
padding: 0.5em;
margin-bottom: 0.5em;
border: 2px solid darkorange
}
<h1>Embedded in html</h1>
<h2>version with 'inner-html' element</h2>
<outer-html></outer-html>
<h2>version with 'div is inner-html' element</h2>
<outer-html-revised></outer-html-revised>
<h1>Embedded programmatically</h1>
<main></main>
Each element is defined separately as a ES6 Module. I am currently not using shadow DOM.
Why a custom element does fully render including all nested elements when embedded in HTML, but not when implemented in the DOM programmatically?
Edit As Danny Engelman stated, the only cross-browser option is to extend HTMLElement.
While in HTML the custom elements may be referenced by there names even in the string literals, the custom elements are based upon, this seems not to be true, when they are generated programmatically. In this case the spec requires something like <div is="custom-element-name">, but it wouldn't be cross-browser, as Apple only supports HTMLElement.
Related
I'm new to web development and I've been trying to apply styling to div elements that are generated using javascript, but I've only been able to do this manually using the following function:
function addStyle(element) {
rem = element.style;
rem.fontSize = "16px";
rem.background = "#fff";
rem.color = "#333";
}
This works fine for individual elements, but there might be potentially dozens of dynamic elements created which all must include the same inline styling. I've read elsewhere that apparently this is not a good practice and should be avoided if possible. Thus, I would prefer that these css rules are defined in a separate file so that I can potentially access them for other elements as well.
However, I have been unable to find a solution anywhere online no matter how similar their issue may seem.
Here's my relevant HTML code:
<h3 id="kudos_title">Kudos</h3>
<div class="remarks"></div>
My JS to create new div element:
function addElement (i) {
// create a new div element
const newDiv = document.createElement("div");
// Add message, author, and source to content
const content = document.createTextNode('"' + `${msg[i].remark}` + '"' + " - " + `${msg[i].author}` + " from " + `${msg[i].source}`);
// add the text node to the newly created div
newDiv.appendChild(content);
addStyle(newDiv, i);
// add the newly created element and its content into the DOM
const current_remark = document.querySelector(".remarks");
document.body.insertBefore(newDiv, current_remark);
}
Lastly, the CSS:
#kudos_title {
text-align: center;
font-family: Spectral, serif;
font-size: 50px;
}
.remarks {
padding: 20px;
color: pink;
background-color: springgreen;
}
I should mention that the heading with id=kudos_title is successfully styled, but anything part of the remarks class is not. So clearly the .css file is being recognized for static elements, but JS created divs are not.
You are using insertBefore which will insert the element before the target (thus NOT inserting inside it). Try appending instead. Additionally, it's best practice to use HTML entities for things like quotes in text, so I used them here, showing how you can combine your string using your template literals. To add certain classes, use element.classList.add()
let msg = [{
remark: "test",
author: "they",
source: "there"
}];
function addElement(i) {
// create a new div element
const newDiv = document.createElement("div");
// Add message, author, and source to content
const content = `"${msg[i].remark}" - ${msg[i].author} from ${msg[i].source}`;
// add the text node to the newly created div
newDiv.innerHTML = content;
newDiv.classList.add('special');
// add the newly created element and its content into the DOM
const current_remark = document.querySelector(".remarks");
current_remark.append(newDiv);
}
addElement(0);
#kudos_title {
text-align: center;
font-family: Spectral, serif;
font-size: 50px;
}
.remarks {
padding: 20px;
color: pink;
background-color: springgreen;
}
.special {
color: #f00;
font-weight: bold;
}
<h3 id="kudos_title">Kudos</h3>
<div class="remarks"></div>
If you want all the new divs to be styled in the same way, you can just give them a class and then define those styles in a CSS file.
You can do it like this: note that the name "myclass" is given purely for illustration, I'm sure you can come up with a meaningful name that works for your application:
JS
function addElement (i) {
// create a new div element
const newDiv = document.createElement("div");
// Add message, author, and source to content
const content = document.createTextNode('"' + `${msg[i].remark}` + '"' + " - " + `${msg[i].author}` + " from " + `${msg[i].source}`);
// add the text node to the newly created div
newDiv.appendChild(content);
// add CSS class
newDiv.classList.append("myclass");
// add the newly created element and its content into the DOM
const current_remark = document.querySelector(".remarks");
document.body.insertBefore(newDiv, current_remark);
}
CSS
.myclass {
font-size: 16px;
background: #fff";
color: #333;
}
Just use a class and include it when making the element
JS:
newDiv.className = 'bar';
CSS:
.dynamicElement{
padding: 20px;
color: pink;
background-color: springgreen;
}
You can just use a css file
I am trying to move an element from the light DOM to the shadow DOM, but when I do so the styling isn't copying over. I tried to fix this by setting the newElement.style = window.getComputedStyle(elem), but this hasn't seemed to work. The styles should be:
.card {
color: #ff0;
font-size: 3rem;
font-weight: 600;
border: 3px solid blueviolet;
background-color: greenyellow;
}
but the styles don't apply and when I print the getComputedStyle() to console what I see is:
all the values are empty
However, when I loop through the properties of getComputedStyle() with .getPropertyValue() like so:
for(let property of style){
console.log(`property: ${property}, value: ${style.getPropertyValue(property)}`);
}
what I get in the console is:
the correct values
So I'm confused as to why getComputedStyle() doesn't contain the values, but using getComputedStyle().getPropertyValue() returns the correct values. I'm sure I'm missing something obvious, as I couldn't find another post about this anywhere.
Any help would be greatly appreciated, thanks in advance.
EDIT: I've taken the code provided by Danny below and modified it to better show the issue I'm facing:
<style>
.card {
color: yellow;
background: green;
}
</style>
<my-element>
<div class="card">lightDOM reflected to shadowDOM</div>
</my-element>
<script>
customElements.define("my-element", class extends HTMLElement {
constructor(){
super().attachShadow({mode:"open"}).innerHTML = ``;
}
connectedCallback() {
setTimeout(() => { // wait till innerHTML is parsed
let card = this.children[0]; // Get the light DOM Card element
this.shadowRoot.appendChild(card.cloneNode(true)); // Append it to the shadowDOM
let style = window.getComputedStyle(card); // Get style of the Light DOM Card
this.shadowRoot.querySelector('.card').style = style; // Set the ShadowDOM card style equal to the Light DOM Style
console.log(style);
console.log(style.color); // yellow = rgb:255,255,0
console.log(style.background); // green = rgb:0,128,0
card.remove(); // Remove the card from the Light DOM to prevent duplication
})
}
})
</script>
Notice that the styling above doesn't apply even though it seems to be exactly as the docs specify:
"The returned object is the same CSSStyleDeclaration type as the object returned from the element's style property. However, the two objects have different purposes:
The object from getComputedStyle is read-only, and should be used to inspect the element's style — including those set by a element or an external stylesheet.
The element.style object should be used to set styles on that element, or inspect styles directly added to it from JavaScript manipulation or the global style attribute."
https://developer.mozilla.org/en-US/docs/Web/API/Window/getComputedStyle#description
From MDN Documentation:
The Window.getComputedStyle() method returns an object containing the values of all CSS properties of an element, after applying active stylesheets and resolving any basic computation those values may contain. Individual CSS property values are accessed through APIs provided by the object, or by indexing with CSS property names.
It's stated that you need to use API functions, such as getPropertyValue() to get the value of it.
Ref: https://developer.mozilla.org/en-US/docs/Web/API/Window/getComputedStyle
If you want to print all of the CSS styles from a specific element you may just iterate all the attributes like this:
function dumpCSSText(element){
var s = '';
var o = getComputedStyle(element);
for(var i = 0; i < o.length; i++){
s+=o[i] + ': ' + o.getPropertyValue(o[i])+';\n';
}
return s;
}
var e = document.querySelector('.card');
console.log(dumpCSSText(e));
.card {
color: #ff0;
font-size: 3rem;
font-weight: 600;
border: 3px solid blueviolet;
background-color: greenyellow;
}
<div class="card"></div>
property style is read-only so you can't assign anything to it;
(I stand corrected per comments; you can assign a value, but it
will override all values)
The innerHTML of Custom Elements is not parsed yet when the connectedCallback fires. So getting styles of its children with getComputedStyle is an operation on non-existing elements.
If you reflect the lightDOM contents to a <slot> in shadowDOM, there is no need to copy styles as the styling from lightDOM is reflected
<style>
.card {
color: yellow;
background: green;
}
</style>
<my-element>
<div class="card">lightDOM reflected to shadowDOM</div>
</my-element>
<script>
customElements.define("my-element", class extends HTMLElement {
constructor(){
super().attachShadow({mode:"open"}).innerHTML = `<slot></slot>`
}
connectedCallback() {
setTimeout(() => { // wait till innerHTML is parsed
let card = this.querySelector(".card"); // in lightDOM!
let style = window.getComputedStyle(card);
console.log(style.color); // yellow = rgb:255,255,0
console.log(style.background); // green = rgb:0,128,0
})
}
})
</script>
More reading:
https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/style
https://developer.mozilla.org/en-US/docs/Web/HTML/Element/slot
::slotted CSS selector for nested children in shadowDOM slot
wait for Element Upgrade in connectedCallback: FireFox and Chromium differences
I'm working with creating custom html tags/elements that will then be dynamically added via JavaScript/jQuery. My end goal is to create a custom element like <xt-vertical-layout height="30px" width="50px"></xt-vertical-layout> where height and width will set the rendered size. I have figured out how to create the custom elements and have custom attributes that can hold data, but I cannot seem to find a way to modify the rendering. To be clear, I am not having an issue with CSS height and width not working. My issue is that I could potentially have hundreds of the elements all with varying sizes that are unknown untill being injected into the DOM well after the document has loaded.
I had at first looked into CSS with attribute selectors but it does not seem to be able to carry the value of the attribute down into the CSS rule. Similarly I had looked into the attr() function of CSS but it seems this only works on content. I found a bit of documentation on this function being allowed for all attributes in CSS4 though I then read something saying CSS4 will not be released and instead CSS will begin to be updated per module and so it seems this has not made it into the current standard.
Any light that could be shed on this subject would be greatly appreciated. I feel like there is some way to do this but am not too sure how. To be clear though, I'm not wanting something like parsing through the markup with JavaScript/jQuery and modifying that element with $().css() or something like that, as this then modifies the actual markup. My desire is to have the markup unchanged and the attributes modify the rendering, similar to how the <img/> tag works.
EDIT:
I do not mind doing this with JS/jQuery in the constructor for the custom element if it can be done there without modifying an inline style attribute, leaving the markup unchanged.
You would normally handle this in your CustomElement's class connectedCallback, where you will be able to access the node as this, and its DOM methods like getAttribute() if it inherits from HTMLElement.
class VerticalLayout extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
this.style.width = this.getAttribute('width');
this.style.height = this.getAttribute('height');
}
}
customElements.define('xt-vertical-layout', VerticalLayout);
// dynamically inserted
const elem = document.createElement('xt-vertical-layout');
elem.setAttribute('width', '25px');
elem.setAttribute('height', '25px');
document.body.append(elem);
xt-vertical-layout {
border: 1px solid;
display: inline-block;
}
<xt-vertical-layout height="30px" width="50px"></xt-vertical-layout>
<xt-vertical-layout height="50px" width="150px"></xt-vertical-layout>
<xt-vertical-layout height="10px" width="50px"></xt-vertical-layout>
And if you really do not want to modify the serialized DOM, then you can append a stylesheet inside the shadowDOM of your elements, and set a rule there. But note that doing so, you will loose support for older browsers as this feature can't be polyfilled.
class VerticalLayout extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
const shadow = this.attachShadow({mode: 'open'});
const style = document.createElement('style');
style.textContent = `:host {
width: ${ this.getAttribute('width') };
height: ${ this.getAttribute('height') };
}`;
shadow.append(style);
}
}
customElements.define('xt-vertical-layout', VerticalLayout);
// dynamically inserted
const elem = document.createElement('xt-vertical-layout');
elem.setAttribute('width', '25px');
elem.setAttribute('height', '25px');
container.append(elem);
console.log(container.innerHTML);
xt-vertical-layout {
border: 1px solid;
display: inline-block;
}
.as-console-wrapper {max-height: 120px !important;}
<div id="container">
<xt-vertical-layout height="30px" width="50px"></xt-vertical-layout>
<xt-vertical-layout height="50px" width="150px"></xt-vertical-layout>
<xt-vertical-layout height="10px" width="50px"></xt-vertical-layout>
</div>
Custom tags will have a display of inline which cannot have width and height. You need have a display of block or inline block.
addContent('Here is some content', 200, 50);
function addContent(content, width, height) {
const elm = document.createElement('xt-vertical-layout');
elm.setAttribute('style', `width: ${width}px; height: ${height}px;`);
elm.innerText = content;
document.body.append(elm);
}
xt-vertical-layout {
display: inline-block;
color: white;
padding: 5px;
background-color: red;
}
I want to generate css dynamically at run time.
Initially I had used sass and defined some variables and was using those variables. But css has to be generated first from the scss. Sass had given me flexibility to use variables and functions but still I was not able to changes them at run time via javascript.
One way was to change the inline styles via javascript but that approach was not completly flexible.
document.getElementById("myDiv").style.color = "red";
I don't want to do above, neither I want to attach any <style> attribute via javascript.
I want to use javascript but not for chaniging each and every style properties. I want to achieve scss like effect using css and javascript but at run time i.e dynamically.
E.g. suppose I got the color information from the ajax call now I want to change the whole theme of website based on that color received immediately without restarting or re-deploying my application.
e.g
as done in scss
.myClass {
background:$color;
// Update color value dynamically at run-time
}
Is it even possible or I am thinking in wrong direction!
Wound up playing with this and CSS variables. I'm adding a second answer because it's very different method from my first answer and it better aligns with your original question (updating CSS variables with JS).
BUT... don't do this. :) Browser support in IE < Edge doesn't exist and it is almost certainly slower than updating an on-page <style> element though I haven't tested it. This jsperf tests various style update methods. It doesn't include innerHTML on a single style element (likely the fastest) but you can see that the following CSS DOM methods are slower than the rest.
// get the stylesheet
// array position depends on how many style sheets you're loading.
// adjust as needed.
var sheet = document.styleSheets[0];
// simplest method: insertRule()
// setTimeout only for demo so you can see the change
window.setTimeout(function(){
// #media all {} is a trick to insert more than one
// selector and/or properties at once. Otherwise it's:
// sheet.insertRule(":root", "--header-color: green"); ...repeat...
sheet.insertRule("#media all { :root { --header-color: green; --main-color: orange; } }", 1);
}, 1200);
// SAFER method via addCSSRule.
// button and getAjaxStyles are just placeholders, obviously
var btn = document.querySelector('button');
btn.addEventListener("click", getAjaxStyles);
function getAjaxStyles() {
// success callback... break apart the json and update the CSS variables
addCSSRule(sheet, ":root", "--header-color: orange");
addCSSRule(sheet, ":root", "--main-color: blue");
addCSSRule(sheet, ":root", "--alt-color: red");
addCSSRule(sheet, ":root", "--borderColorA: lavender");
// or go with a single big string. definitely faster:
// addCSSRule(sheet, ":root", "--alt-color: red; --borderColorA: #0ff; ")
}
// Credit for addCSSRule() goes to Diego Flórez in a comment on
// https://davidwalsh.name/add-rules-stylesheets
var addCSSRule = function(sheet, selector, rules) {
//Backward searching of the selector matching cssRules
var index = sheet.cssRules.length - 1;
for (var i = index; i > 0; i--) {
var current_style = sheet.cssRules[i];
if (current_style.selectorText === selector) {
//Append the new rules to the current content of the cssRule;
rules = current_style.style.cssText + rules;
sheet.deleteRule(i);
index = i;
}
}
if (sheet.insertRule) {
sheet.insertRule(selector + "{" + rules + "}", index);
} else {
sheet.addRule(selector, rules, index);
}
return sheet.cssRules[index].cssText;
}
/* Set initial CSS variables */
:root {
--header-color: #333;
--main-color: #888;
--alt-color: #bbb;
--borderColorA: #ccc;
}
h1 {
color: var(--header-color);
}
p {
border-bottom: 1px solid var(--borderColorA);
color: var(--main-color);
}
p+p {
color: var(--alt-color);
}
<h1>header</h1>
<p>paragraph 1</p>
<p>paragraph 2</p>
<button>Update CSS Variables</button>
To expand on the information that is provided in the linked "possible duplicate" question, you could easily set up a "default" set of styles in your page CSS file and then create a inline <style> ekement containing any overrides based on the response from your AJAX call. As long as the element/class/id definitions are the same in the two locations (i.e., CSS file and inline style section), specificity will cause the inline definitions to override the CSS ones.
So, using your example, your static CSS file would contain:
.myClass {
background: #FFFFFF;
}
. . . so that there is a default value if the AJAX call were to fail, and then your dynamically created <style> section would contain:
.myClass {
background: THE_AJAX_RESPONSE_VALUE;
}
. . . which would override the default value.
UPDATE #1:
Based on your sample JSON, this would be REALLY easy . . . you would loop through each top-level property of the JSON and create this:
KEY_NAME {
. . .
}
Then, within that block, loop through each property within that property and add the keys and values to create the style definitions:
KEY_NAME {
key1: value1,
key2: value2,
. . .
keyN: valueN
}
UPDATE #2:
You can also use StyleSheet and CSSStyleSheet interfaces to access the rules that are in the existing stylesheets, but, given that it uses an array-like structure, that means looping through all of the CSS definitions to find the one that you want and alter it. An example of how to do that can be found in this answer to another SO question: Is it possible to alter a CSS stylesheet using JavaScript? (NOT the style of an object, but the stylesheet itself)
Between the two approaches, though, creating an overriding <style> section seems like the easier approach.
Since the JSON has both the element names and the related styles, refreshing an on page stylesheet (vs inline element styles) would probably be the fastest since it uses innerHTML and only requires a single DOM lookup.
You'll need to loop through your JSON to create CSS compatible strings and then just dump it into the onpage style element. You can append CSS by concatenating the existing innerHTML with the new CSS string. I added an ID to the stylesheet for simplicity but you could also generate the style element when needed.
var StringifiedAjaxStyleObject = "h1 {background-color: #ecc; color: #633}";
var styleSheet = document.getElementById("style-update");
// some additional fake test style returns...
var testStyle1 = "h1 {background-color: #ccc; color: #333}";
var testStyle2 = "h1 {background-color: #667; color: #bbc}";
var testStyle3 = "h1 {background-color: #fee; color: #b00}";
// some fake ajax returns...
window.setTimeout(function() {
styleSheet.innerHTML = StringifiedAjaxStyleObject;
}, 1000);
window.setTimeout(function() {
styleSheet.innerHTML = testStyle1;
}, 2000);
window.setTimeout(function() {
styleSheet.innerHTML = testStyle2;
}, 3000);
window.setTimeout(function() {
styleSheet.innerHTML = testStyle3;
}, 4000);
/* base styles ... */
h1 {
padding: 5px;
background-color: #eef;
color: #007
}
<!-- empty stylesheet -->
<style id="style-update" type="text/css" rel="stylesheet"></style>
<h1>Hi, mom</h1>
<button>Update Styles<button>
EDIT:
Here's a slightly more real-world version based on the JSON object in your comment. Trigger it via the button.
var styleSheet = document.getElementById("style-update");
var btn = document.querySelector('button');
btn.addEventListener("click", updateStyles);
function updateStyles() {
var StringifiedAjaxStyleObject
, newCSS
, ajaxReturn
;
// ...your ajax method to get the new styles...
// on success...
ajaxReturn = {
".base": {
"background-color": "#b83605",
"border-color": "#543927",
"color": "gray",
"text-shadow": "0 -1px 0 rgba(0, 0, 0, 0.15)"
},
".overlay": {
"background": "rgba(76, 65, 80, 0.2)",
"color" : "#ddd"
}
};
// Convert the object to a string
newCSS = cssStringFromJson(ajaxReturn);
// Update the stylesheet
styleSheet.innerHTML = newCSS;
}
function cssStringFromJson(cssJSON) {
var styleStr = "",
i, j;
for (i in cssJSON) {
styleStr += i + " {\n"
for (j in cssJSON[i]) {
styleStr += "\t" + j + ": " + cssJSON[i][j] + ";\n"
}
styleStr += "}\n"
}
return styleStr;
}
/* base styles ... */
.base {
border: 1px solid #ccf;
background-color: #eef;
color: #000;
padding: 15px;
}
.overlay {
padding: 5px 15px;
background: rgba(96, 95, 180, 0.2);
}
body {
font-family: sans-serif;
}
button {
margin-top: 1em;
font-size: 1em;
}
<!-- empty stylesheet -->
<style id="style-update" type="text/css" rel="stylesheet"></style>
<div class="base">
<p>.base</p>
<div class="overlay">
<p>.overlay</p>
</div>
</div>
<button>Update Styles</button>
You can try angular templates.
It is going to break your previous sass, but it will work out later.
I've got a chunk of CSS as a string, and I want to insert it into the DOM such that it only applies to elements in a particular container. Some tools, like Polymer, for example, rewrite CSS selectors so they only apply within a limited scope. How can I do something similar so that, when I insert this CSS into the DOM, it won't change all elements on the page?
To make it more concrete, imagine the following HTML and CSS from an external source:
<style>p { font-size: 20px; }</style>
<p>Boo.</p>
I want to insert these elements into a #container element, but I don't want to change the font-size for all <p> elements. I'd like to rewrite all the selectors inside that <style> element so they only apply within #container (p -> #container p, etc.). How?
Use https://github.com/reworkcss/css to parse the CSS, then alter selectors, and finally stringify:
const CSS = require('css');
function scopeCSS(css, scope) {
const ast = CSS.parse(css);
for (let rule of ast.stylesheet.rules) {
if (rule.type == 'rule') {
rule.selectors = rule.selectors.map(selector => `${scope} ${selector}`);
}
}
return CSS.stringify(ast);
}
scopeCSS('div { color: black; }', '#foo');
// #foo div {
// color: black;
// }
http://requirebin.com/?gist=trevordixon/839d0674531dafa98fb95ae51474245e