I have some checkboxes which I'd like to use to show/hide rows in a table whose contents match the values of the checkbox which is selected.
Checkboxes:
<input type='checkbox' name='foo1' value='foo1' v-model="selectedType"/> foo1
<input type='checkbox' name='foo2' value='foo2' v-model="selectedType"/> foo2
<input type='checkbox' name='bar1' value='bar1' v-model="selectedType"/> bar1
I have an object which I used to construct a table using v-for:
<table>
<template v-for="sampleItem in sampleObj">
<tr>
<td>{{sampleItem.item}}</td>
<td>{{sampleItem.description}}</td>
</tr>
</template>
</table>
JS:
new Vue({
data: {
selectedType: [],
sampleObj = [{'item': 'item1', 'description': 'foo1 blah'},
{'item': 'item2', 'description': 'foo2 vlah'},
{'item': 'item3', 'description': 'bar1 nlah'},
{'item': 'item4', 'description': 'bar2 clah'},
];
}
});
By default, the checkboxes are unchecked. So, only the row which has a cell with description 'bar2' is initially visible. Then when I toggle the other checkboxes, the other rows should also become visible (the descriptions don't match the checkbox values verbatim but has a few words following it. I can do some string processing here).
I thought I could use the v-if directive in the tag to look at value of selectedType, but I am not sure how I can accomplish this.
Pseudo-code:
<tr v-if="selectedType ~= /sampleItem.description/">
...
...
</tr>
How could I accomplish this?
You actually have two conditions you need for the v-if: if there is no checkbox matching the description, you want the row to display; if there is a checkbox, it has to be checked.
I put the checkbox values into data, where they belong. I made a method for the test. It first looks to see whether the description matches any checkbox value, then it checks whether the matched value is selected.
new Vue({
el: '#app',
data: {
selectedType: [],
sampleObj: [{
'item': 'item1',
'description': 'foo1 blah'
},
{
'item': 'item2',
'description': 'foo2 vlah'
},
{
'item': 'item3',
'description': 'bar1 nlah'
},
{
'item': 'item4',
'description': 'bar2 clah'
},
],
cbValues: ['foo1', 'foo2', 'bar1']
},
methods: {
isVisible(row) {
const matchedValue = this.cbValues.find(v => row.description.indexOf(v) >= 0);
if (!matchedValue) {
return true;
}
return this.selectedType.includes(matchedValue);
}
}
});
td {
border: thin solid black;
}
<script src="//unpkg.com/vue#latest/dist/vue.js"></script>
<div id="app">
<div v-for="val in cbValues">
<label>
<input type='checkbox' :value='val' v-model="selectedType">
{{val}}
</label>
</div>
<table>
<template v-for="sampleItem in sampleObj">
<tr v-if="isVisible(sampleItem)">
<td>{{sampleItem.item}}</td>
<td>{{sampleItem.description}}</td>
</tr>
</template>
</table>
</div>
Related
I'm retrieving a data set that has a few fields as a list included. The results need to be shown in a Buefy table (https://buefy.org/documentation/table) and I would like to show the list items as separate tags (https://buefy.org/documentation/tag/) in the table cell.
The code below simulates the issue. The result of this is showing the data in the second column as plain text Value1,Value2,Value3.
Not only does this look bad, but because there are no spaces between the values, it makes the table too wide for the screen and other columns are not visible anymore because of it.
I would like it to look something like this in the List cell:
The code to reproduce:
<template>
<b-table :data="data" :columns="columns"></b-table>
</template>
<script>
export default {
data() {
return {
data: [
{ 'id': 1, 'list': ["Value1","Value2","Value3"] },
{ 'id': 2, 'list': ["Value1","Value2","Value3"] },
{ 'id': 3, 'list': ["Value1","Value2","Value3"] }
],
columns: [
{
field: 'id',
label: 'ID',
},
{
field: 'list',
label: 'List',
}
]
}
}
}
</script>
Try out the following custom rendering and add the class helper mr-2 to make space between tags :
<b-table :data="data">
<b-table-column field="id" label="ID" centered v-slot="props">
{{props.row.id}}
</b-table-column>
<b-table-column field="list" label="List" centered v-slot="props">
<span v-for="item in props.row.list" class="tag mr-2">
{{item}}
</span>
</b-table-column>
</b-table>
Live demo
I'm dynamically binding to an Array of objects in a Vue.js 2.0 app. I want to respond to changes as values in that Array change. At this time, as shown in this Fiddle, I have the following:
html
<div id="app">
<table>
<thead>
<tr>
<th v-for="(col, index) in cols">
<input :placeholder="col.title" v-model="inputValues[col.prop]" />
</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in items">
<td>{{ item.name }}</td>
<td>{{ item.age }}</td>
<td>{{ item.birthday }}</td>
</tr>
</tbody>
</table>
<hr />
<textarea>{{ inputValues }}</textarea>
</div>
javascript
new Vue({
el: '#app',
data: {
cols: [
{ title: 'Name', prop:'name' },
{ title: 'Age', prop:'age' },
{ title: 'Birthday', prop:'birthday' },
],
inputValues: [],
items: [
{ id:1, name:'Andreas Winchell', age:47, birthday:'08-04-1970' },
{ id:2, name:'Victoria Hodges', age:80, birthday:'01-24-1937' },
{ id:2, name:'James Morris', age:59, birthday:'06-14-1958' },
{ id:2, name:'Larry Walker', age:68, birthday:'08-07-1949' },
{ id:2, name:'Lee Maynard', age:46, birthday:'04-17-1971' }
]
},
methods: {
buttonClick: function() {
alert(JSON.stringify(this.inputValues));
}
}
})
I can't seem to find a way to bind to changes in any of the values entered into the header. How do I detect and react to property values changes in an Array?
Update 2022
Vue 3 will detect all changes in a reactive array. Square bracket syntax is no longer a limitation.
You can use arrays in your data, but Vue will not detect direct changes to the array referencing an item by its index. Vue will detect changes made with push(), pop(), slice() etc.
Arrays like 'Cols' are a disaster. 'Name', 'Age' and 'Birthday' are code (property names), not data. You really don't want to be iterating over an array generating forms like this. Keep it simple and code your three inputs.
inputValues should be an object, not an array.
data: {
cols: [
{ title: 'Name', prop:'name' },
{ title: 'Age', prop:'age' },
{ title: 'Birthday', prop:'birthday' },
],
// inputValues should be an object.
inputValues: {name: null, age: null, birthday: null },
items: [
{ id:1, name:'Andreas Winchell', age:47, birthday:'08-04-1970' },
{ id:2, name:'Victoria Hodges', age:80, birthday:'01-24-1937' },
{ id:2, name:'James Morris', age:59, birthday:'06-14-1958' },
{ id:2, name:'Larry Walker', age:68, birthday:'08-07-1949' },
{ id:2, name:'Lee Maynard', age:46, birthday:'04-17-1971' }
]
}
As stated here in the VueJS documentation.
Due to limitations in JavaScript, Vue cannot detect the following changes to an array:
When you directly set an item with the index, e.g.
vm.items[indexOfItem] = newValue
To overcome this, both of the following will accomplish the same as vm.items[indexOfItem] = newValue, but will also trigger state updates in the reactivity system:
// Vue.set
Vue.set(vm.items, indexOfItem, newValue)
// Array.prototype.splice
vm.items.splice(indexOfItem, 1, newValue)
The issue is that when I bind options to the dropdown, "ui dropdown" makes it disappear and nothing is in the cell in my browser(it dosent use the css properly in jsfiddle). If i remove that css then i see the out of the box dropdown.
creating a table with a viewmodel collection and want a dropdown of values for the individual risks
//part of the viewmodel
var ViewModel = {
Collection: ko.observableArray(),
availableRisks: ['L', 'H'],
using
$('.ui.dropdown').dropdown();
doesn't help.
Sample code that dosent work with the dropdown:
http://jsfiddle.net/7vh2t33m/2/
There are bindings for jQuery UI and knockout, use them. http://gvas.github.io/knockout-jqueryui/
Rule of thumb, in a knockout application nothing may touch the DOM except knockout, or without informing knockout. Therefore, mixing knockout with jQuery UI without anything that bridges the gap between them will not work.
Taken from the example in knockout-jqueryUI selectmenu binding documentation:
var ViewModel = function () {
this.items = ko.observableArray([
{ id: '1', text: 'First' },
{ id: '2', text: 'Second' },
{ id: '3', text: 'Third' },
{ id: '4', text: 'Fourth' }
]);
this.value = ko.observable('1');
};
ko.applyBindings(new ViewModel());
and in the view
<!-- ko foreach: items -->
<input type="radio" name="radios" data-bind="attr: { value: id }, checked: $parent.value" />
<!-- /ko -->
<br/>
<select data-bind="value: value, selectmenu: { width: 300 }, options: items, optionsValue: 'id', optionsText: 'text'">
</select>
<br />
Description
I have a small product order system, where a user can add order lines, and on each order line add one or more products. (I realise it's quite unusual for more than one product to be on the same order line, but that's another issue).
The products that can be selected on each line is based on a hierarchy of products. For example:
Example product display
T-Shirts
V-neck
Round-neck
String vest
JSON data
$scope.products = [
{
id: 1,
name: 'T Shirts',
children: [
{ id: 4, name: 'Round-neck', children: [] },
{ id: 5, name: 'V-neck', children: [] },
{ id: 6, name: 'String vest (exclude)', children: [] }
]
},
{
id: 2,
name: 'Jackets',
children: [
{ id: 7, name: 'Denim jacket', children: [] },
{ id: 8, name: 'Glitter jacket', children: [] }
]
},
{
id: 3,
name: 'Shoes',
children: [
{ id: 9, name: 'Oxfords', children: [] },
{ id: 10, name: 'Brogues', children: [] },
{ id: 11, name: 'Trainers (exclude)', children: []}
]
}
];
T-Shirts isn't selectable, but the 3 child products are.
What I'm trying to achieve
What I'd like to be able to do, is have a 'select all' button which automatically adds the three products to the order line.
A secondary requirement, is that when the 'select all' button is pressed, it excludes certain products based on the ID of the product. I've created an 'exclusion' array for this.
I've set up a Plunker to illustrate the shopping cart, and what I'm trying to do.
So far it can:
Add / remove order lines
Add / remove products
Add a 'check' for all products in a section, excluding any that are in the 'exclusions' array
The problem
However, although it adds the check in the input, it doesn't trigger the ng-change on the input:
<table class="striped table">
<thead>
<tr>
<td class="col-md-3"></td>
<td class="col-md-6"></td>
<td class="col-md-3"><a ng-click="addLine()" class="btn btn-success">+ Add order line</a></td>
</tr>
</thead>
<tbody>
<tr ng-repeat="line in orderHeader.lines">
<td class="col-md-3">
<ul>
<li ng-repeat="product in products" id="line_{{ line.no }}_product_{{ product.id }}">
{{ product.name }} <a ng-click="selectAll(product.id, line.no)" class="btn btn-primary">Select all</a>
<ul>
<li ng-repeat="child in product.children">
<input type="checkbox"
ng-change="sync(bool, child, line)"
ng-model="bool"
data-category="{{child.id}}"
id="check_{{ line.no }}_product_{{ child.id }}"
ng-checked="isChecked(child.id, line)">
{{ child.name }}
</li>
</ul>
</li>
</ul>
</td>
<td class="col-md-6">
<pre style="max-width: 400px">{{ line }}</pre>
</td>
<td class="col-md-3">
<a ng-click="removeLine(line)" class="btn btn-warning">Remove line</a>
</td>
</tr>
</tbody>
</table>
Javascript
$scope.selectAll = function(product_id, line){
target = document.getElementById('line_'+line+'_product_'+product_id);
checkboxes = target.getElementsByTagName('input');
for (var i = 0; i < checkboxes.length; i++) {
if (checkboxes[i].type == 'checkbox') {
category = checkboxes[i].dataset.category;
if($scope.excluded.indexOf(parseInt(category)) == -1)
{
checkboxes[i].checked = true;
// TODO: Check the checkbox, and set its bool parameter to TRUE
}
}
}
}
Update with full solution
There were a couple of issues with the above code. Firstly, I was trying to solve the problem by manipulating the DOM which is very much against what Angular tries to achieve.
So the solution was to add a 'checked' property on the products so that I can track if they are contained on the order line, and then the view is updated automatically.
One drawback of this method is that the payload would be significantly larger (unless it is filtered before being sent to the back-end API) as each order line now has data for ALL products, even if they aren't selected.
Also, one point that tripped me up was forgetting that Javascript passes references of objects / arrays, not a new copy.
The solution
Javascript
var myApp = angular.module('myApp', []);
myApp.controller('CartForm', ['$scope', function($scope) {
var inventory = [
{
id: 1,
name: 'T Shirts',
checked: false,
children: [
{ id: 4, name: 'Round-neck', checked: false, children: [] },
{ id: 5, name: 'V-neck', checked: false, children: [] },
{ id: 6, name: 'String vest (exclude)', checked: false, children: [] }
]
},
{
id: 2,
name: 'Jackets',
checked: false,
children: [
{ id: 7, name: 'Denim jacket', checked: false, children: [] },
{ id: 8, name: 'Glitter jacket', checked: false, children: [] }
]
},
{
id: 3,
name: 'Shoes',
checked: false,
children: [
{ id: 9, name: 'Oxfords', checked: false, children: [] },
{ id: 10, name: 'Brogues', checked: false, children: [] },
{ id: 11, name: 'Trainers (exclude)', checked: false, children: []}
]
}
];
$scope.debug_mode = false;
var products = angular.copy(inventory);
$scope.orderHeader = {
order_no: 1,
total: 0,
lines: [
{
no: 1,
products: products,
total: 0,
quantity: 0
}
]
};
$scope.excluded = [6, 11];
$scope.addLine = function() {
var products = angular.copy(inventory);
$scope.orderHeader.lines.push({
no: $scope.orderHeader.lines.length + 1,
products: products,
quantity: 1,
total: 0
});
$scope.loading = false;
}
$scope.removeLine = function(index) {
$scope.orderHeader.lines.splice(index, 1);
}
$scope.selectAll = function(product){
angular.forEach(product.children, function(item){
if($scope.excluded.indexOf(parseInt(item.id)) == -1) {
item.checked=true;
}
});
}
$scope.removeAll = function(product){
angular.forEach(product.children, function(item){
item.checked=false;
});
}
$scope.toggleDebugMode = function(){
$scope.debug_mode = ($scope.debug_mode ? false : true);
}
}]);
Click here to see the Plunker
You are really over complicating things first by not taking advantage of passing objects and arrays into your controller functions and also by using the DOM and not your data models to try to update states
Consider this simplification that adds a checked property to each product via ng-model
<!-- checkboxes -->
<li ng-repeat="child in product.children">
<input ng-model="child.checked" >
</li>
If it's not practical to add properties to the items themselves, you can always keep another array for the checked properties that would have matching indexes with the child arrays. Use $index in ng-repeat for that
And passing whole objects into selectAll()
<a ng-click="selectAll(product,line)">
Which allows in controller to do:
$scope.selectAll = function(product, line){
angular.forEach(product.children, function(item){
item.checked=true;
});
line.products=product.children;
}
With angular you need to always think of manipulating your data models first, and let angular manage the DOM
Strongly suggest reading : "Thinking in AngularJS" if I have a jQuery background?
DEMO
Why ng-change isn't fired when the checkbox is checked programatically?
It happens because
if($scope.excluded.indexOf(parseInt(category)) == -1)
{
checkboxes[i].checked = true;
// TODO: Check the checkbox, and set its bool parameter to TRUE
}
only affects the view (DOM). ng-change works alongside ngModel, which can't be aware that the checkbox really changed visually.
I suggest you to refer to the solution I provided at How can I get angular.js checkboxes with select/unselect all functionality and indeterminate values?, works with any model structure you have (some may call this the Angular way).
I am having trouble handling checkboxes in Knockout JS.
jsfiddle link: http://jsfiddle.net/wenbert/Xyuhk/72/
Note that I have provided 2 select boxes for each parent (Hero). Each one is using a different way but both are more or less "observing" the same object.
Workflow
Click on a gray box
A box with a blue dotted line should appear containing the items of the Select box.
From here, you can edit the items of the select box.
Fiddle here: http://jsfiddle.net/wenbert/Xyuhk/72/
The Problems
When I remove an item, I am not able to remove it from the Select boxes. Note that I do not want to completely remove it from the object. I just want the current item to be flagged as "isDeleted".
Select A - hides the item but it leaves an empty option in the select box.
Select B - the "ifnot: isDeleted" is not having any effect on the options.
The Question
How do I handle Select Boxes? I have rendered 2 Select Boxes in 2 different ways to try to have the ifnot: isDeleted take effect but none of them are working.
Update: With this setup, how do I do the "selected" value of the select box?
HTML
<button data-bind="click: addHero">Add Hero</button>
<ul data-bind="foreach: heroes">
<li class="parent" data-bind="ifnot: isDeleted, click: $parent.selectHero">
<input class="big-box" type="text" data-bind="value: name" />
<button class="btn-small" data-bind="click: $parent.removeHero">Remove Hero</button>
<br/>
SKILLS:
Select A) <select data-bind="foreach: skills">
<option data-bind="value: name, text: name, ifnot: isDeleted"></option>
</select>
Select B) <select data-bind="options: skills, optionsText: 'name', value: selected_skill.name, ifnot: isDeleted">
</select>
<br/>
<span class="instructions">(Step 1: Click somewhere here)</span>
</li>
</ul>
<div class="edit-panel" data-bind="if: selectedHero">
Edit Skill:<br/>
<div data-bind="with: selectedHero">
<button class="btn-small" data-bind="click: addSkill">Add Skill</button>
<ul data-bind="foreach: skills">
<li data-bind="ifnot: isDeleted">
<button class="btn-small" data-bind="click: $parent.setAsDefaultSkill">Set as default</button>
<input data-bind="value: name" />
<button class="btn-small" data-bind="click: $parent.removeSkill">Remove Skill</button>
</li>
</ul>
<span class="instructions">(Step 2: Remove a Skill, then have a look at the Select Box above.)</span>
</div>
</div>
<pre data-bind="text: ko.toJSON($data, null, 2)"></pre>
Javascript
var initialData = [
{
id: '1',
name: "Batman",
isDelete: false,
selected_skill: {name: "Boxing", isDeleted: false},
skills: [
{ id: '1', name: "Karate", isDeleted: false },
{ id: '2', name: "Boxing", isDeleted: false},
{ id: '6', name: "Sonar", isDeleted: false}
]
},
{
id: '2',
name: "Hulk",
isDelete: false,
skills: [
{ id: '3', name: "MMA", isDeleted: false },
{ id: '4', name: "Rage", isDeleted: false},
{ id: '5', name: "Extra Strength", isDeleted: false}
]
},
];
function Hero(data) {
var self = this;
self.name = ko.observable(data.name);
self.selected_skill= ko.observable(data.selected_skill);
self.skills = ko.observableArray(ko.utils.arrayMap(data.skills, function(i) {
return new Skills(i);
}));
self.addSkill = function() {
self.skills.push(new Skills({name: '---', isDeleted: false}));
}
self.setAsDefaultSkill = function(item) {
self.selected_skill(item);
}
self.isDeleted = ko.observable(data.isDeleted);
self.removeSkill = function(item) {
item.isDeleted(true);
}
}
function Skills(data) {
var self = this;
self.name = ko.observable(data.name);
self.isDeleted = ko.observable(data.isDeleted);
}
function SuperheroViewModel(data) {
var self = this;
self.heroes = ko.observableArray(ko.utils.arrayMap(data, function(i){
return new Hero(i);
}));
self.selectedHero = ko.observable();
self.selectedHero.skills = ko.observableArray();
self.addHero = function() {
self.heroes.push(
new Hero({
name: 'Wolverine',
isDelete: false,
skills: [{name: 'Breathing', isDeleted: false}],
})
);
}
self.selectHero = function(item) {
self.selectedHero(item);
}
self.removeHero= function(item) {
item.isDeleted(true);
}
}
ko.applyBindings(new SuperheroViewModel(initialData ));
I hope everything is clear.
Any reply will be greatly appreciated.
THanks!
I would do the filtering in the viewmodel. So I would create a filtered collection something like availableSkills
self.availableSkills = ko.computed(function() {
return ko.utils.arrayFilter(self.skills(), function(item) {
return !item.isDeleted();
})
});
And then I would use this in the selects:
<select data-bind="foreach: availableSkills">
<option data-bind="value: name, text: name"></option>
</select>
Demo fiddle.