Communcation between Web Components - javascript

I am building a project with web components and vanilla javascript.
I have a component/module called meal.module - It is the parent component of the components meal-list and meal-search.
meal-list displays multiple meals from an api.
meal-search contains an input field and seachterm as attribute.
meal.module.js
export default class MealModule extends HTMLElement {
connectedCallback() {
this.innerHTML = '<mp-meal-search searchterm=""></mp-meal-search> ' +
'<mp-meal-list></mp-meal-list> ' +
}
}
if (!customElements.get('mp-meal-module')) {
customElements.define('mp-meal-module', EssenModule);
}
meal-list.component
export default class MealListComponent extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = Template.render();
// Renders the meals from api into the template
this.getMeals();
}
(....) more code
}
if (!customElements.get('mp-meal-list')) {
customElements.define('mp-meal-list', MealListComponent);
}
meal-search.component
export default class MealSearchComponent extends HTMLElement {
static get observedAttributes() {
return ['searchterm'];
}
attributeChangedCallback(name, oldVal, newVal) {
if (name === 'searchterm') {
this.doSearch();
}
}
set searchTerm(val) {
this.setAttribute('searchterm', val)
}
get searchTerm() {
return this.getAttribute('searchterm');
}
constructor() {
super();
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = '<input type="text" id="searchterm" value=""/>'
this.shadowRoot.addEventListener('change', (event) =>
this.searchTerm = event.target.value
)
}
doSearch() {
// send signal to MealListComponent for search
}
}
if (!customElements.get('mp-meal-search')) {
customElements.define('mp-meal-search', MealSearchComponent);
}
In the seach-component the SearchTerm is configured as Attribute. Everytime the input field gets changed, the attribute also changes.
Now I want to implement a "searchMeal" function, that always triggers when the attribute in MealSearchComponent changes.
I already tried to import the MealSearchComponent into the MealListComponent. But it does seem to break the rule of components, not having any dependencies.

JavaScript from the outside of the shadow DOM can access the shadow DOM via the element.ShadowRoot property.

Related

XSS recommendation: Use innerText\textContent instead of innerHTML

I have created a Web Component for a requirement and I have received a The best way to handle this is by not injecting untrusted strings in this way. Instead, use node.innerText or node.textContent to inject the string- the browser will not parse this string at all, preventing an XSS attack. code review comment. I am still thinking about how to replace innerHTML to innerText or textContent.
Would the community have an input?
import RealBase from '../real-base';
import productCardTitleCss from '../../../css/_product-card-title.scss?inline';
import baseCss from '../../../css/_base.scss?inline';
const ESAPI = require('node-esapi');
class RealProductCardTitle extends RealBase {
constructor() {
super();
this.defaultClass = 'real-product-card-title';
}
connectedCallback() {
super.connectedCallback();
this.render();
}
static get observedAttributes() {
return ['heading', 'form'];
}
get heading() {
return this.getAttribute('heading') || '';
}
set heading(value) {
this.setAttribute('heading', value ? ESAPI.encoder().encodeForHTML(value) : value);
}
get form() {
return this.getAttribute('form') || '';
}
set form(value) {
this.setAttribute('form', value ? ESAPI.encoder().encodeForHTML(value) : value);
}
attributeChangedCallback() {
this.render();
}
render() {
const {
heading,
form,
} = this;
this.classList.add('real-block');
this.classList.add(this.defaultClass);
if (!this.shadowRoot) {
this.attachShadow({ mode: 'open' });
}
//Recommendation for this line below
this.shadowRoot.innerHTML = `
<style>
${productCardTitleCss}
${baseCss}
</style>
<real-heading
class="real-product-card-title-heading"
input="${heading}">
</real-heading>
<div
class="real-product-card-title-attributes real-inline-block">
${form}
</div>`;
}
}
window.customElements.define('real-product-card-title', RealProductCardTitle);

Lit reactive controllers fail to requestUpdate in nested components

I want to create a basic state management using Lit reactive controllers.
The purpose is to share property values accross the application.
The issue occurs when a controller is attached to a view and to a component nested in the view. When value inside controller changes, the value in the view gets updated, but not in the nested component.
Example:
state.js contains the store logic. A view access the store to create a value and show state value. Nested components also show state value.
state.js
export class StateController {
static get properties() {
return {
state: { type: Object },
host: { type: Object }
}
}
constructor(host) {
// Store a reference to the host
this.host = host;
this.state = {};
// Register for lifecycle updates
host.addController(this);
}
_setStoreValue(property, val) {
this.state[property] = val;
this.host.requestUpdate();
}
}
component.js
import { LitElement, html } from 'lit';
import { StateController } from '../state.js';
export class TestComponent extends LitElement {
static get properties() {
return {
stateCtrl: { type: Object },
state: { type: Object },
};
}
constructor() {
super();
this.stateCtrl = new StateController(this);
this.state = this.stateCtrl.state
}
render() {
return html` Value in component: ${this.state?.test} `;
}
}
customElements.define('test-component', TestComponent);
view.js
import { LitElement, html } from 'lit';
import { StateController } from '../state.js';
import './test-component.js';
export class MonTodo extends LitElement {
static get properties() {
return {
stateCtrl: { type: Object },
state: { type: Object },
};
}
constructor() {
super();
this.stateCtrl = new StateController(this);
this.state=this.stateCtrl.state
}
render() {
return html`
<button #click=${() => this.setValueTest()}>Set value to 3</button>
Value in view: ${this.state?.test}
<h3> Component 1</h3>
<test-component></test-component>
<h3> Component 2</h3>
<test-component></test-component>
`;
}
setValueTest() {
this.stateCtrl._setStoreValue("test", 3)
}
}
customElements.define('mon-todo', MonTodo);
A button click in view.js updates this.state.test in view.js but not in component.js
Since you create a new StateController in both MonTodo and TestComponent, they are two different instances of the StateController which only have their specific component as host.
So the StateController in MonTodo only has MonTodo as a host and only updates that and not TestComponent.
You would need to share one controller with both components and call requestUpdate on both.

How to send props from parent to child with LitElement

Can someone show me some simple example about sending props from parent class to child? What is the problem:
parent component:
import { LitElement, html, css } from 'lit-element';
import './child.js';
class Parent extends LitElement {
constructor() {
super()
this.message = "hello world"
}
render() {
return html `<child-component message=${this.message}></child-component>` //how to get this props in child component?
}
}
customElements.define('parent-component', Parent);
and child component:
import { LitElement, html, css } from 'lit-element';
class Child extends LitElement {
...
render() {
return html `<p>${message from parent, but how}</p>` //message props should go to constructor? to render method as argument? how?
}
}
}
customElements.define('child-component', Child);
ok, I found solution. If I want to define properties in Parent class I have to add dot.
Reference: https://lit-element.polymer-project.org/guide/properties
render() {
return html `<child-component .message=${this.message}></child-component>`
}
So now everything is working.
And full example:
parent component:
import { LitElement, html, css } from 'lit-element';
import './child.js';
class Parent extends LitElement {
constructor() {
super()
this.message = "hello world"
}
render() {
return html `<child-component .message=${this.message}></child-component>`
}
}
customElements.define('parent-component', Parent);
child component:
import { LitElement, html, css } from 'lit-element';
class Child extends LitElement {
...
render() {
return html `<p>${this.message}</p>` //this.message have "hello world" value now
}
}
customElements.define('child-component', Child);
Make it much standard.
In your child component., set properties to listen to message.
import { LitElement, html, css } from 'lit-element';
class Child extends LitElement {
static get properties(){
return{
message: {
type: String,
}
}
}
constructor(){
super();
console.log(message);
}
render() {
return html `<p>${this.message}</p>` //this.message have "hello world" value now
}
}
customElements.define('child-component', Child);
In children to load property from parent component:
#property({type: Number}) propertyChild: number;
constructor() {
super();
this.propertyChild = (this.parentElement as TypeParent).propertyParent;
}
In parent to update properties on children:
override updated(): void {
this.childNodes.forEach(node=>{
const nodeType = node as TypeChildren;
if(nodeType.property) {
nodeType.propertyChild = this.propertyParent;
}
})
}

Lit-elements, the idiomatic way to write a controlled component

I'm working with lit-elements via #open-wc and is currently trying to write a nested set of components where the inner component is an input field and some ancestor component has to support some arbitrary rewrite rules like 'numbers are not allowed input'.
What I'm trying to figure out is what the right way to built this is using lit-elements. In React I would use a 'controlled component' see here easily forcing all components to submit to the root component property.
The example below is what I've come up with using Lit-Elements. Is there a better way to do it?
Please note; that the challenge becomes slightly harder since I want to ignore some characters. Without the e.target.value = this.value; at level-5, the input elmement would diverge from the component state on ignored chars. I want the entire chain of components to be correctly in sync, hence the header tags to exemplify.
export class Level1 extends LitElement {
static get properties() {
return {
value: { type: String }
};
}
render() {
return html`
<div>
<h1>${this.value}</h1>
<level-2 value=${this.value} #input-changed=${this.onInput}></level-2>
</div>`;
}
onInput(e) {
this.value = e.detail.value.replace(/\d/g, '');
}
}
...
export class Level4 extends LitElement {
static get properties() {
return {
value: { type: String }
};
}
render() {
return html`
<div>
<h4>${this.value}</h4>
<level-5 value=${this.value}></level-5>
</div>`;
}
}
export class Level5 extends LitElement {
static get properties() {
return {
value: { type: String }
};
}
render() {
return html`
<div>
<h5>${this.value}</h5>
<input .value=${this.value} #input=${this.onInput}></input>
</div>`;
}
onInput(e) {
let event = new CustomEvent('input-changed', {
detail: { value: e.target.value },
bubbles: true,
composed: true
});
e.target.value = this.value;
this.dispatchEvent(event);
}
}
export class AppShell extends LitElement {
constructor() {
super();
this.value = 'initial value';
}
render() {
return html`
<level-1 value=${this.value}></level-1>
`;
}
}
Added later
An alternative approach was using the path array in the event to access the input element directly from the root component.
I think it's a worse solution because it results in a stronger coupling accross the components, i.e. by assuming the child component is an input element with a value property.
onInput(e) {
const target = e.path[0]; // origin input element
this.value = e.path[0].value.replace(/\d/g, '');
// controlling the child elements value to adhere to the colletive state
target.value = this.value;
}
Don't compose your events, handle them in the big parent with your logic there. Have the children send all needed info in the event, try not to rely on target in the parent's event handler.
To receive updates, have your components subscribe in a shared mixin, a la #mishu's suggestion, which uses some state container (here, I present some imaginary state solution)
import { subscribe } from 'some-state-solution';
export const FormMixin = superclass => class extends superclass {
static get properties() { return { value: { type: String }; } }
connectedCallback() {
super.connectedCallback();
subscribe(this);
}
}
Then any component-specific side effects you can handle in updated or the event handler (UI only - do logic in the parent or in the state container)
import { publish } from 'some-state-solution';
class Level1 extends LitElement {
// ...
onInput({ detail: { value } }) {
publish('value', value.replace(/\d/g, ''));
}
}

Cannot see parent variable after changing to use custom dropdown?

I have a component with 2 direct children, both of which use the event variable that is held within the parent. However, upon changing one of the components which uses a dropdown list from using a <select> to a custom-animated dropdown... I can no longer see the event within this child, despite using the near-identical code.
Parent.ts
event: IEvent;
constructor(private eventService: EventService) {
}
ngOnInit() {
this.subToEventService();
}
subToEventService() {
this.eventService.eventSubject
.subscribe(res => {
this.event = res;
}
}
Child 1 (can see event)
export class ChildOne extends ParentComponent implements OnInit {
constructor(eventService: EventService) {
super(eventService);
}
ngOnInit() {
console.log(this.event);
}
}
Child 2 (cannot see event)
export class ChildTwo extends ParentComponent implements OnInit {
#ViewChild('dropdown') dropdown: ElementRef;
expanded = false;
constructor(eventService: EventService) {
super(eventService);
}
ngOnInit() {
console.log(this.event);
}
toggleDropdown() {
const dropdown = this.dropdown.nativeElement;
if (this.expanded) {
TweenMax.to(dropdown, 0.5, {...});
} else {
TweenMax.to(dropdown, 0.5, {...});
}
this.expanded = !this.expanded;
}
determineStyle() {
const style = this.dropdown.nativeElement.style;
style.height = this.expanded ? 376 : 34;
}
}
In both your child components, you are overriding the ngOnInit method of the ParentComponent class.
You need to call super.ngOnInit(); on both child ngOnInit to subscribe to your subject inside those components.

Categories