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

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.

Related

Create dynamic tags with Styled Components

Is there a way to create a dynamic tag using styled-components?
For example:
const MyComponent = styled(CustomTag)``;
<MyComponent element="h1" />
You can use as prop by default on components created with styled-components. If in your example CustomTag is also a styled-component that styles a native element (e.g.:)
const CustomTag = styled.h1`
color: red;
`;
then you can do
const MyComponent = styled(CustomTag)`
font-size: 64px;
`;
<MyComponent as="span">Something</MyComponent>
and you'll end up with a <span> tag with font-size of 64px and red text color. Of course you can also use as prop on CustomTag so you don't necessarily need MyComponent.
Maybe this will help you:
Add you code in your typescript
class Test extends HTMLElement {
connectedCallback() {
this.innerHTML = `<h1>Hello World...</h1>`;
this.style.color = "red";
}
}
customElements.define('test', Test);
after compiling refer the js file in you HTML and you can use it like <test></test>

When inserting DOM element from JavaScript, CSS styles are not displayed

I want to display a log with a title and a content.
With the following code, li elements are inserted in the DOM with the right className but styles are not displayed.
This is inside an Angular Component, which I think may be the origin of the error. It's just a basic new app with html: <app-example></app-example>
Note: if I insert an element by hand in the html it is displayed correctly.
The only difference I notice is that li elements inserted from javascript do not have _ngcontent-cxr-c40.
Html:
<ul class="d-flex f-column log-list" id="log-list">
<li id="log-item" class="log-message">
<span class="log-message-title">TEST</span>
</li>
</ul>
This is the function to add an element to the log:
private addLogElement(title: string, message: string): void {
const newNode = document.createElement('li');
newNode.className = 'log-message';
const newNodeTitle = document.createElement('span');
newNodeTitle.className = 'log-message-title';
const headerText = document.createTextNode(title);
newNodeTitle.appendChild(headerText);
newNode.appendChild(newNodeTitle);
const parentDiv = document.getElementById('log-list');
const childDiv = document.getElementById('log-item');
parentDiv.insertBefore(newNode, childDiv);
}
You should use Renderer2 API for such DOM Manipulations.
In Component class in constructor inject it like :-
constructor(public renderer: Renderer2) {}
Then change your method to :-
private addLogElement(title: string, message: string): void {
const newNode = this.renderer.createElement('li');
this.renderer.addClass(newNode, 'log-message');
const newNodeTitle = this.renderer.createElement('span');
this.renderer.addClass(newNodeTitle, 'log-message-title');
const headerText = this.renderer.createText(title);
this.renderer.appendChild(newNodeTitle, headerText);
this.renderer.appendChild(newNode, newNodeTitle);
const parentDiv = this.renderer.selectRootElement(document.getElementById('log-list'), true);
const childDiv = this.renderer.selectRootElement(document.getElementById('log-item'), true);
this.renderer.insertBefore(parentDiv, newNode, childDiv);
}
There are other reasons why you should use Renderer2 instead of native DOM manipulation.
You can refer to those reasons here :- https://medium.com/dev-genius/dont-use-native-dom-manipulations-in-angular-6c8db13f463f
Add this in your css/scss
I have added sample styles
:host ::ng-deep .log-message{
color: red;
}
:host ::ng-deep .log-message-title{
background-color: #eeee33
}

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>

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

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>

Mouse-Over event Lit-Element/Polymer

Is there a way to create a custom event in in Lit-Element/Polymer, such as a mouse-over event? I've been searching for this a while now, but I can seem to find a way of doing it. I know about events in Lit-Element, like #click, but nothing about mouse-over events.
This can be done using lit-html #event bindings.
For the mouseover event type, use #mouseover="${this.handleMouseover}"
For more information on lit-element event listeners, see
https://lit-element.polymer-project.org/guide/events
Live example:
<script src="https://unpkg.com/#webcomponents/webcomponentsjs#latest/webcomponents-loader.js"></script>
<script type="module">
import { LitElement, html, css } from 'https://unpkg.com/lit-element/lit-element.js?module';
class MyElement extends LitElement {
static get styles() {
return [
css`
span {
padding: 4px 8px;
border: 2px solid orange;
}
`
];
}
render() {
return html`
<span
#mouseover="${this.handleEvent}"
#mouseleave="${this.handleEvent}">
Hover me
</span>
`;
}
handleEvent(e) {
console.log(`Event type: ${e.type}`);
}
}
customElements.define('my-element', MyElement);
</script>
<my-element></my-element>
So I just figured it out, and just going to post here if anyone has the same difficulty.
You have to use CustomEvents, here some code example:
in your element's firstUpdate method you should add a new EventListener
firstUpdated(){
this.addEventListener('mouseover', this.mouseOverHandler);
}
and declare the method
mouseOverHandler(){
console.log('Hello');
}
Simple as that!!!

Categories