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

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 ?

Related

Parent and child web components sharing the same attribute

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

Angular #Input() value is not changed when the value is same

When I run this program, initially I get the output as false, 5, 500 as I have initialized them in the child component like that, but when I try to click on update button, I am not able to revert to the previous values. I am expecting the output to be true, 10, 1000, but I am getting it as false, 5, 1000. Only the number which was different got changed.
How can I solve this issue so that I can get the values whatever I set in the parent component?
Link to stackblitz.
app-component.html
<span (click)="clickme()">Update data</span>
app-component.ts
export class AppComponent {
public parentBool: boolean;
public parentNum: number;
public p2: number;
public ngOnInit(): void {
this.parentBool = true;
this.parentNum = 10;
this.p2 = 100;
}
public clickme(): void {
this.parentBool = true;
this.parentNum = 10;
this.p2 = 1000;
}
}
hello.component.ts
#Component({
selector: 'hello-comp',
template: `
<div>
Boolean value is - {{boolChild}} <br/>
Number value is - {{numChild}} <br />
Second number is - {{numChild2}}
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class MyComponent {
#Input() boolChild: boolean;
#Input() numChild: number;
#Input() numChild2: number;
public ngOnInit(): void {
this.boolChild = false;
this.numChild = 5;
this.numChild2 = 500;
}
constructor(private _cd: ChangeDetectorRef){}
public ngOnChanges(changes: {}): void {
// this._cd.detectChanges();
console.log(changes);
}
}
Input bindings occur right before ngOnInit, you’re overriding the values in ngOnInit.
Put your defaults in the declaration:
#Input() numChild: number = 5;
Like that
Well your problem is when everything is initialized
Parent value is True and Child Value is False
You are clicking on parent span. This sets the parentBool value to True. This is not a change when it comes to Parent Component. Hence the Change detection does not fire.
I forked this https://stackblitz.com/edit/angular-rgdgd6
from your link below
https://stackblitz.com/edit/angular-jv3sjz
You can try 2 different approachs
Capture ViewChildren and update within Parent https://stackblitz.com/edit/angular-4fqlqg
Have a service to maintain intermediate state and children will listen to that state. (if required use ngrx or else a simple service will suffice) (Prefer this a bit better as this is more scaleable)
have you tried calling detectChanges before anything else?
ngOnInit() {
this._cd.detectChanges();
....rest of your code...
}
Btw, I don't understand why you are setting the same values in the ngOnInit() as on line 14 & 15. Actually you don't need to...
This is how the input variables in the child component is initialized at the moment.
Initialize AppComponent, trigger ngOnInit and render the template.
Initialize MyComponent, trigger ngOnChanges. This will show in the console log as follows
{
"boolChild": {
"currentValue": true,
"firstChange": true
},
"numChild": {
"currentValue": 10,
"firstChange": true
},
"numChild2": {
"currentValue": 100,
"firstChange": true
}
}
Trigger ngOnInit (note: this is triggered after ngOnChanges) in MyComponent assign values false, 5 and 500. This is the reason why these values are shown in the template and not the one sent initially from the AppComponent.
Click Update data button in the AppComponent which will push values true, 10 and 1000. It will show the following output
{
"numChild2": {
"previousValue": 100,
"currentValue": 1000,
"firstChange": false
}
}
Explanation
According to ngOnChanges in the MyComponent, the previous values were true, 10 and 100 (from the ngOnInit of the AppComponent). The values false, 5 and 500 were not registered by the ngOnChanges because they weren't changes through the Input(). And so changes shows the only object that has been changed from it's previous state, which is numChild2. The other 2 values stay the same and they aren't reflected in the ngOnChanges.
One solution would be to avoid assigning values in the ngOnInit() hook in the child and assigning the values during definition.
_boolChild: boolean = false;
_numChild: number = 5;
_numChild2: number = 500;
Even with these changes, you will still observe only numChild2 object in the changes variable when you click the button because that is how SimpleChanges works. It will only show the value that has been changed from it's previous state, it won't reflect the values that stay the same.
Here is the hook calling order for reference
OnChanges
OnInit
DoCheck
AfterContentInit
AfterContentChecked
AfterViewInit
AfterViewChecked
OnDestroy

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.

Typescript "Cannot assign to property, because it is a constant or read-only" even though property is not marked readonly

I have the following code
type SetupProps = {
defaults: string;
}
export class Setup extends React.Component<SetupProps, SetupState> {
constructor(props: any) {
super(props);
this.props.defaults = "Whatever";
}
When trying to run this code the TS compiler returns the following error:
Cannot assign to 'defaults' because it is a constant or a read-only
property.
How is deafualts a readonly property, when its clearly not marked this way.
You're extending React.Component and it defines props as Readonly<SetupProps>
class Component<P, S> {
constructor(props: P, context?: any);
...
props: Readonly<{ children?: ReactNode }> & Readonly<P>;
state: Readonly<S>;
...
}
Source
If you want to assign some default values, you can go with something like:
constructor({ defaults = 'Whatever' }: Partial<SetupProps>) {
super({defaults});
}

Categories