Angular select all checkboxes from outside ng-repeat - javascript

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).

Related

How to update nested array (multi react-select)

I'm creating an app where I need to store selected values in array nested in object (category below). State looks like:
state = {
data: {
user: "",
title: "",
text: "",
category: [], // should store values
},
updateNoteId: null,
}
In my render() I have following form:
<form onSubmit={this.submitNote}>
<Select
name="category"
value={this.state.data.category}
options={options}
onChange={this.handleMultiChange}
multi
/>
<input type="submit" value="Save" />
</form>
Options are:
const options = [
{ value: 1, label: 'one' },
{ value: 2, label: 'two' },
{ value: 3, label: 'three' }
]
So the question is how this.handleMultiChange function should looks like to work. Category[] need to keep all values selected in Select which is react-select component (eg. it should be category = [1,3], when 'one' and 'three' were chosen). I tried many combination but none of them worked so far. I prefer to use ES6 without any external libraries/helpers to do that.
handleMultiChange(selectedOptions) {
this.setState({
data: {
...this.state.data,
categories: selectedOptions
}
})
}

Make a Tree view from JSON data using React JS

First of all i am very new to React JS. So that i am writing this question. I am trying this for three days.
What I have to do, make a list of category, like-
Category1
->Sub-Category1
->Sub-Category2
Categroy2
Category3
.
.
.
CategoryN
And I have this json data to make the listing
[
{
Id: 1,
Name: "Category1",
ParentId: 0,
},
{
Id: 5,
Name: "Sub-Category1",
ParentId: 1,
},
{
Id: 23,
Name: "Sub-Category2",
ParentId: 1,
},
{
Id: 50,
Name: "Category2",
ParentId: 0,
},
{
Id: 54,
Name: "Category3",
ParentId: 0,
},
];
I have tried many open source examples, but their json data format is not like mine. so that that are not useful for me. I have build something but that is not like my expected result. Here is my jsfiddle link what i have done.
https://jsfiddle.net/mrahman_cse/6wwan1fn/
Note: Every subcategory will goes under a category depend on "ParentId",If any one have "ParentId":0 then, it is actually a category, not subcategory. please see the JSON
Thanks in advance.
You can use this code jsfiddle
This example allows to add new nested categories, and do nested searching.
code with comments:
var SearchExample = React.createClass({
getInitialState: function() {
return {
searchString: ''
};
},
handleChange: function(e) {
this.setState({
searchString: e.target.value.trim().toLowerCase()
});
},
isMatch(e,searchString){
return e.Name.toLowerCase().match(searchString)
},
nestingSerch(e,searchString){
//recursive searching nesting
return this.isMatch(e,searchString) || (e.subcats.length && e.subcats.some(e=>this.nestingSerch(e,searchString)));
},
renderCat(cat){
//recursive rendering
return (
<li key={cat.Id}> {cat.Name}
{(cat.subcats && cat.subcats.length) ? <ul>{cat.subcats.map(this.renderCat)}</ul>:""}
</li>);
},
render() {
let {items} = this.props;
let {searchString} = this.state;
//filtering cattegories
if (searchString.length) {
items = items.filter(e=>this.nestingSerch(e,searchString))
console.log(items);
};
//nesting, adding to cattegories their subcatigories
items.forEach(e=>e.subcats=items.filter(el=>el.ParentId==e.Id));
//filter root categories
items=items.filter(e=>e.ParentId==0);
//filter root categories
return (
<div>
<input onChange={this.handleChange} placeholder="Type here" type="text" value={this.state.searchString}/>
<ul>{items.map(this.renderCat)}</ul>
</div>
);
}
});

AngularJS: Using ng-selected to select multiple options, based on collection

I have two arrays of objects, one array being a subset of the other:
$scope.taskGroups = [
{id: 1, name: 'group1', description: 'description1'},
{id: 2, name: 'group2', description: 'description2'},
{id: 3, name: 'group3', description: 'description3'}
];
$scope.selectedGroups = [
{id: 1, name: 'group1', description: 'description1'},
{id: 2, name: 'group3', description: 'description3'}
];
After unsuccessfully trying to get my head around using ng-option, I thought that I could perhaps create a function to determine if an option should be selected in the select list, based on what I picked up in the documentation:
ngSelected
- directive in module ng Sets the selected attribute on the element, if the expression inside ngSelected is truthy.
So, I came up with this function:
$scope.inSelectedGroups = function(taskGroup) {
angular.forEach($scope.selectedGroups, function(group) {
if (taskGroup.id == group.id) {
return true;
}
return false;
});
};
and tried to use it in this html:
<select multiple ng-model="selectedGroups" style="width: 100%" size="7">
<option ng-repeat="taskGroup in taskGroups" value="{{taskGroup.id}}" ng-selected="inSelectedGroups(taskGroup)">{{taskGroup.name}}</option>
</select>
but, no dice - the full list of taskGroups shows, but the selectedTaskGroups aren't, well, selected...
Am I barking up the wrong tree here?
the full list of taskGroups shows, but the selectedTaskGroups aren't,
well, selected.
I tried your solution which is using the ngSelected attribute but I was unsuccessful as well so I tried using the ngOptions instead and it works.
angular.module('app', []).controller('TestController', ['$scope',
function($scope) {
$scope.taskGroups = [{
id: 1,
name: 'group1',
description: 'description1'
}, {
id: 2,
name: 'group2',
description: 'description2'
}, {
id: 3,
name: 'group3',
description: 'description3'
}];
$scope.selectedGroups = [{
id: 1,
name: 'group1',
description: 'description1'
}, {
id: 2,
name: 'group3',
description: 'description3'
}];
}
])
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="app" ng-controller="TestController">
<select multiple="true" ng-model="selectedGroups" style="width: 100%" ng-options="taskGroup.id as taskGroup.description for taskGroup in taskGroups track by taskGroup.id" size="7">
</select>
</div>
See carefully, you are returning Boolean value from function defined in angular.forEach parameter and so nothing is returned from inSelectedGroups function
Try modifying your function to:
$scope.inSelectedGroups = function(taskGroup) {
var flag = false;
angular.forEach($scope.selectedGroups, function(group) {
if (taskGroup.id == group.id) {
flag = true;
return;
}
flag = false;
return;
});
return flag;
};

Why this knockout mapping code is not working?

I have a list of json objects which i want to map to knockout viewmodel list using ko mapping plugin. The below is my code (just to explain my problem, no need to take this code seriously):
var itemsList = [
{ Id: 1, Name: 'A' },
{ Id: 2, Name: 'B' },
{ Id: 3, Name: 'C' }
];
var item = function(data) {
ko.mapping.fromJS(data, {}, this);
}
var listOfItems = ko.observableArray();
var listOfItems = ko.mapping.fromJS(itemsList, {
create: function(options) {
return new item(options.data);
}
});
Now my listOfItems are always empty, why ??
You could really simplify this code and it is become working well.
var itemsList = [{ Id: 1, Name: 'A' }, { Id: 2, Name: 'B' }, { Id: 3, Name: 'C' }];
var viewModel = {
listOfItems: ko.mapping.fromJS(itemsList)
}
ko.applyBindings(viewModel);
Now you have viewModel with observableArray listOfItems, and all items in this array have Id and Name properties.
You can test it with folowing code:
<ul data-bind="foreach: listOfItems">
<li>
<span data-bind="text: Id"></span>
<span data-bind="text: Name"></span>
</li>
</ul>

dynamically create two column checkbox list in angular

I have an application that is using angular.js and I'm very new to it. I have a list of checkboxes that gets dynamically created based on a previous selection.
For example, if I have a dropdown of Fruits, the following html will get created:
<input type='checkbox' value="apple">apple</input>
<input type='checkbox' value="banana">banana</input>
<input type='checkbox' value="mango">mango</input>
<input type='checkbox' value="orange">orange</input>
<input type='checkbox' value="pear">pear</input>
<input type='checkbox' value="watermelon">water</input>
However, sometimes the amount of checkboxes that get generated gets more than 20 items, and I want to make use of some unused space.
So I was wondering if it's possible to split a list of checkboxes into two columns instead of one, so that a new column will generate filling up the rest of the checkboxes?
For example: If I have 18 items, instead of one large list of a single column containing 18 checkboxes, the final result will be to have 10 checkboxes in on column, and 8 checkboxes in another column next to it. I want to only have 2 columns as the maximum. Is this possible?
Here is what I have so far, I'm not sure if this is the best way of doing it. Otherwise I'll just make an answer for this question and mark it as such. Logic for splitting the data will be done in code-behind I guess.
example: http://jsfiddle.net/7843b/
Visual representation
X Apple X Pears
X Banana X Watermelon
X Mango
X Orange
The X represents a checkbox.
Seems like these solutions are a bit more complicated than needs to be. Let css handle putting them into columns:
Javascript:
$scope things = ["car", "box", "plant", "dice", "knife", "calendar"];
html:
<div class="checkbox-column" ng-repeat='thing in things'>
<input type="checkbox" /><span>{{thing}}</span>
</div>
in the css display each element with an inline-block and a width around 48%.
.checkbox-column{
display: inline-block;
width:48%;
}
The width of 48% will give it 2 columns. If you want 3 columns, then just use a width of like 30%.
This will also keep the columns aligned when the browser window is adjusted.
Another way is to add column number to each team in the $scope.teams.
http://jsfiddle.net/dkitchen/y5UzD/4/
This splits them into groups of 10...
function TeamListController($scope) {
$scope.teams = [
{ name: "apple", id: 0, isChecked: true, col:1 },
{ name: "banana", id: 1, isChecked: false, col:1 },
{ name: "mango", id: 2, isChecked: true, col:1 },
{ name: "orange", id: 3, isChecked: true, col:1 },
{ name: "pear", id: 4, isChecked: false, col:1 },
{ name: "john", id: 5, isChecked: true, col:1 },
{ name: "paul", id: 6, isChecked: false, col:1 },
{ name: "george", id: 7, isChecked: true, col:1 },
{ name: "ringo", id: 8, isChecked: true, col:1 },
{ name: "roger", id: 9, isChecked: false, col:1 },
{ name: "dave", id: 10, isChecked: true, col:2 },
{ name: "nick", id: 11, isChecked: false, col:2 }
];
}
You can do that at the data source, or you can assign the column number later in the controller.
For example, this bit re-groups them into 8 items per column:
var colCounter = 1;
var colLimit = 8;
angular.forEach($scope.teams, function(team){
if((team.id + 1) % (colLimit + 1) == 0) {
colCounter++;
}
team.col = colCounter;
});
Then in the view, you can filter each repeater by column number:
<div ng-app>
<div ng-controller="TeamListController">
<div class="checkboxList">
<div id="teamCheckboxList">
<div ng-repeat="team in teams | filter: { col: 1 }">
<label>
<input type="checkbox" ng-model="team.isChecked" /> <span>{{team.name }}</span>
</label>
</div>
</div>
</div>
<div>
<div id="teamCheckboxList1">
<div ng-repeat="team in teams | filter: { col: 2 }">
<label>
<input type="checkbox" ng-model="team.isChecked" /> <span>{{team.name}}</span>
</label>
</div>
</div>
</div>
</div>
This is not so surprising question at all. You can dynamically add the items in either js or else you can do the same in html itself. I am here mentioning how to split dynamically based on the number of items.
function TeamListCtrl($scope) {
$scope.teams = [
{ name: "apple", id: 0, isChecked: true },
{ name: "banana", id: 1, isChecked: false },
{ name: "mango", id: 2, isChecked: true },
{ name: "orange", id: 3, isChecked: true },
{ name: "pear", id: 4, isChecked: false },
{ name: "watermelon", id: 5, isChecked: true }
];
column1 = [];
column2 = [];
$.each($scope.teams, function(index){
console.log("index"+index);
if(index%2==0) {
column1.push($scope.teams[index]);
} else{
column2.push($scope.teams[index]);
}
});
$scope.columns.push(column1);
$scope.columns.push(column2);
}
And you can modify your html code as:
<div ng-app>
<div ng-controller="TeamListCtrl" class="checkboxList">
<div id="teamCheckboxList">
<div ng-repeat='column in columns'>
<div class='someClassToArrangeDivsSideBySide' ng-repeat="team in column">
<label>
<input type="checkbox" ng-model="team.isChecked" /> <span>{{team.name}}</span>
</label>
</div>
</div>
</div>
</div>
</div>

Categories