Parent and child web components sharing the same attribute - javascript

I'm trying to create a custom textfield web component based on Material Web Components mwc-textfield:
import {LitElement, html, css} from 'lit-element';
import '#material/mwc-textfield';
export class CustomTextfield extends LitElement {
static get properties() {
return {
label: {type: String},
required: {type: Boolean},
value: {type: String}
}
};
get value_() {
return this.shadowRoot.getElementById("input").value;
};
constructor() {
super();
this.label = "";
this.value = "";
this.required = false;
}
render() {
return html`
<mwc-textfield id="input"
label="${this.label}"
value="${(this.value === undefined) ? "" : this.value}"
?required="${this.required}"
outlined
>
</mwc-textfield>
`;
};
}
customElements.define('custom-textfield', CustomTextfield);
At this moment, I can get the mwc-textfield value attribute with the custom-textfield value_ attribute. Is there any way to get the mwc-textfield value attribute with the custom-textfield value attribute?

For that to happen the value attribute in mwc-textfield should be reflecting its value, but that's not what's happening in https://github.com/material-components/material-components-web-components/blob/master/packages/textfield/src/mwc-textfield-base.ts#L123
So, at the moment it's not possible what you are looking for.
But anyway check this issue where they talk about the possible future which is implementing an internal api to mimic form elements with Form Associated Custom Elements

Related

methods/computed cant access data in vue component

I have a simple vue component where i defined a boolean constant in data with start value false. I now want to create a method to change it to true and bind certain stuff conditionally to that in the template when it changes. But somehow i get the error "property value does not exist on type...". when i move computed out of data i get "Property 'computed' has no initializer"
export default class Something extends Vue {
data() {
return {
value: false,
computed: {
valueTransform() {
this.value = true
alert(this.value)
},
},
}
}
}
This syntax is not valid in class components, you should have something like :
export default class Something extends Vue {
//data
value = false,
//computed
get valueTransform(){
return this.value
}
}

How to pass data between lit-element templates?

I am trying to pass data from one component to another. My idea is to have a generic component with buttons that increase or decrease a value and add those specific values ​​to another template.
Here is my code with the buttons:
import { LitElement, html } from 'lit-element';
class ButtonsCounter extends LitElement {
static get properties() {
return {
max: {type: Number},
min: {type: Number},
num: {type: Number},
value: {type: Number},
};
}
constructor() {
super();
this.value = 0;
}
createRenderRoot() {
return this;
}
increment(e) {
e.preventDefault();
if (this.value < this.maxValue) this.value++;
}
decrement(e) {
e.preventDefault();
if (this.value > this.minValue) this.value--;
}
render(){
return html`
<div class="searchCounter" max=${this.maxValue} min=${this.minValue}>
<span num=${this.value}>${this.value}</span>
</div>
`;
}
}
customElements.define('buttons-counter', ButtonsCounter);
Here is the template where I want to add the data that will go to the previous template:
import { LitElement, html } from 'lit-element';
import './buttons-counter'
class SelectOption extends LitElement {
constructor() {
super();
this.num = 0;
this.max = 5;
this.min = 0;
}
createRenderRoot() {
return this;
}
render(){
return html`
<buttons-counter .max="${this.max}" .min="${this.min}" .num="${this.num}"></buttons-counter>
`;
};
}
customElements.define('select-option', SelectOption);
I've tried different ways but none works. Any idea?
It looks like you're mixing up value with num, maxValue with max, and minValue with min.
You'll also likely want some text inside your a tags so that they're reasonably clickable. Technically, they should also be button tags (styled to your liking), as a tags are considered for linking to parts of a page or to another page (see https://stackoverflow.com/a/37148542/6090140 for more details). You then will not need e.preventDefault(); inside decrement and increment.
<button #click="${this.decrement}">-</button>

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, ''));
}
}

What is the proper way of initializing nullable state in React/TypeScript apps?

What is the proper way to initialize initial empty(null) state in React, using TypeScript interfaces/types?
For example, I have an interface:
interface IObject {
name: string,
age: number,
info: IAnotherObject
}
and a simple component, where I want to define initial information state as null(w/o creating a class that implements my interface and shape default values for all properties), but with IObject interface
interface IState {
information: null|IObject;
}
class App extends React.Component<{}, IState> {
state = {
information: null
};
componentDidMount() {
// fetch some data and update `information` state
}
render() {
<div>
{this.state.information &&
<div>
<div>{this.state.information.name}</div>
<div>//other object data</div>
</div>
</div>
}
Do we have another way to initialize nullable state w/o using union type:
// worked way, but with union null type
interface IState {
information: null|IObject;
}
// some another way(no union), but with initial `null` or `undefined`
interface IState {
information: IObject;
}
state = {
information: null
}
(it's seems not very "smart" for me to annotate with null every object which I want to have initial empty value)type object in state?
If you want to have initial empty state, you should to do this,
Mark information as empty,
interface IState {
information?: IObject;
}
now you can initialize it as empty.
state = {}
You should rely on undefined instead of null for missing properties. From typescript style guide, (link)
Use undefined. Do not use null.

Setting title as an attribute on a web component triggers maximum callstack

I'm working on a navigation link web component. One of the properties that I want to set on the component is the title. It seems that this is somehow triggering a maximum callstack error. Should I avoid title at all? I could use caption instead.
First error
Class 'NavLinkCmp' incorrectly extends base class 'HTMLElement'.
Property 'title' is private in type 'NavLinkCmp' but not in type 'HTMLElement'.
Second error
nav-link.cmp.ts:72 Uncaught RangeError: Maximum call stack size exceeded.
at HTMLElement.attributeChangedCallback (nav-link.cmp.ts:72)
navigator-cmp.ts
<nav-link-cmp href="${link.url}" fa-icon="${link.icon}"
title="${link.title}" description="${link.description}">
</nav-link-cmp>
nav-link-cmp.ts
export class NavLinkCmp extends HTMLElement {
// State
private title: string;
constructor() {
super();
}
connectedCallback() {
this.render();
}
render() {
this.innerHTML = `
<div class="info">
<div class="title">${this.title}</div>
</div>
`;
}
static get observedAttributes() {
return ['title'];
}
attributeChangedCallback(name: string, oldValue: string, newValue: string) {
this.title = newValue
}
}
// Component
require('./nav-link.cmp.scss');
window.customElements.define('nav-link-cmp', NavLinkCmp);
Try this:
attributeChangedCallback(name: string, oldValue: string, newValue: string) {
if (oldValue !== newValue) {
this.title = newValue;
}
}
If oldValue and newValue are the same then there is no need to set the property again.
If you do then it changes the attribute, which then calls attributeChangedCallback again and loops forever.
the infite loop is caused by the change of the 'title' value in the attributeChangedCallback.
as this function is called on every changes on the title attribute, its called over an over...
why can't you use the parent class title property directly ?

Categories