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

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>

Related

Value not emitting from child to parent component using custom events

I have a problem with two web components created using lit-element v3.2.0 and I'm using custom events to emit the input value from the child component up to the parent component.
The form-input component is a reusable input that extracts the input value and emits it to the parent component with a custom event named "new-value" which is dispatched when the user writes in the field.
The form-container component contains the form-input, in here I'm binding the custom event "new-value" to a method called "updateInputValue" which should reassign the inputValue property with the emitted value from the child component, but instead is stuck with whatever value initialized in the parent constructor.
form-container.js
static get properties() {
return {
inputValue: { type: String },
items: { type: Array },
}
}
constructor() {
super()
this.inputValue = ''
this.items = []
}
render() {
return html`<form>
<h1>My form container</h1>
<form-input
#new-value=${this.updateInputValue}
fieldName="name"
id="name"
label="Name"
placeholder="Enter anything"
value="${this.inputValue}"
></form-input>
<button #click=${this.addNewItem} type="submit">Add</button>
<form-list .items="${this.items}"></form-list>
</form>`
}
updateInputValue(e) {
// Update input value with the value emitted from the form-input
this.inputValue = e.detail
}
addNewItem(e) {
// Add new item to the list
e.preventDefault()
console.log('add new item with the following value:', this.inputValue)
form-input.js
static get properties() {
return {
value: { type: String },
fieldName: { type: String },
label: { type: String },
placeholder: { type: String },
type: { type: String },
}
}
constructor() {
super()
}
render() {
return html`
<div>
<label for="name">${this.label}</label>
<input
#input=${this.dispatchEvent}
id="${this.fieldName}"
name="${this.fieldName}"
placeholder="${this.placeholder}"
type="${this.type || 'text'}"
value="${this.value}"
/>
</div>
`
}
dispatchEvent(e) {
// Emit the new value from the input to the parent component
const target = e.target
if (target) {
this.dispatchEvent(
new CustomEvent('new-value', {
detail: target.value,
})
)
}
}
Any help will be very much appreciated.
You are overwriting the dispatchEvent method and calling yourself. Rename the dispatchEvent Method and give it a meaningful name. It works perfectly.
<script type="module">
import {
LitElement,
html, css
} from "https://unpkg.com/lit-element/lit-element.js?module";
class FormInput extends LitElement {
static get properties() {
return {
value: { type: String },
fieldName: { type: String },
label: { type: String },
placeholder: { type: String },
type: { type: String },
};
}
render() {
return html`
<div>
<label for="name">${this.label}</label>
<input
#input=${this.changedInput}
id="${this.fieldName}"
name="${this.fieldName}"
placeholder="${this.placeholder}"
type="${this.type || 'text'}"
value="${this.value}"
/>
</div>
`
}
changedInput(e) {
// Emit the new value from the input to the parent component
console.log(e.target.value);
const myevent = new CustomEvent('my-event', {
bubbles: true,
composed: true,
detail: {
value: e.target.value
}
})
this.dispatchEvent(
myevent
);
}
}
class FormContainer extends LitElement {
static get properties() {
return {
name: {
inputValue: { type: String },
}
};
}
updateInputValue(e) {
console.log('received ' + e.detail.value);
this.inputValue = e.detail.value;
}
addNewItem(e) {
// Add new item to the list
e.preventDefault()
console.log('add new item with the following value:', this.inputValue);
}
render() {
return html`<form>
<h1>My form container</h1>
<form-input
#my-event="${this.updateInputValue}"
fieldName="name"
id="name"
label="Name"
placeholder="Enter anything"
value="${this.inputValue}"
></form-input>
<button #click=${this.addNewItem} type="submit">Add</button>
<form-list .items="${this.items}"></form-list>
</form>
`;
}
}
customElements.define("form-container", FormContainer);
customElements.define("form-input", FormInput);
</script>
<form-container></form-container>

LitElement remove item from list

When the page first loads, the delete buttons generated by the code below work as expected. However, if you alter the text in one of the <textarea> elements, the delete button no longer works correctly. How can I fix this?
import { LitElement, html } from 'lit-element';
class MyElement extends LitElement {
static get properties() {
return {
list: { type: Array },
};
}
constructor() {
super();
this.list = [
{ id: "1", text: "hello" },
{ id: "2", text: "hi" },
{ id: "3", text: "cool" },
];
}
render() {
return html`${this.list.map(item =>
html`<textarea>${item.text}</textarea><button id="${item.id}" #click="${this.delete}">X</button>`
)}`;
}
delete(event) {
const id = event.target.id;
this.list = this.list.filter(item => item.id !== id);
}
}
customElements.define("my-element", MyElement);
I'm not sure of the exact cause, but I think it has to do with the way lit-html decides which DOM elements to remove when rendering a list with fewer items than the previous render. The solution is to use the repeat directive. It takes as its second argument a function that helps lit-html identify which DOM elements correspond to which items in the array:
import { repeat } from 'lit-html/directives/repeat.js'
// ...
render(){
return html`
${repeat(this.list, item => item.id,
item => html`<textarea>${item.text}</textarea><button id="${item.id}" #click="${this.delete}">X</button><br>`
)}
`;
}

How would I change the background color of a list item upon click? (ES6 & Polymer)

I've cloned a repository which focuses on creating a To-Do application using ES6 and Polymer 3. I'm trying to implement a button which turns the background color containing a string green upon click. I've tried doing this, but I keep failing to get the desired result.
Example code:
static get properties() {
return {
list: {type: Array},
todo: {type: String},
};
}
constructor() {
super();
this.list = [
this.todoItem('buy cereal'),
this.todoItem('buy milk')
];
this.todo = '';
this.createNewToDoItem = this.createNewToDoItem.bind(this);
this.handleKeyPress = this.handleKeyPress.bind(this);
this.handleInput = this.handleInput.bind(this);
}
todoItem(todo) {
return {todo}
}
createNewToDoItem() {
this.list = [
...this.list,
this.todoItem(this.todo)
];
this.todo = '';
}
//Right here is where I tried to implement the background color change.
checkItem() {
checkItem = document.getElementById('checkItem'),
checkItem.addEventListener('click', () => {
this.list = this.list.filter(this.todo)
document.body.style.backgroundColor = 'green';
});
}
deleteItem(indexToDelete) {
this.list = this.list.filter((toDo, index) => index !== indexToDelete);
}
render() {
return html`
${style}
<div class="ToDo">
<h1>Grocery List</h1>
<h1 class="ToDo-Header">What do I need to buy today?</h1>
<div class="ToDo-Container">
<div class="ToDo-Content">
${repeat(
this.list,
(item, key) => {
return html`
<to-do-item
item=${item.todo}
.deleteItem=${this.deleteItem.bind(this, key)}
></to-do-item>
`;
}
)}
</div>
I'd be eternally thankful if someone helped me out. I've created two JSFiddle links which show the code I've worked on thus far:
Link 1: https://jsfiddle.net/r2mxzp1c/ (Check line 42-49)
Link 2: https://jsfiddle.net/zt0x5u94/ (Check line 13 & 22-24)
I'm not sure about the approach. But this link might help you
https://stackblitz.com/edit/web-components-zero-to-hero-part-one?file=to-do-app.js
from this guy: https://stackblitz.com/#thepassle
You should try to make the reactive templating work for you by defining presentation details in terms of your element's properties.
For example, this is a stripped-down approach to the same problem:
class TestElement extends LitElement{
static get properties() {
return {
'items': { 'type': Array }
};
}
constructor() {
super();
// set up a data structure I can use to selectively color items
this.items = [ 'a', 'b', 'c' ].map((name) =>
({ name, 'highlight': false }));
}
render() {
return html`<ol>${
this.items.map((item, idx) =>
html`<li
#click="${ () => this.toggle(idx) }"
style="background: ${ item.highlight ? '#0f0' : '#fff' }">
${ item.name }
</li>`)
}</ol>`;
}
toggle(idx) {
// rendering won't trigger unless you replace the whole array or object
// when using properties of those types. alternatively, mutate with the
// usual .push(), .splice(), etc methods and then call `this.requestUpdate()`
this.items = this.items.map((item, jdx) =>
jdx === idx ? { ...item, 'highlight': !item.highlight } : item
);
}
}
https://jsfiddle.net/rzhofu81/305/
I define the template such that the elements are colored the way I want depending on an aspect of their state (the "highlight" attribute of each entry in the list), and then I focus the interaction on updating the state to reflect what the user is doing.

Computed property in Vue is not triggering a watch

According to this post, it shouldn't be a problem to watch a computed property. And yet my code isn't working.
<template>
<div v-if="product" class="section">
<form>
<div class="control"><input type="text" class="input" v-model="title"></div>
<div class="control"><input type="text" class="input" v-model="description"></div>
</form>
</div>
</template>
<script>
export default {
data() {
return {
title: null,
description: null
}
},
computed: {
product() {
// const payload = { collection: 'products', id: this.$route.params.productId }
// return this.$store.getters.objectFromId(payload)
console.log('working')
return { title: 'Awesome Title', description: 'Awesome Description' }
}
},
watch: {
product() {
this.title = this.product.title,
this.description = this.product.description
}
}
}
</script>
I'm expecting the watch to trigger when product is returned, but it doesn't.
I could set the properties in the computed property like so:
computed: {
product() {
const payload = { collection: 'products', id: this.$route.params.productId }
const product = this.$store.getters.objectFromId(payload)
this.title = product.title
this.description = product.description
return product
}
}
But then the compiler gives me a warning: error: Unexpected side effect in "product" computed property
Accordingly to OP's comments, his intention is to get and load some initial data.
The common way to achieve this behavior is to place it inside created or mounted vuejs lifecycle hooks.
<template>
<div v-if="product" class="section">
<form>
<div class="control"><input type="text" class="input" v-model="title"></div>
<div class="control"><input type="text" class="input" v-model="description"></div>
</form>
</div>
</template>
<script>
export default {
data() {
return {
title: '',
description: ''
}
},
created() {
this.getInitialData();
this.foo();
console.log("created!");
},
methods: {
getInitialData: function(){
const payload = {
collection: 'products',
id: this.$route.params.productId
};
var product = this.$store.getters.objectFromId(payload);
this.title = product.title;
this.description = product.description;
},
foo: function(){// ...}
},
}
</script>
Your structure is a bit all over the place. product is a computed, so it runs whenever it's source values change. (You have no control over when it runs.) It shouldn't have side effects (assignments this.description, this.title), or trigger network requests.
The code in product is fetching your source data. This belongs in methods, linked explicitly to a user action or a lifecyle event.
Why do you need to copy your data (this.description = product.description in watch:product)? Vue works best when you have your data (your app state) outside Vue, in a global variable say. Then your Vue components just transparently reflect whatever the app state is at a given moment.
Hope this helps.
Try the following:
watch: {
product: {
immediate: true,
handler(value) {
updateCode();
}
}
}

Reset values within dom-repeat

I'm using a dom-repeat in Polymer. The corresponding list includes an initial value that should be set whenever the list of the dom-repeat is reset. However, when the first element of the list keeps the same initial value, the value is not reset even though I completely empty the list before resetting it to the new value. Here's my minimum example:
<dom-module id="console-app">
<template>
<div id="command-selection">
<paper-dropdown-menu id="command" label="Function">
<paper-listbox slot="dropdown-content" selected="{{_commandIndex}}">
<paper-item>A</paper-item>
<paper-item>B</paper-item>
</paper-listbox>
</paper-dropdown-menu>
</div>
<div id="parameters">
<template is="dom-repeat" items="[[_parameterData]]">
<parameter-block name="[[item.name]]" initial-value="[[item.initialValue]]" ></parameter-block>
</template>
</div>
</template>
<script>
class ConsoleApp extends Polymer.Element {
static get is() {
return 'console-app';
}
static get properties() {
return {
_commandIndex: {
type: Number,
value: -1,
observer: '_onIndexChange'
},
_parameterData: {
type: Array,
value: () => { return []; }
}
};
}
_onIndexChange() {
this.set('_parameterData', []);
switch (this._commandIndex) {
case 0:
this.set('_parameterData', [
{ name: 'AAA', initialValue: '111'},
{ name: 'BBB', initialValue: '123'}
]);
break;
case 1:
this.set('_parameterData', [
{ name: 'CCC', initialValue: '112'}
]);
break;
}
}
}
customElements.define(ConsoleApp.is, ConsoleApp);
</script>
</dom-module>
parameter-block:
<dom-module id="parameter-block">
<template>
<paper-input id="non-bool-value" label="[[name]]"
value="{{_value}}"></paper-input>
</template>
<script>
class ParameterBlock extends Polymer.Element {
static get is() {
return 'parameter-block';
}
static get properties() {
return {
_value: {
type: String,
value: () => { return ''; }
},
initialValue: {
type: String,
value: () => { return ''; },
observer: '_onInitialValueChange'
},
name: {
type: String,
value: () => { return ''; }
}
};
}
_onInitialValueChange() {
this.set('_value', this.initialValue);
}
}
customElements.define(ParameterBlock.is, ParameterBlock);
</script>
</dom-module>
When the index of the dropdown menu changes I reset _parameterData to [] and would assume that after that all future changes to _parameterData are evaluated as new elements. However, it seems like the list remembers the previous initial value after all as the corresponding listener is not called and previous changes to the first text element don't reset to 111 even though I'm changing the selection. If I use different initial values everything works fine, so I assume that I need to tell Polymer somehow to properly reset the elements, but how?
Since, you were only observing the initialValue the application will not know that the initialValue you were changing is for different name. That is why it is not resetting to default values you assigned.
You will need to observe both properties name and initialValue. So, change your observer code to :
static get observers() {
return [
'_onInitialValueChange(name, initialValue)'
]
}
and your method to:
_onInitialValueChange(name, initialValue) {
this.set('_value', this.initialValue);
}
I have updated the plnkr link provided.

Categories

Resources