How to provide a fallback CSS-value within a custom element? - javascript

I have a custom web-component which is basically an SVG-Icon:
<custom-icon>
<svg>{svg-stuff}</svg>
</custom-icon>
I want to be able to change it's size by applying CSS like so:
custom-icon {
width: 20px;
}
But I also would like to have a fallback default value when no CSS is applied. However, when I inline some CSS like <custom-icon style="width:15px"> it just overwrites all CSS I apply afterwards. How can I have the default "15px" only apply if there is no custom CSS?
MWE:
class CustomIcon extends HTMLElement {
constructor() {
super();
let size = "100px"
this.style.height = size;
this.style.width = size;
this.style.background = "firebrick"
this.style.display = "block"
}
}
window.customElements.define('custom-icon', CustomIcon);
custom-icon {
--icon-size: 50px;
height: var(--icon-size);
width: var(--icon-size);
}
<custom-icon />

If the content of your custom element is encapsulated in a Shadow DOM, which is a recommended practice, you can use the :host pseudo-class to define a default style.
Then if you define a global style for your custom element it will override the one defined with :host.
customElements.define( 'custom-icon', class extends HTMLElement {
constructor() {
super()
let size = 100
this.attachShadow( { mode: 'open' } )
.innerHTML = `
<style>
:host {
display: inline-block ;
height: ${size}px ;
width: ${size}px ;
background-color: firebrick ;
color: white;
}
</style>
<slot></slot>`
}
} )
custom-icon#i1 {
--icon-size: 50px;
height: var(--icon-size);
width: var(--icon-size);
}
<custom-icon id="i1">sized</custom-icon>
<hr>
<custom-icon>default</custom-icon>

The order is applied according to the cascade.
CSS applied via the style attribute is at the bottom of the cascade. In effect, if you don't specify via the attribute when it falls back to the stylesheet.
So 20px is the fallback for when you don't specify 15px.
You could write your fallback CSS using another rule-set with a less specific selector (although the only thing less specific than a single type selector (like custom-icon) is the universal selector (*) which isn't helpful) so you would need to replace custom-icon with something more specific.
The other option is the take the sledgehammer approach and make every rule in your ruleset !important.
The best option would probably be to fix whatever circumstance might cause your CSS to be missing in the first place.

You can consider data attribute and then use that attribute as a fallback for the custom property.
You can see in the below, that the size will have no effect until we remove the custom property (by setting initial)
class CustomIcon extends HTMLElement {
constructor() {
super();
this.style.height = `var(--icon-size, ${this.getAttribute('size')})`;
this.style.width = `var(--icon-size, ${this.getAttribute('size')})`;
this.style.background = "firebrick"
this.style.display = "block"
}
}
window.customElements.define('custom-icon', CustomIcon);
custom-icon {
--icon-size: 50px;
margin:5px;
}
<custom-icon size="15px"></custom-icon>
<custom-icon size="25px"></custom-icon>
<custom-icon size="2050px"></custom-icon>
<custom-icon size="200px" style="--icon-size:initial"></custom-icon>
Related question to understand the use of initial : CSS custom properties (variables) for box model
Another example where the custom property is not set initially.
class CustomIcon extends HTMLElement {
constructor() {
super();
this.style.height = `var(--icon-size, ${this.getAttribute('size')})`;
this.style.width = `var(--icon-size, ${this.getAttribute('size')})`;
this.style.background = "firebrick"
this.style.display = "block"
}
}
window.customElements.define('custom-icon', CustomIcon);
custom-icon {
margin: 5px;
}
.set {
--icon-size: 50px;
}
<div class="set">
<custom-icon size="15px"></custom-icon>
<custom-icon size="25px"></custom-icon>
<custom-icon size="2050px"></custom-icon>
</div>
<custom-icon size="200px" ></custom-icon>

Related

HTML web component does not use shadow DOM style

I have created a vanilla web component or HTML element. It just displays two links.
To encapsulate the thing, I use shadow DOM. However it does not seem to be encapsulated. In the DOM tree it's inside #shadow-root which is good.
Why does the web component use the global style instead of the style I provided in the template for my web component?
The text is red and I expected it to be green.
class MyEl extends HTMLElement {
constructor() {
super();
this.shadow = this.attachShadow({ mode: "open" });
}
connectedCallback() {
const template = `
<style>
a {
color: green;
}
</style>
<slot></slot>`;
this.shadow.innerHTML = template;
}
}
window.customElements.define("my-el", MyEl);
a {
color: red
}
<my-el>
Item1
Item2
</my-el>
While this question already has an accepted answer, moving a slot's children to the shadowRoot isn't desirable for most use cases.
What you probably want to do is to use the ::slotted() selector.
Just bear in mind that styles applied to a slot's children through the ::slotted() selector only act as "default" styles and can still be overridden by using styles in light DOM.
For example, check this edited version of your snippet:
As you can see, this time my-el tries to apply both a color and a text-decoration style to anchor (<a>) children in any of it's slots.
However, in light dom, we have a a.special selector that overrides the color, so the <a class="special"> will be red, not green
class MyEl extends HTMLElement {
constructor() {
super();
this.shadow = this.attachShadow({ mode: "open" });
}
connectedCallback() {
const template = `
<style>
::slotted(a) {
color: green;
text-decoration: none;
}
</style>
<slot></slot>`;
this.shadow.innerHTML = template;
}
}
window.customElements.define("my-el", MyEl);
a.special {
color: red
}
<my-el>
Item1
<a class="special" href="example.com">Item2</a>
</my-el>
The full, detailed explanation is in: ::slotted CSS selector for nested children in shadowDOM slot
TL;DR
Your links are in lightDOM and thus styled by its DOM (in your code the document DOM)
Moving the nodes from lightDOM to shadowDOM is one "solution"; but you are not using slots then.
FYI, your code can be compacted to:
class MyEl extends HTMLElement {
constructor() {
super().attachShadow({ mode: "open" })
.innerHTML = `<style>a{color:green}</style><slot></slot>`;
}
}
window.customElements.define("my-el", MyEl);
More SLOT related answers can be found with StackOverflow Search: Custom Elements SLOTs
observe this line, you have to move/copy elements to shadow for example with:
this.shadow.innerHTML = this.innerHTML + template;
I've added this to demonstrate that only inline style will be applied to shadow dom elements .. so copied links in SD are using your style :)
so red will be GLOBAL, green will be SHADOW elements
class MyEl extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
this.shadow = this.attachShadow({ mode: "open" });
const template = `
<style>
a {
color: green;
}
</style>
<slot></slot>`;
this.shadow.innerHTML = this.innerHTML + template;
}
}
window.customElements.define("my-el", MyEl);
a {
color: red
}
<my-el>
Item1
Item2
</my-el>

Can't get overlay to show up in javascript, but could in jquery

I want an overlay to show up when I click a search icon.
I managed to get it working using jQuery. But can't seem to get it working with javascript.
The click event does not seem to be registering and I don't know why.
I've checked all the class names so they match in the same in both the HTML and javascript
Here is the jQuery code that works:
import $ from 'jquery';
class Search {
constructor() {
this.openButton = $('.js-search-trigger');
this.closeButton = $('.search-overlay__close');
this.searchOverlay = $(".search-overlay");
this.events();
}
events() {
this.openButton.on('click', this.openOverlay.bind(this));
this.closeButton.on('click', this.closeOverlay.bind(this));
}
openOverlay() {
this.searchOverlay.addClass("search-overlay--active");
}
closeOverlay() {
this.searchOverlay.removeClass("search-overlay--active");
}
}
export default Search;
Here is the javascript code that does not work:
class Search {
constructor() {
this.openButton = document.querySelector('.js-search-trigger');
this.closeButton = document.querySelector('.search-overlay__close');
this.searchOverlay = document.querySelector('.search-overlay');
this.events();
}
events() {
this.openButton.addEventListener('click', function() {
this.openOverlay.bind(this);
});
this.closeButton.addEventListener('click', function() {
this.closeOverlay.bind(this);
});
}
openOverlay() {
this.searchOverlay.classList.add('search-overlay--active');
}
closeOverlay() {
this.searchOverlay.classList.remove('search-overlay--active');
}
}
export default Search;
No errors were shown in the javascript where the overlay was not showing.
You'll probably want to change your event listeners to use the correct this binding:
this.openButton.addEventListener("click", this.openOverlay.bind(this));
Or use an arrow function to go with your approach - but make sure you actually call the resulting function, as in the above approach the function is passed as a reference and is called. If you removed the additional () from the code below, it would be the same as writing a function out in your code normally - it would be defined, but nothing would happen.
this.openButton.addEventListener("click", () => {
this.openOverlay.bind(this)();
});
jQuery also uses collections of elements rather than single elements, so if you have multiple elements, querySelectorAll and forEach might be in order.
If we are speaking of ecmascript-6 (I see the tag), I would recommend to use arrow function to have this inherited from the above scope, and no bind is needed:
this.openButton.addEventListener('click', () =>
this.openOverlay()
);
The problems with your code are that a) the function creates new scope with its own this; b) bound methods are not being invoked.
Why Search? You're creating an Overlay. Stick with the plan.
No need to bind anything. Use Event.currentTarget if you want to.
No need to handle .open/.close if all you need is a toggle.
And the below should work (as-is) for multiple Overlays. The overlay content is up to you.
class Overlay {
constructor() {
this.toggleButtons = document.querySelectorAll('[data-overlay]');
if (this.toggleButtons.length) this.events();
}
events() {
this.toggleButtons.forEach(el => el.addEventListener('click', this.toggleOverlay));
}
toggleOverlay(ev) {
const btn = ev.currentTarget;
const sel = btn.getAttribute('data-overlay');
const overlay = sel ? document.querySelector(sel) : btn.closest('.overlay');
overlay.classList.toggle('is-active');
}
}
new Overlay();
*{margin:0; box-sizing:border-box;} html,body {height:100%; font:14px/1.4 sans-serif;}
.overlay {
background: rgba(0,0,0,0.8);
color: #fff;
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
padding: 5vw;
transition: opacity 0.4s, visibility 0.4s;
visibility: hidden;
opacity: 0;
}
.overlay.is-active {
opacity: 1;
visibility: visible;
}
<button type="button" data-overlay="#search">OPEN #search</button>
<button type="button" data-overlay="#qa">OPEN #qa</button>
<div class="overlay" id="search">
<button type="button" data-overlay>CLOSE</button>
<h2>SEARCH</h2>
<input type="text" placeholder="Search…">
</div>
<div class="overlay" id="qa">
<button type="button" data-overlay>CLOSE</button>
<h2>Q&A</h2>
<ul><li>Lorem ipsum</li></ul>
</div>
The above is still not perfect, still misses a way to "destroy" events and not re-attach duplicate events to already initialised buttons when trying to target dynamically created ones.
Also, the use of Classes for the above task is absolutely misleading and unnecessary.

Detect dom element style change using Mutation Observer

Using MutationObserver I would like to detect a dom element change due to media query in a component.
But the MutationObserver fails to trigger an event when the style changes.
detectDivChanges() {
const div = document.querySelector('.mydiv');
const config = { attributes: true, childList: true, subtree: true };
const observer = new MutationObserver((mutation) => {
console.log("div style changed");
})
observer.observe(div, config);
}
}
<div class="mydiv">test</div>
.mydiv {
height: 40px;
width: 50px;
background-color: red;
}
#media screen and (min-width : 500px) {
.mydiv {
background-color: blue;
}
}
Here is a live version of the code
Mutation Observer can observe changes being made to the DOM tree.
When your CSS MediaQuery changes, the DOM tree is not affected whatsoever, so the MutationObserver won't catch it.
Your confusion comes from the fact that HTMLElements do have a style attribute. This attibute is indeed part of the DOM tree. But this style attribute is not the style that is applied on the element. This attribute does declare a StyleSheet that the CSSOM will parse and use if needed, but the CSSOM and the DOM are two separate things.
So what you want to detect is a CSSOM change not a DOM one (the style attribute doesn't change when you resize your screen), and this, a MutationObserver can't do it.
However, since you are willing to listen for a CSS MediaQuery change, then you can use the MediaQueryList interface and its onchange event handler:
const mediaQuery = window.matchMedia('screen and (min-width : 500px)');
mediaQuery.onchange = e => {
console.log('mediaQuery changed', 'matches:', mediaQuery.matches);
}
.mydiv {
height: 40px;
width: 50px;
background-color: red;
}
#media screen and (min-width : 500px) {
.mydiv {
background-color: blue;
}
}
<div class="mydiv">test</div>

html custom element, slot content appended after #shadow-root

i try to create custom element with js. this is my custom element
class ImageBackground extends HTMLElement {
createdCallback() {
let src = this.hasAttribute('src') ? this.getAttribute('src') : '/static/images/user.png'
let className = `img-bg ${this.hasAttribute('className') ? this.getAttribute('className') : ''}`
let isLazy = this.getAttribute('lazy') !== false
const slotContent = document.createElement('slot')
slotContent.setAttribute('name', 'slot-content')
const wrapper = document.createElement('div')
wrapper.appendChild(slotContent)
wrapper.style.backgroundImage = `url("${src}")`
wrapper.style.backgroundSize = 'cover'
wrapper.style.backgroundPosition = 'center'
wrapper.style.height = '300px'
wrapper.setAttribute('class', className)
this.createShadowRoot().appendChild(wrapper)
}
}
document.registerElement('img-bg', ImageBackground)
and this is my pug template
img-bg(src="/static/images/email.svg")
p(slot="slot-content") cek
i want to append p element inside the slot. but the p element appended after the #shadow-root.
can anyone solve this... :( sorry for the bad english
<slot> is defined in Shadow DOM v1. Therefore you mus use attachShadow() instead of createShadowRoot():
this.attachShadow({ mode:'open'}).appendChild(wrapper)
The <slot> tag is used to import children of the element into the <slot> and not a place for you to put your own children.
The best you could do is to wrap your <slot> in something else and then place the component's <p> tag just after the <slot>.
Alternatively, and not recommended:
If you want to add any tag, generated by the component, into the slot then you need to place the tag as a child of your element and not into the shadowDOM. As long as it is placed as a proper child, matching the requirements of the <slot> than it should show in the <slot>.
class MyEl extends HTMLElement {
constructor() {
super();
this.attachShadow({mode:'open'}).innerHTML = `
<style>
.wrapper {
border: 1px solid black;
padding: 5px;
}
::slotted(*) {
background-color: #8F8;
margin: 1px;
padding: 5px;
}
</style>
<p>Before Slot</p>
<div class="wrapper"><slot>Hi There</slot></div>
<p>After Slot</p>
`;
}
connectedCallback() {
}
}
customElements.define('my-el', MyEl);
function addChild() {
var el = document.querySelector('my-el');
var p = document.createElement('p');
p.textContent = "New stuff as a child.";
el.appendChild(p);
}
var button = document.querySelector('#one');
button.addEventListener('click', addChild);
p {
font-family: tahoma;
}
my-el p {
font-weight: bold;
}
<p>Before Element</p>
<my-el>
<p>Stuff in the slot</p>
</my-el>
<p>After Element</p>
<hr/>
<button id="one">Add Child</button>
So the only way to add into the slot is to add child elements to the element.
That is probably not a good thing to do since you are altering the content that the user of your component created. And that could mess up their code, CSS or both.

How would you call specific CSS stylesheet in a React component on a specific site?

So I'm working on a platform built on React and that platform already has global CSS that is already set up for all clients a certain way.
A subset of affiliates under a common group name want a new sticky ad component that has to change the CSS a bit so the footer does not get covered up.
So normally, I'd check what our global variable value is, window.client.group, and if it's the certain group, add css or css stylesheet through javascript in an affiliate js file (our old generic platform).
The new CSS needed is:
#media (max-width:767px){
#ad201 {width:100%;position:fixed;bottom:0;left:0;z-
index:99999;margin:0;border:1px solid rgb(232, 232, 232);background-
color:#fff;margin: 0 !important;}
.Footer .container {padding-bottom:50px;}
}
In React though, what's the best and most proper way to do this?
As you can see it's complicated by needing to be in with a media query.
I have a start using group and matchMedia, but what's the best way to bring in the CSS? a whole stylesheet? (stickyAd.css? some other way? and tips on how to do it?)
const group = (window.client.group).toLowerCase();
console.log(group);
const mq = window.matchMedia("(max-width: 767px)");
if ((mq.matches) && (group === 'premiere')) {
// bring in the new css
} else {
// do nothing
}
Thanks for the advice !
It depends on how much control you have over the ad201 and Footer.
I'm assuming Footer is a component you've created in React. In that case, you could add a class like premiere-footer-fix (you can probably think of a better name) to components being rendered when you detect your group:
render() {
const group = (window.client.group).toLowerCase();
return (
<Footer className={group === 'premiere' ? 'premiere-footer-fix' : ''}/>
)
}
or if you import the very handy classnames package,
import classNames from 'classnames';
// ...
render() {
const group = (window.client.group).toLowerCase();
const classes = classNames({
'premiere-footer-fix': group === 'premiere'
});
return (
<Footer className={classes}/>
)
}
Then wherever you declare CSS for the Footer, you just add something like:
#media (max-width:767px) {
.Footer.premiere-footer-fix .container {
padding-bottom: 50px;
}
}
As for your ad, you'd have to provide more info about how it's being inserted into the page since it's not clear how much control you have over the element. But I would add premiere to your ad's classList and find a place to insert this bit of CSS:
#media (max-width:767px) {
#ad201.premiere {
width: 100%;
position: fixed;
bottom: 0;
left: 0;
z-index: 99999;
margin: 0;
border: 1px solid rgb(232, 232, 232);
background-color: #fff;
margin: 0 !important;
}
}

Categories