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);
}
}]);