Reference web component root attribute with CSS - javascript

Created a web component with a shadow DOM. When the button is clicked it adds the open attribute to the web component.
I would like to show the hidden div in the CSS when the open is added with CSS styling. Is it possible for the shadow DOM styles to reference attributes on the web component root? Otherwise, I have to add a superfluous class or attribute within the shadow DOM.
class CustomComponent extends HTMLElement {
constructor() {
super();
this.element = this.attachShadow({mode: 'open'});
}
static get observedAttributes() {
return ['open'];
}
attributeChangedCallback(attrName, oldValue, newValue) {
if (newValue !== oldValue) {
this[attrName] = this.hasAttribute(attrName);
}
}
connectedCallback() {
const template = document.getElementById('custom-component');
const node = document.importNode(template.content, true);
this.element.appendChild(node);
this.element.querySelector('button').addEventListener('click', () => {
this.setAttribute('open', '');
});
}
}
customElements.define('custom-component', CustomComponent);
<template id="custom-component">
<style>
div {
display: none;
}
[open] div {
display: block;
}
</style>
<button>Open</button>
<div>Content</div>
</template>
<custom-component></custom-component>

It appears the host CSS pseudo selector is designed to handle this precise situation.
class CustomComponent extends HTMLElement {
constructor() {
super();
this.element = this.attachShadow({mode: 'open'});
}
static get observedAttributes() {
return ['open'];
}
attributeChangedCallback(attrName, oldValue, newValue) {
if (newValue !== oldValue) {
this[attrName] = this.hasAttribute(attrName);
}
}
connectedCallback() {
const template = document.getElementById('custom-component');
const node = document.importNode(template.content, true);
this.element.appendChild(node);
this.element.querySelector('button').addEventListener('click', () => {
this.setAttribute('open', '');
});
}
}
customElements.define('custom-component', CustomComponent);
<template id="custom-component">
<style>
div {
display: none;
}
:host([open]) div {
display: block;
}
</style>
<button>Open</button>
<div>Content</div>
</template>
<custom-component></custom-component>

Related

text input (input type="text") value is not updating after changing property using an event with LitElement library

The source code:
import { LitElement, html, css } from '../vendor/lit-2.4.0/lit-all.min.js';
export class SearchInput extends LitElement {
static get properties() {
return {
src: { type: String },
items: { type: Array }
}
};
static styles = css`
`;
constructor() {
super();
this.items = [
{ text: 'Hola' },
{ text: 'mundo!' }
];
this.selectedItem = null;
this.text = 'foo';
}
selectItem(item) {
this.selectedItem = item;
this.text = this.selectedItem.text;
}
render() {
return html`
<div class="control">
<input class="input" type="text" value="${this.text}">
<ul class="result-list">
${this.items.map((item) => html`<li #click="${this.selectItem(item)}">${item.text}</li>`)}
</ul>
</div>
`;
}
}
customElements.define('search-input', SearchInput);
The text input (input type="text") value is not updating after changing property (this.text) using an event (this.selectItem) with LitElement library.
I tried it in browser but there is no error in browser console.
I expect that input value update after changing property with the event.
Thanks for the question! There are a few minor issues resulting in the value not updating.
One issue is that this.text is not a reactive property, so changing it isn't scheduling a re-render. Fix is to add text to the static properties.
The second issue is that your event listener click handler is the result of calling this.selectItems(item) and not a function, fixed with: #click=${() => this.selectItems(item)}.
Bonus: You may want to change the value attribute expression to a property expression using the live directive, .value="${live(this.text)}". I suggested this because the native input browser element always updates its contents if you update the value property, but only updates before a user has interacted with it when updating the value attribute. And the live directive is useful to tell Lit to dirty check the live DOM value in the input element.
Your code with the minor fixes: https://lit.dev/playground/#gist=a23dfbcdfbfcfb7de28b1f7255aaa8ee
or running in StackOverflow:
<script type="module">
import { LitElement, html, live } from 'https://cdn.jsdelivr.net/gh/lit/dist#2/all/lit-all.min.js';
class SearchInput extends LitElement {
static get properties() {
return {
src: { type: String },
items: { type: Array },
text: { type: String }, // <- Added this to make `this.text` a reactive property.
}
};
constructor() {
super();
this.items = [
{ text: 'Hola' },
{ text: 'mundo!' },
{ text: 'click these' },
];
this.selectedItem = null;
this.text = 'foo';
}
selectItem(item) {
this.selectedItem = item;
this.text = this.selectedItem.text;
}
render() {
return html`
<div class="control">
<!-- live directive is needed because user can edit the value of the input.
This tells Lit to dirty check against the live DOM value. -->
<input class="input" type="text" .value="${live(this.text)}">
<ul class="result-list">
<!-- Click event is a function -->
${this.items.map((item) =>
html`<li #click="${() => this.selectItem(item)}">${item.text}</li>`)}
</ul>
</div>
`;
}
}
customElements.define('search-input', SearchInput);
</script>
<search-input></search-input>

Not able to bind onchange event to lit-flatpickr element

import 'lit-flatpickr';
import { html, LitElement } from 'lit-element';
class MyApp extends LitElement {
getValue() {
this.shadowRoot.querySelector('#my-date-picker').getValue();
}
getSelectedDate(){
console.log('selected date');
}
render() {
return html<lit-flatpickr id="my-date-picker" altInput altFormat="F j, Y" dateFormat="Y-m-d" theme="material_orange" minDate="2020-01" maxDate="2020-12-31" #change="${this.getSelectedDate}" ></lit-flatpickr>;
}
}
getSelectedDate is not triggering at all. Can you help us how invoke hooks and methods of lit-flatpickr?
https://github.com/Matsuuu/lit-flatpickr
Try this:
(async function() {
await import('https://unpkg.com/lit-flatpickr?module');
const { html, css, LitElement } = await import('https://unpkg.com/lit?module');
class MyApp extends LitElement {
static get styles() {
return css`
lit-flatpickr {
background: pink;
}
`;
}
get picker() {
return this.shadowRoot.querySelector('#my-date-picker')
}
getValue() {
this.picker.getValue();
}
getSelectedDates(e) {
console.log(e);
}
render() {
return html `
<lit-flatpickr id="my-date-picker"
altInput
allowInput
altFormat="F j, Y"
dateFormat="Y-m-d"
theme="material_orange"
minDate="2020-01"
maxDate="2020-12-31"
.onChange="${this.getSelectedDates}"
></lit-flatpickr>
`;
}
}
customElements.define('my-app', MyApp);
})();
<my-app id="app"></my-app>
<lit-flatpickr> (at least the version i got as of this writing) doesn't have any DOM events, you have to pass these custom on* functions instead.

Toggling button issue in polymer

Im trying to toggle the displaying of message using a button.
Below is my code.
class DisplayMessage extends PolymerElement {
// DO YOUR CHANGES HERE
static get template() {
return html`
<style>
:host {
display: block;
}
</style>
<h2>Hello [[prop1]]!</h2>
<button on-click="toggle">Toggle Message</button> <br />
<template is="dom-if" if="{{user.authorise }}">
<br />
<span>I should now display message.</span>
</template>
`;
}
toggle() {
// DO YOUR CHANGES HERE
// hint: use set method to do the required changes
//console.log(this.user);
//this.user = !this.user
}
static get properties() {
return {
prop1: {
type: String,
value: 'user',
},
user: {
type: Object,
value: function () {
return { authorise: false}; // HINT: USE BOOLEAN VALUES TO HIDE THE MESSAGE BY DEFAULT
},
notify: true,
},
};
}
}
window.customElements.define('display-message', DisplayMessage);
I tried thinking for like hours, but couldn't solve. The requirement her is on clicking the button, the click handler toggle should change the value of authorize in user property to true. And on clicking again to false and so on. I need to use set method within toggle method. I'm not getting how to do this. Please help me on this.
Thanks in advance.
Why use a library/dependency for such a small component, that can be done with native code
<display-message id=Message name=Cr5>You are authorized</display-message>
<script>
customElements.define("display-message", class extends HTMLElement {
static get observedAttributes() {
return ["name", "authorized"]
}
connectedCallback() {
this.innerHTML = `<h2>Hello <span>${this.getAttribute("name")}</span></h2><button>Toggle message</button><br><div style=display:none>${this.innerHTML}</div>`;
this.querySelector('button').onclick = () => this._toggle();
}
_toggle(state) {
let style = this.querySelector('div').style;
style.display = state || style.display == "none" ? "inherit" : "none";
this.toggleAttribute("authorized", state);
console.log(Message.name, Message.authorized);
}
get name() { return this.getAttribute("name") }
set name(value) {
this.querySelector('span').innerHTML = value;
this.setAttribute("name", value);
}
get authorized() { return this.hasAttribute("authorized") }
set authorized(value) { this._toggle(value) }
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue) this[name] = newValue;
}
})
Message.name = "Cr5";
Message.authorized = true;
</script>
class DisplayMessage extends PolymerElement {
static get template() {
return html`
<style>
:host {
display: block;
}
</style>
<h2>Hello [[prop1]]!</h2>
<button on-click="toggle">Toggle Message</button> <br />
<template is="dom-if" if="{{user.authorise}}">
<br />
<span>I should now display message.</span>
</template>
`;
}
toggle() {
if(this.user.authorise==false)
{
this.set('user.authorise', true);
}
else
{
this.set('user.authorise', false);
}
}
static get properties() {
return {
prop1: {
type: String,
value: 'user',
},
user: {
type: Object,
value: function () {
return { authorise: false };
},
},
};
}
}
window.customElements.define('display-message', DisplayMessage);

WebComponents: how to get the resolved value of a slot in shadow DOM?

I have a web component and I want to adjust the value of its slot.
Unfortunately, I am unable to get the resolved value from it.
How can I do that?
const template = document.createElement('template');
template.innerHTML = `
<p><slot></slot></p>
`;
class MyComp extends HTMLElement {
constructor() {
super();
this.root = this.attachShadow({mode: 'open'});
this.root.appendChild(template.content.cloneNode(true));
}
connectedCallback() {
const slot = this.shadowRoot.querySelector('slot');
console.log('VALUE:', slot.innerText); // always empty
}
}
customElements.define('my-comp', MyComp);
<my-comp>abc</my-comp>
You can assign an EventListener to watch for SLOT changes
MDN Documentation
slotchange event
assignedNodes
::slotted
How do I style the last slotted element in a web component
customElements.define('my-element', class extends HTMLElement {
constructor() {
super();
this.attachShadow({
mode: 'open'
}).appendChild(document.getElementById(this.nodeName).content.cloneNode(true));
}
connectedCallback() {
this.listeners = [...this.shadowRoot.querySelectorAll("SLOT")].map(slot => {
let name = "slotchange";
let func = (evt) => {
let nodes = slot.assignedNodes();
console.log(`Slot ${slot.name} changed to ${nodes[0].outerHTML}`)
}
slot.addEventListener(name, func);
return () => slot.removeEventListener(name, func); // return cleanup function!!!
})
}
disconnectedCallback() {
this.listeners.forEach(removeFunc => removeFunc());
}
});
<template id="MY-ELEMENT">
<style>
::slotted(*) {
background: yellow;
margin: 0;
}
::slotted(span) {
color: red;
}
</style>
<b>
<slot name=title></slot>
</b>
<slot name=content></slot>
</template>
<my-element>
<span slot="title">Hello World</span>
<p slot="content">What a wonderful day!</p>
</my-element>
<my-element>
<span slot="title">Hello Tomorrow</span>
<p slot="content">What will you bring?</p>
</my-element>
Less code, easier to understand. For simple templates I'd rather create the elements programmatically, then add a slotchange listener:
class MyComp extends HTMLElement {
p = document.createElement('p');
slot = document.createElement('slot');
constructor() {
super();
this.p.append(this.slot);
this.attachShadow({mode: 'open'}).append(this.p);
this.slot.addEventListener('slotchange', () => console.log('VALUE:', this.slottedValue))
}
get slottedValue() { return this.slot.assignedNodes()[0].wholeText }
}
customElements.define('my-comp', MyComp);
console.log(document.querySelector('my-comp').slottedValue);
<my-comp>abc</my-comp>

How to pass event to a child in LitElement

I want to make a drop-down menu and that when clicking on the input, the menu is displayed with a toggle that removes or places the 'hidden' class
I have this method
toggleMenu() {
this.classList.toggle("hidden");
}
And here the template.
render(){
return html`
<input #click="${this.toggleMenu}" type="button">
<ul class="hidden">
<slot></slot>
</ul>
`;
}
One straightforward solution is to add a property to your custom element, e.g. open, that is toggled in your toggleMenu method:
static get properties() {
return {
open: { type: Boolean },
};
}
constructor() {
super();
this.open = false;
}
toggleMenu() {
this.open = !this.open;
}
Then in your render method set the <ul>'s class attribute based on the value of this.open:
render(){
return html`
<button #click=${this.toggleMenu} type="button">Toggle</button>
<ul class=${this.open ? '' : 'hidden'}>
<slot></slot>
</ul>
`;
}
You can see this working in the below snippet:
// import { LitElement, css, html } from 'lit-element';
const { LitElement, css, html } = litElement;
class DropDownMenu extends LitElement {
static get properties() {
return {
open: { type: Boolean },
};
}
static get styles() {
return css`
ul.hidden {
display: none;
}
`;
}
constructor() {
super();
this.open = false;
}
toggleMenu() {
this.open = !this.open;
}
render(){
return html`
<button #click=${this.toggleMenu} type="button">Toggle</button>
<ul class=${this.open ? '' : 'hidden'}>
<slot></slot>
</ul>
`;
}
}
customElements.define('drop-down-menu', DropDownMenu);
<script src="https://bundle.run/lit-element#2.2.1"></script>
<drop-down-menu>
<li>Item 1</li>
<li>Item 2</li>
</drop-down-menu>
If you want to apply additional classes to the <ul> you'll want to look into the classMap function as described in the LitElement docs.
Alternatively, you can add reflect: true to the open property declaration, which lets you show or hide the <ul> using CSS alone, rather than setting its class in render:
static get properties() {
return {
open: {
type: Boolean,
reflect: true,
},
};
}
static get styles() {
return css`
ul {
display: none;
}
:host([open]) ul {
display: block;
}
`;
}
Here it is in a working snippet:
// import { LitElement, css, html } from 'lit-element';
const { LitElement, css, html } = litElement;
class DropDownMenu extends LitElement {
static get properties() {
return {
open: {
type: Boolean,
reflect: true,
},
};
}
static get styles() {
return css`
ul {
display: none;
}
:host([open]) ul {
display: block;
}
`;
}
constructor() {
super();
this.open = false;
}
toggleMenu() {
this.open = !this.open;
}
render(){
return html`
<button #click=${this.toggleMenu} type="button">Toggle</button>
<ul>
<slot></slot>
</ul>
`;
}
}
customElements.define('drop-down-menu', DropDownMenu);
<script src="https://bundle.run/lit-element#2.2.1"></script>
<drop-down-menu>
<li>Item 1</li>
<li>Item 2</li>
</drop-down-menu>
Both of these are common approaches and the best one for your application will depend on your use case and personal preferences.
I like to keep it simple, if you need a reference to the DOM node then pass the event to the function like the following:
toggleMenu(ev) {
ev.target.classList.toggle("hidden");
}
And for the render method
render(){
return html`
<input #click="${(ev)=>{this.toggleMenu(ev)}}" type="button">
<ul class="hidden">
<slot></slot>
</ul>
`;
}
And you're done

Categories

Resources