So I'm trying to build a chart as a webcomponent and I'm having some trouble with mapping an array. The error says: this.values.map is not a function
Here is the code:
import {LitElement, html, css} from 'lit-element';
class ApexChart extends LitElement {
static get properties(){
return{
values: {Array},
labels: {Array}
};
}
constructor(){
super();
this.values = [];
this.labels =[];
}
static get styles(){
return css `
`;
}
render(){
return html`
<p>${this.values.map(value => {value + 1})}</p>
`;
}
}
customElements.define('apex-chart', ApexChart);
I'm passing the values from html
<apex-chart values="[1,2,3,4]" labels="['Hi', 'Hello', 'Oi', 'Hola']"></apex-chart>
I can't see what I'm doing wrong
You have two issues:
1) the properties type converter is not defined correctly. It should be:
static get properties(){
return{
values: { type: Array },
labels: { type: Array }
};
}
2) The map method isn't working correctly. Currently it returns undefined values because if you use {} you must use the return keyword.
> [0, 1].map(value => { value + 1 })
<- (2) [undefined, undefined]
Instead use:
render(){
return html`
<p>${this.values.map(value => value + 1)}</p>
`;
}
Or:
render(){
return html`
<p>${this.values.map(value => { return value + 1; })}</p>
`;
}
Related
I am trying to build a chart with LitElement. The chart takes a data property from the user, and displays this data (the chart plot). It also gets series names from the data, in order to display a legend with a checkbox for each series that can be used to show or hide the data for that series on the chart plot.
The below is a very minimal example where the chart plot is simply divs containing the data points (3, 5, 4, 7), and the legend is just checkboxes. The expected behaviour is that when a checkbox is selected/deselected, the corresponding data in the chart plot (data divs) is shown/hidden. For example, initially both checkboxes are selected by default, and the data for both series is correctly display. However, if I deselect the first checkbox, I expect the data for "series1" to be hidden, so only 5 and 7 are displayed.
It is this checkbox behaviour that I cannot get working. When I select or deselect a checkbox, I log this.series which seems to be correctly updated reflect which checkboxes are selected, however the chart plot (data divs) is not updated.
import { LitElement, css, html } from "lit-element";
import { render } from "lit-html";
class TestElement extends LitElement {
static get properties() {
return {
data: { type: Array },
series: { type: Array },
};
}
constructor() {
super();
this.data = [];
this.series = [];
}
checkboxChange(e) {
const inputs = Array.from(this.shadowRoot.querySelectorAll("input")).map(n => n.checked);
this.series = this.series.map((s, i) => ({ ...s, checked: inputs[i] }));
console.log("this.series", this.series);
}
render() {
this.series = Object.keys(this.data[0]).map(key => ({ key, checked: true }));
const data = this.data.map(d => this.series.map(s => (s.checked ? html`<div>${d[s.key]}</div>` : "")));
const series = this.series.map(
s => html`<input type="checkbox" ?checked=${s.checked} #change=${this.checkboxChange} />`
);
return html`${data}${series}`;
}
}
customElements.define("test-element", TestElement);
render(
html`<test-element
.data=${[
{ series1: "3", series2: "5" },
{ series1: "4", series2: "7" },
]}
></test-element>`,
window.document.body
);
Try the following:
import { LitElement, html } from 'lit-element';
class TestElement extends LitElement {
static get properties() {
return {
data: { attribute: false, accessors: false },
series: { attribute: false, accessors: false },
checked: { attribute: false, accessors: false },
};
}
constructor() {
super();
this.data = [];
this.series = new Map();
this.checked = new Map();
}
get data() {
return this.__data || [];
}
set data(v) {
const oldValue = this.__data;
this.__data = Array.isArray(v) ? v : [];
this.series = new Map();
for (const row of this.data) {
for (const [series, value] of Object.entries(row)) {
this.series.set(series, [...this.series.get(series) || [], value])
}
}
for (const series of this.series.keys()) {
this.checked.set(series, this.checked.get(series) ?? true);
}
this.requestUpdate('data', oldValue);
this.requestUpdate('series', null);
this.requestUpdate('checked', null);
}
checkboxChange(e) {
this.checked.set(e.target.dataset.series, e.target.checked);
this.requestUpdate('checked', null);
}
render() {
return [
[...this.series.entries()].map(([series, values]) => values.map(value => html`
<div ?hidden="${!this.checked.get(series)}">${value}</div>
`)),
[...this.checked.entries()].map(([series, checked]) => html`
<input type="checkbox" ?checked=${checked} data-series="${series}" #change=${this.checkboxChange} />
`)
];
}
}
customElements.define("test-element", TestElement);
Live Example: https://webcomponents.dev/edit/FEbG9UA3nBMqtk9fwQrD/src/index.js
This solution presents a few improvements:
cache the series and checked state when data updates, instead of on each render
use hidden attr to hide unchecked series
use data-attributes to pass serializable data on collection items to event listeners.
use attribute: false instead of type: Array (assuming you don't need to deserialize data from attributes.
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>`
)}
`;
}
I have 2 components using same array binding for example:
{
title: "food",
data : ["data1", "data2", "data3"]
}
title is on parent component and data is binded from parent to child component to child works with the array.
how can i do to when i remove a array data element notify to parent component?
Here the example.
In the example i have a children with binded array and one method to remove array elements and notify. And parent compontent has a observer named arrayChanges.
if code works parent component has to know about child array length, but it doesnt works.
<script type='module'>
import {PolymerElement, html} from 'https://unpkg.com/#polymer/polymer/polymer-element.js?module';
import {} from 'https://unpkg.com/#polymer/polymer#3.1.0/lib/elements/dom-repeat.js?module';
class ParentComp extends PolymerElement {
static get properties() {
return {
myArr: {
type: Array,
observer: "arrayChanges"
},
changes: {
type: String
}
};
}
static get template() {
return html`
<div>[[myArr.title]]</div>
<children-comp data='[[myArr.data]]'></children-comp>
<div>[[changes]]</div>
`;
}
ready(){
super.ready();
this.myArr = {
title : "My component",
data : [
{titulo: "titulo1", comment : "im comment number 1"},
{titulo: "titulo2", comment : "im comment number 2"}
]
}
}
arrayChanges(){
this.changes = "Array length : "+this.myArr.data.length;
console.log("the Array has been changed");
}
}
class ChildrenComp extends PolymerElement {
static get properties() {
return {
data: {
type: Array,
notify: true
}
};
}
static get template() {
return html`
<ul>
<dom-repeat items='[[data]]' >
<template>
<li>
[[index]] )
[[item.titulo]]
[[item.comment]]
<button data-index$='[[index]]' on-click='handle_button'>Borrar</button>
<hr>
</li>
</template>
</dom-repeat>
</ul>
`;
}
handle_button(e){
var index = e.currentTarget.dataset.index;
this.notifyPath("data");
this.splice("data", index, 1);
}
}
customElements.define('children-comp', ChildrenComp);
customElements.define('parent-comp', ParentComp);
</script>
<parent-comp></parent-comp>
The parent component would only handle change-notifications in two-way bindings (using curly brackets). Your data binding incorrectly uses one-way binding (square brackets).
<children-comp data='[[myArr.data]]'></children-comp>
^^ ^^ square brackets: one-way binding
<children-comp data='{{myArr.data}}'></children-comp>
^^ ^^ curly brackets: two-way binding
Also note the simple observer (specified in myArr-property declaration) does not detect array mutations. You should use a complex observer instead. You could observe data/length changes and/or array mutations:
static get properties() {
return {
myArr: {
// type: Array, // DON'T DO THIS (myArr is actually an object)
type: Object,
// observer: 'arrayChanges' // DON'T DO THIS (doesn't detect array splices)
}
}
}
static get observers() {
return [
'arrayChanges(myArr.data, myArr.data.length)', // observe data/length changes
'arraySplices(myArr.data.splices)', // observe array mutations
]
}
arrayChanges(myArrData, myArrDataLength) {
console.log({
myArrData,
myArrDataLength
})
}
arraySplices(change) {
if (change) {
for (const s of change.indexSplices) {
console.log({
sliceIndex: s.index,
removedItems: s.removed,
addedItems: s.addedCount && s.object.slice(s.index, s.index + s.addedCount)
})
}
}
}
<html>
<head>
<script src="https://unpkg.com/#webcomponents/webcomponentsjs/webcomponents-loader.js"></script>
</head>
<body>
<script type='module'>
import {PolymerElement, html} from 'https://unpkg.com/#polymer/polymer/polymer-element.js?module';
import {} from 'https://unpkg.com/#polymer/polymer#3.1.0/lib/elements/dom-repeat.js?module';
class ParentComp extends PolymerElement {
static get properties() {
return {
myArr: {
type: Array,
},
changes: {
type: String
}
};
}
static get observers() {
return [
'arrayChanges(myArr.data, myArr.data.length)',
'arraySplices(myArr.data.splices)',
]
}
static get template() {
return html`
<div>[[myArr.title]]</div>
<children-comp data='{{myArr.data}}'></children-comp>
<div>{{changes}}</div>
`;
}
ready(){
super.ready();
this.myArr = {
title : "My component",
data : [
{titulo: "titulo1", comment : "im comment number 1"},
{titulo: "titulo2", comment : "im comment number 2"}
]
}
}
arrayChanges(myArr, myArrLength){
this.changes = "Array length : " + myArrLength;
console.log("the Array has been changed", myArr);
}
arraySplices(change) {
if (change) {
for (const s of change.indexSplices) {
console.log({
sliceIndex: s.index,
removedItems: s.removed,
addedItems: s.addedCount && s.object.slice(s.index, s.index + s.addedCount)
})
}
}
}
}
class ChildrenComp extends PolymerElement {
static get properties() {
return {
data: {
type: Array,
notify: true
}
};
}
static get template() {
return html`
<ul>
<dom-repeat items='[[data]]' >
<template>
<li>
[[index]] )
[[item.titulo]]
[[item.comment]]
<button data-index$='[[index]]' on-click='handle_button'>Borrar</button>
<hr>
</li>
</template>
</dom-repeat>
</ul>
`;
}
handle_button(e){
var index = e.currentTarget.dataset.index;
this.notifyPath("data");
this.splice("data", index, 1);
}
}
customElements.define('children-comp', ChildrenComp);
customElements.define('parent-comp', ParentComp);
</script>
<parent-comp></parent-comp>
</body>
</html>
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.
I would like to use universe:i18n for translating my meteor application (using react).
In this component you can see, that I iterate through an array using map() and as the output I would like to get the categories as translations:
imports/ui/components/example.jsx
import React, { Component } from 'react'
import i18n from 'meteor/universe:i18n'
class Example extends Component {
getCategories(index) {
const categories = [ 'one', 'two', 'three' ]; // <-- Get correct translations of these elements
return categories[index - 1];
}
render() {
return (
<div id="content">
{ this.props.sections.map((i) => {
return (
<div>
{ this.getCategories(i.index) }
</div>
);
}) }
</div>
);
}
}
i18n/de.i18.json
{
categories: {
one: 'Eins',
two: 'Zwei',
three: 'Drei'
}
}
I tried to do it with
const T = i18n.createComponent()
class Example extends Component {
getCategories(index) {
const categories = [ 'one', 'two', 'three' ]; // <-- Get correct translations of these elements
return categories[index - 1];
}
render() {
return (
<div id="content">
{ this.props.sections.map((i) => {
return (
<div>
<T>categories[{ this.getCategories(i.index) }]</T>
</div>
);
}) }
</div>
);
}
}
It won't work, because you have to use dot instead of bracker notation, so
<T>categories.{ this.getCategories(i.index) }</T>
Instead of
<T>categories[{ this.getCategories(i.index) }]</T>
But it still won't work, because it will create an children array, but only string is accepted, so use it like this:
<T children={`categories.${ this.getCategories(i.index) }`} />
Source.