I am trying to use class.bind in a way that makes it dependent on checked.bind.
My use case is pretty simple. I have a list of items, displayed using a table. Each row of this table has a checkbox. I want to mark a row as "selected" whenever the corresponding checkbox (of the row) is checked.
For this I have used following binding:
<table class="table table-striped table-hover table-responsive">
<tbody>
<tr repeat.for="item of items" class.bind="$parent.selectedItems.indexOf(item)>-1?'info':''">
<td>
<input type="checkbox" checked.bind="$parent.selectedItems" model.bind="item" />
</td>
<td>${item.id}</td>
<td>${item.name}</td>
</tr>
</tbody>
</table>
However, the same doesn't work as intended, and this can be seen in this plunk.
As a workaround I used a getter with #computedFrom('selectedItems', 'items') and/or declarePropertyDependencies(App, 'classes', ['selectedItems', 'items']); , as follows:
import {computedFrom, declarePropertyDependencies} from "aurelia-framework";
export class App {
...
#computedFrom('selectedItems', 'items')
get classes() {
const self = this;
const retval= self.items.map((item: any) => {
return self.selectedItems.indexOf(item) > -1 ? "info" : "";
});
return retval;
}
}
//declarePropertyDependencies(App, 'classes', ['selectedItems', 'items']);
However, this too does not work as can be seen here in this workaround plunk.
It only works, if none of #computedFrom and/or declarePropertyDependencies is used, and that obviously involves dirty-checking.
Is there a clean way to do this?
The binding system will reevaluate the class binding expression class.bind="$parent.selectedItems.indexOf(item)>-1?'info':''" anytime a property used in the expression changes. The selectedItems property never changes, it remains the same array instance. Understandably this is a little confusing because the array instance is mutating. Here's a workaround you can use: add selectedItems.length to the binding expression... we know it will change when items are pushed/popped from the array.
Here's an example: https://gist.run?id=09d32941842352ff0025
app.html
<template>
<p>${message}</p>
<table class="table table-striped table-hover table-responsive">
<tbody>
<tr repeat.for="item of items" class.bind="selectedItems.length === 0 || selectedItems.indexOf(item) === -1 ? '' : 'info'">
<td>
<input type="checkbox" checked.bind="selectedItems" model.bind="item" />
</td>
<td>${item.id}</td>
<td>${item.name}</td>
</tr>
</tbody>
</table>
${selectedItems.length} items selected
</template>
app.js
export class App {
constructor(router) {
this.message = "Hello World!";
this.items = [{
id: 1,
name: "A"
}, {
id: 2,
name: "B"
}, {
id: 3,
name: "C"
}];
this.selectedItems=[];
}
}
Related
I am trying to make form project. When person submmit to personal info to form. I want add this info to Table. But I get this error when I am adding info to table.
Uncaught Error: Objects are not valid as a React child (found: object with keys {inputValue}). If you meant to render a collection of children, use an array instead.
I know this is related to const newToTable part. But I don't know how to make it right. Please help me, ıf you know how can ı solve this problem.
This is my contact.js
const buttonOnClick = () => {
if (inputValue === "" || emailValue === "" || phoneNumberValue === "") {
setShowModal(false);
} else {
setShowModal(true);
// setInputValue("");
//setEmailValue("");
//setPhoneValue("");
// sa
}
const newToTable = [...addFormData, { fullName:{inputValue},email:{emailValue}, phoneNumber:{phoneNumberValue},country:{countryValue} }];
setAddFormData(newToTable);
console.log(`Form submitted, ${showModal}`);
}
This is my AddTable.js
<div className="app-container">
<form>
<Table>
<thead>
<tr>
<th>Full Name</th>
<th>Email </th>
<th>Phone Number</th>
<th>Country</th>
</tr>
</thead>
<tbody>
{addFormData.map((addForm) => (
<tr>
<td>{addForm.fullName}</td>
<td>{addForm.email}</td>
<td>{addForm.phoneNumber}</td>
<td>{addForm.counry}</td>
</tr>
))}
</tbody>
</Table>
</form>
</div>
You've accidently wrapped your form values in objects here:
const newToTable = [...addFormData, { fullName:{inputValue},email:{emailValue}, phoneNumber:{phoneNumberValue},country:{countryValue} }];
For example fullName:{inputValue} will evalute to fullName: { inputValue: 'the value' }
Instead you need:
const newToTable = [
...addFormData,
{
fullName: inputValue,
email: emailValue,
phoneNumber: phoneNumberValue,
country: countryValue,
},
];
This is what the error means by Objects are not valid as a React child - when it tries to render your table, the values being passed are objects such as { inputValue: 'the value' } (this is the (found: object with keys {inputValue}) part of the error - an object with inputValue as a key).
I am trying to render an object held in state. The contents of this.state is:
"statement": {
"creator": "8ff243b2-f21e-43f3-9090-4aa679fbeb1a",
"id": "bf4c965e-bd59-48c8-b31a-8f67529e5fde",
"impactors": [
"978388e8-2987-4c89-82b6-4da619d82935",
"1d75e2a7-bf2a-4f55-ba68-373752b24f98"
],
"score": {
"impactors": 2,
"net": 3,
"percentage": 0.75,
"total_votes": 4
},
"statement": "The Earth is round."
}
In my React JS app, I am able to render some of the child object (statement) but not all. Here is my code:
renderStatement() {
return (
<tbody>
<tr>
<td>Statement: </td>
<td>{this.state.statement.score}</td>
</tr>
</tbody>
)
}
As expected, the above code returns an error: Error: Objects are not valid as a React child (found: object with keys {impactors, net, percentage, total_votes}). If you meant to render a collection of children, use an array instead.
What I actually want is percentage, which is a node underneath score. When I try to drill down and have it just render percentage, like this:
renderStatement() {
return (
<tbody>
<tr>
<td>Statement: </td>
<td>{this.state.statement.score.percentage}</td>
</tr>
</tbody>
)
}
I get the following error: TypeError: Cannot read property 'percentage' of undefined.
Any of the other objects ('creator', 'id', 'impactors', 'statement') work just fine.
This is how I fixed it:
renderStatement() {
const score = this.state.statement.score || {}
return (
<tbody>
<tr>
<td>Statement: </td>
<td>{score.percentage}</td>
</tr>
</tbody>
)
}
you can do the following:
renderStatement() {
const text = (typeof this.state.statement.score !== "undifined") ?
this.state.statement.score.percentage : ''
return (
<tbody>
<tr>
<td>Statement: </td>
<td>{text}</td>
</tr>
</tbody>
)
}
I'm trying to filter a default value if no result is found on my query.
I tried using ng-template, but I simply don't manage to do it.
Rather than writing, here are images to better understand :
This is my successful filter : it correctly shows my datas once I filter them in the search box.
However if I try to enter an invalid data as done below :
It simply returns me an empty html table.
Which is not what I'd like to achieve. I'd like it instead to show a message saying : no data found.
How can I do it please?
And here is my source code :
Component :
import {Component, OnInit} from '#angular/core';
import {IProduct} from './product';
#Component({
selector: 'pm-products',
templateUrl: './product-list.component.html',
styleUrls: ['./product-list.component.css']
})
export class ProductListComponent implements OnInit {
///////////////////////////////////// PROPERTIES //////////////////////////////////////
// String Interpolation
pageTitle = 'Product List';
// Property binding
imageWidth = 50;
imageMargin = 2;
// Event binding
showImage = false;
// Two-way binding
// listFilter = 'cart';
// Filter products
private _listFilter: string;
// Filter Products Array
filteredProducts: IProduct[];
///////////////////////////////////// CONSTRUCTOR //////////////////////////////////////
constructor() {
this.filteredProducts = this.products;
this.listFilter = 'cart';
}
/////////////////////////////////// GETTERS/SETTERS ///////////////////////////////////
get listFilter(): string {
return this._listFilter;
}
set listFilter(value: string) {
this._listFilter = value;
/***
* If there is a list of filtered value, show the list of the filtered values => this.performFilter(this.listFilter)
* Else, (if there is no filtered) return the whole set of products
*/
this.filteredProducts = this.listFilter ? this.performFilter(this.listFilter) : this.products;
}
/////////////////////////////////////// METHODS ///////////////////////////////////////
// Get Products
products: IProduct[] = [
{
productId: 2,
productName: 'Garden Cart',
productCode: 'GDN-0023',
releaseDate: 'March 18, 2019',
description: '15 gallon capacity rolling garden cart',
price: 32.99,
starRating: 4.2,
imageUrl: 'assets/images/garden_cart.png'
},
{
productId: 5,
productName: 'Hammer',
productCode: 'TBX-0048',
releaseDate: 'May 21, 2019',
description: 'Curved claw steel hammer',
price: 8.9,
starRating: 4.8,
imageUrl: 'assets/images/hammer.png'
},
];
performFilter(filterBy: string): IProduct[] {
/**
* filterBy result => to lower case. => case insensitive comparison.
* Return a new array of the filtered productS by the product name,
* by checking if that product name given is an index of the an element in the product array.
*/
filterBy = filterBy.toLowerCase(); // 1.
return this.products.filter((product: IProduct) => product.productName.toLowerCase().indexOf(filterBy) !== - 1); // 2.
}
toggleImage = (): void => {
this.showImage = !this.showImage;
}
////////////////////////////////// LIFECYCLE HOOKS ///////////////////////////////////
ngOnInit(): void {
console.log('hello');
}
}
HTML
<div class="card">
<div class="card-header">{{pageTitle}}</div>
<!-- CARD -->
<div class="card-body">
<div class="row">
<div class="col-md-2"> Filter by:</div>
<div class="col-md-4">
<input [(ngModel)]="listFilter" type="text"/>
</div>
</div>
<div class="row">
<div class="col-md-6">
<h4>Filtered by: {{listFilter}} </h4>
</div>
</div>
<!-- ./CARD -->
<!-- TABLE -->
<div class="table-responsive">
<table *ngIf="products && products.length" class="table">
<thead>
<tr>
<th>
<button (click)="toggleImage()" class="btn btn-primary">{{showImage ? "Hide" : "Show"}} image</button>
</th>
<th>Product</th>
<th>Code</th>
<th>Available</th>
<th>Price</th>
<th>5 Star Rating</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let product of filteredProducts">
<td><img *ngIf="showImage" [src]="product.imageUrl" [title]="product.productName"
[style.width.px]="imageWidth" [style.margin.px]="imageMargin" alt=""></td>
<td>{{product.productName}}</td>
<td>{{product.productCode | lowercase | convertToSpaces: '-'}}</td>
<td>{{product.releaseDate}}</td>
<td>{{product.price | currency: 'EUR':'symbol':'2.2-2'}}</td>
<td>{{product.starRating}}</td>
</tr>
</tbody>
</table>
</div>
<!-- ./TABLE -->
</div>
</div>
Please take care of yourself.
Maybe just include an *ngIf="!filteredProducts.length" wherever you'd like to show your message.
ex.
<tr *ngFor="let product of filteredProducts">
//Your table stuff
</tr>
<tr *ngIf="!filteredProducts.length">
<td colspan="6">No data found</td>
</tr>
You have to add tr contains string no data found after ngfor tr
<tr *ngFor="let product of filteredProducts">
<td><img *ngIf="showImage" [src]="product.imageUrl" [title]="product.productName"
[style.width.px]="imageWidth" [style.margin.px]="imageMargin" alt=""></td>
<td>{{product.productName}}</td>
<td>{{product.productCode | lowercase | convertToSpaces: '-'}}</td>
<td>{{product.releaseDate}}</td>
<td>{{product.price | currency: 'EUR':'symbol':'2.2-2'}}</td>
<td>{{product.starRating}}</td>
</tr>
<tr *ngIf="filteredProducts.length === 0 " >
<td colspan="6" >Your message here </td>
</tr>
You can simply add a row that appears only when the list is empty and the search bar has any value. like this:
<tr *ngFor="let product of filteredProducts">...</tr>
<tr col-span ="6" *ngIf="!filteredProducts.length && listFilter.length"> uh oh,
could not find what you searched for</tr>
I have the following HTML code with vue.js bindings. The code shows a list of items in a table.
<table id="test-vue">
<tr v-for="(item, index) in items">
<td>
<input type="text" v-model="item.foreignId" />
</td>
<td>
<div v-if="canApproveItem(item, foreignValues)">
// UI allowing to approve the item
</div>
</td>
</tr>
</table>
The Vue app is defined as:
new Vue({
el: "#test-vue",
data: function() {
return {
items: [
{
foreignId: '4943',
}
],
foreignValues: [
{ id: '2424', canApprove: false },
{ id: '4943', canApprove: true }
],
computed: {
canApproveItem: function(item, foreignValues) {
let foreign = foreignValues(obj => {
return obj.id === item.foreignId;
});
if (foreign) {
return foreign;
} else {
return false;
}
}
}
})
The goal is to show the approve UI only for items which links to a "foreign" whose canApprove property is set to true. The user can edit the foreignId and the user interface should reflect the user-made change of the foreignId values.
If I run the above, the item argument of the canApproveItem function is the Vue object and foreignValues is undefined. I also get a Error in render: "TypeError: canApproveItem is not a function" error in the console.
How to do it properly?
This sounds like a method. You shouldn't pass in foreignValues, as you can access that from the method itself.
<table id="test-vue">
<tr v-for="(item, index) in items">
<td>
<input type="text" v-model="item.foreignId" />
</td>
<td>
<div v-if="canApproveItem(item)">
// UI allowing to approve the item
</div>
</td>
</tr>
</table>
and
new Vue({
// data and stuff here...
methods: {
canApproveItem(item) {
return this.foreignValues.findIndex(obj => obj.id === item.foreignId) !== -1;
}
}
})
I have a table where the row elements are all populated by child components. There is a checkbox in each of these child components. Now I want to get all checked checkboxes at once. I could use prefs emit as two way binding and update an array or object on the parent but I am wondering if there is better way for this.
Here a short example for the template part:
<table>
<thead>
<tr>
<th> Check </th>
<th> Title </th>
</tr>
</thead>
<list-tbody v-for="element in elements" :element="element"> </list-tbody>
</table>
and this is the child component
<tbody>
<tr>
<td>
<input type="checkbox">
</td>
<td> {{element.title}} </td>
</tr>
</tbody>
As mentioned in the comments you could handle this in two ways:
Use Vuex and mutate the elements array from the child component.
Emit an event on each selection click event to the parent and the parent will update the elements array.
You prefer the second because you're not using Vuex and that's OK.
Once you're having the data "linked" between child component and parent you can use a filter method to only show the selected elements.
Please have a look at the demo below or the fiddle from my comment.
const listTbodyVuex = {
props: ['element'],
template: `
<tbody>
<tr>
<td>
<input type="checkbox" #click="selected">
</td>
<td> {{element.title}} </td>
</tr>
</tbody>
`,
methods: {
...Vuex.mapMutations(['changeSelection']),
selected(evt) {
//console.log('clicked', evt.target.checked, this.changeSelection)
// changeSelection mutation could be also called with-out mapping
// this.$store.commit('changeSelection', ...);
this.changeSelection({
id: this.element.id, selected: evt.target.checked
});
}
}
}
const listTbodyEvents = {
props: ['element'],
template: `
<tbody>
<tr>
<td>
<input type="checkbox" #click="selected">
</td>
<td> {{element.title}} </td>
</tr>
</tbody>
`,
methods: {
selected(evt) {
console.log('clicked', evt.target.checked)
this.$emit('selected', {
element: this.element,
newSelection: evt.target.checked
})
}
}
}
const store = new Vuex.Store({
state: {
elements: [
{
id: 0,
title: 'first',
selected: false
},
{
id: 1,
title: 'second',
selected: false
},
{
id: 2,
title: 'third',
selected: false
}
]
},
mutations: {
changeSelection(state, {id, selected}) {
let element = state.elements
.filter((element) => element.id === id)[0];
element.selected = selected;
//console.log('update element', JSON.parse(JSON.stringify(element)));
Vue.set(state.elements, element.id, element);
}
}
})
new Vue({
el: '#app',
store,
data() {
return {
elements: [
{
id: 0,
title: 'first',
selected: false
},
{
id: 1,
title: 'second',
selected: false
},
{
id: 2,
title: 'third',
selected: false
}
]
}
},
computed: {
...Vuex.mapState({
vuexElements: (state) => state.elements
})
},
components: {
listTbodyEvents,
listTbodyVuex
},
methods: {
updateElement(data) {
let element = this.elements
.filter((element) => element.id === data.element.id)[0];
element.selected = data.newSelection;
// console.log('update', element)
},
filterSelected(data) {
// console.log('filter', data.filter((item) => console.log(item.selected)))
return data.filter((item) => item.selected);
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.2/vue.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vuex/3.0.0/vuex.js"></script>
<div id="app">
<h1>Example with vuex</h1>
<table>
<thead>
<tr>
<th> Check </th>
<th> Title </th>
</tr>
</thead>
<list-tbody-vuex v-for="element in elements" :element="element" :key="element.id"> </list-tbody-vuex>
</table>
<pre>only selected: {{filterSelected(vuexElements)}}</pre>
<pre>{{vuexElements}}</pre>
<hr/>
<h1>Example with events</h1>
<table>
<thead>
<tr>
<th> Check </th>
<th> Title </th>
</tr>
</thead>
<list-tbody-events v-for="element in elements" :element="element" :key="element.id" #selected="updateElement"> </list-tbody-events>
</table>
<pre>only selected: {{filterSelected(elements)}}</pre>
<pre>{{elements}}</pre>
</div>
You should really stick to emitting values to maintain separation of your components. That being said, you can do the following if you really wanted to grab the data all at once:
First, you'll need a data attribute in your child component that your checkbox uses with v-model. For the sake of this example, let's just call it checkbox_value. Once you've done that, you can do something like the following in your parent component method:
var checkbox_values = [];
this.$children.forEach(function(child) {
//you can check the child type here if you have other non-checkbox children
checkbox_values.push(child.$data.checkbox_value);
});
Of course, I wouldn't encourage you to do something like this, but you'll have to make that judgement call.
Note: The above will return values only. You could push object key/value pairs instead!