I need to show a details view based on a selected row in a table. I would like to show which row is currently selected in the table. Is it possible to do this with the 'style' binding?
I've created a JSFidle with some code illustrating the idea...or lack of, since it currently all rows change color on row click. Here's the code:
<table>
<tr>
<th>Name</th>
<th>Sales</th>
<th>Price</th>
</tr>
<tbody data-bind='template: { name: "fieldTemplate", foreach: viewModel.items}'></tbody>
</table>
<script type="text/html" id="fieldTemplate">
<tr >
<td> ${name}</td>
<td>${sales}</td>
<td>${price}</td>
</tr>
</script>
And this is the Javascript:
var viewModel = {
items: ko.observableArray([
{ name: "Well-Travelled Kitten", sales: 352, price: 75.95 },
{ name: "Speedy Coyote", sales: 89, price: 190.00 },
{ name: "Furious Lizard", sales: 152, price: 25.00 },
{ name: "Indifferent Monkey", sales: 1, price: 99.95 },
{ name: "Brooding Dragon", sales: 0, price: 6350 },
{ name: "Ingenious Tadpole", sales: 39450, price: 0.35 },
{ name: "Optimistic Snail", sales: 420, price: 1.50 }
])
};
So I think I need a reference to the current row, or adding a style attribute to my item and then bind to this, and then change in the click event. Any ideas?
You'll need to bind a click event to each row of the table. Once a row is clicked. then in the event handler you can change the color of the selected row + you can show the new details
something like this > http://jsfiddle.net/wrzFx/11/
I would bind a selection event on a table row via jQuery live events. Inside the listener I would change the value of selectedRow attribute in your viewModel
That might not sound a Knockout way to do things but as long as it works I'm ok with it.
BTW, I can't get jQuery templates running inside jsFiddle for some reason.
Related
I'm having trouble with displaying my json data correctly. I want to get each products and place them into it's own row.
The problem now is that it places all the data of for example the value "name" into one table row instead of multiple rows.
This is my json data
{
id: "FVFkkD7s8xNdDgh3zAyd",
name: "AlperKaffe",
products: [
{
id: "0cfBnXTijpJRu14DVfbI",
name: "Første Kaffe",
price: "1",
size: "small",
quantity: "20 ml"
},
{
id: "JQadhkpn0AJd0NRnnWUF",
name: "Anden Kaffe",
price: "2",
size: "Medium",
quantity: "25 ml"
},
{
id: "UwHHdH8bFxbVHkDryeGC",
name: "kaffeeen",
price: "300",
size: "Small",
quantity: "23 ml"
},
{
id: "WiX5h0wFMNkCux9cINYq",
name: "kaffe modal",
price: "230",
size: "Medium",
quantity: "39 ml"
},
this is my Js file which gets the json data. As you can see i'm only working with the "name" value for now
// Jquery getting our json order data from API
$.get("http://localhost:8888/products", (data) => {
let rows = data.map(item => {
let $clone = $('#frontpage_new_ordertable tfoot tr').clone();
let productsName = item.products.map(prod => `${prod.name}`);
$clone.find('.name').html(productsName);
return $clone;
});
// appends to our frontpage html
$("#frontpage_new_ordertable tbody").append(rows);
});
this is my html file
<body>
<table id="frontpage_new_ordertable">
<tbody>
<tr>
<th>Name</th>
<th>Price</th>
<th>Size</th>
<th>Quantity</th>
<th></th>
</tr>
</tbody>
<tfoot>
<tr>
<td class="name"></td>
<td class="price"></td>
<td class="size"></td>
<td class="quantity"></td>
<td class="buttons"></td>
</tr>
</tfoot>
</table>
<script src="./itemPage.js"></script>
</body>
you must replace
let rows = data.map(item => {
to
let rows = data.products.map(item => {
and
let productsName = item.products.map(prod => `${prod.name}`);
to
let productsName = item.name;
https://jsfiddle.net/ab7t1vmf/3/
The issue in your JS snippet seems to be let rows = data.map(item => { ...
From what you describe, the JSON data is comprised of 3 keys:
id
name
products
You cannot execute a map function directly on a Javascript Object, this function requires an array.
Looking a bit more at your code, I understand you need to display each product on its own row. Thus, you would need to use the map function on data.products which contains an array (of products).
let rows = data.products.map(item => {
// ...
$clone.find('.name').html(item.name);
// ...
// ...
});
Reference: https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Objets_globaux/Array/map
I've start a little proof of concept this week with Angular Material, and in this POC, I have a table that displays nested data:
<table>
<thead>
<tr>
<th colspan="2">Employee Name</th>
<th>Ovr</th>
<th> </th>
<tr>
<thead>
<tbody ng-repeat="employee in evaluation.employees" >
<tr ng-class-odd="'odd-row'">
<td class="photo"><img src="{{employee.photo}}" /></td>
<td class="name"><span class="firstname">{{employee.firstName}}</span><br/><span class="lastname">{{employee.lastName}}</span></td>
<td class="column-align-center"><span>{{employee.grade}}</span></td>
<td class="column-align-center"><md-button ng-click="toggleAptitudes(employee.id)" class="md-raised md-primary custom-button">+</md-button></td>
</tr>
<tr ng-repeat="skill in employee.skills" ng-show="employee.displayAptitudes">
<td colspan="4" style="padding: 0px 20px 0px 20px;">
<md-slider-container>
<span>{{skill.name}}</span>
<md-slider class="md-primary" flex min="0" max="100" ng-model="skill.value" ng-change="calculateAptitudesGrade(employee.id)" aria-label="skill.name" id="red-slider">
</md-slider>
<md-input-container>
<input flex type="number" ng-model="skill.value" aria-label="skill.title" aria-controls="red-slider">
</md-input-container>
</md-slider-container>
</td>
</tr>
</tbody>
</table>
Snippet from the Controller:
var self = this;
// Mock data...
self.employees = [
{ id: 1, firstName: 'FirstName1', lastName: 'LastName1', photo: 'img/photo1.png', grade: 0, aptitudes: [...], displayAptitudes: false },
{ id: 2, firstName: 'FirstName2', lastName: 'LastName2', photo: 'img/photo2.png', grade: 0, aptitudes: [...], displayAptitudes: false }
];
$scope.calculateAptitudesGrade = function(employeeId) {
// The overall calculation happen here where I collect all the skills values for the employee.
...
};
It's working fine for the first row I modify. I click the toggle button, it shows a list of skills with sliders, I move the slider and the overall calculation works very well.
THE PROBLEM: whenever I choose another employee, the sliders are set visually with the previous values. How to have the sliders set to 0 for each employee?
For your ng-click on the button change it from toggleAptitudes(employee.id) to employee.displayAptitudes = !employee.displayAptitudes
Ok ok ok, I've found the problem!!
Like I says, it is a POC and I did this quick. The problem come from the «Aptitudes» array that I have defined... and reused for every employee defined in the array...
self.aptitudes= [
{ id: 1, title: 'Aptitude 1', value: 0 },
{ id: 2, title: 'Aptitude 2', value: 0 },
{ id: 3, title: 'Aptitude 3', value: 0 }
];
// Mock data...
self.employees = [
{ id: 1, firstName: 'FirstName1', lastName: 'LastName1', photo: 'img/photo1.png', grade: 0, aptitudes: self.aptitudes, displayAptitudes: false },
{ id: 2, firstName: 'FirstName2', lastName: 'LastName2', photo: 'img/photo2.png', grade: 0, aptitudes: self.aptitudes, displayAptitudes: false }
];
Instead of declaring an array, I have create a function that return the array:
function getAptitudes() {
return [
{ id: 1, title: 'Aptitude 1', value: 0 },
{ id: 2, title: 'Aptitude 2', value: 0 },
{ id: 3, title: 'Aptitude 3', value: 0 }
];
}
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 have a table with some data to view in html. when i do click print, i need to get all the data from db and print it. I am getting the data and populating the model data when i click on print, only the model is updated and print shows the old data. In the code below, newitems is not added to items when i click on print.
http://jsfiddle.net/vijaivp/Y3BJa/306/
HTML
<div ng-app>
<div class="hidden-print" ng-controller="PrintCtrl">
<br />
<div id="overallPrint" class='visible-print' style="float:left; margin-right:50px;">
<h4>Overall Report</h4>
<table border="1">
<thead>
<tr>
<td>Name</td>
<td>Price</td>
<td>Quantity</td>
</tr>
</thead>
<tbody>
<tr ng-repeat="item in items">
<td>{{item.Name}}</td>
<td>{{item.Price}}</td>
<td>{{item.Quantity}}</td>
</tr>
</tbody>
</table>
<br>
<input type="button" value="Print Overall" ng-click='printOverallReport()' />
</div>
</div>
</div>
JS
function PrintCtrl($scope, $window, $q) {
$scope.items = [
{Name: "Soap", Price: "25", Quantity: "10"},
{Name: "Shaving cream", Price: "50", Quantity: "15"}
];
$scope.newitems = [
{Name: "Shampoo", Price: "100", Quantity: "5"}
];
$scope.printOverallReport = function () {
$scope.items.push($scope.newitems[0]);
$window.print();
};
}
Using a timeout with Angular's $timeout service will fix it:
function PrintCtrl($scope, $window, $q, $timeout) {
$scope.items = [
{Name: "Soap", Price: "25", Quantity: "10"},
{Name: "Shaving cream", Price: "50", Quantity: "15"}
];
$scope.newitems = [
{Name: "Shampoo", Price: "100", Quantity: "5"}
];
$scope.printOverallReport = function () {
$scope.items = $scope.newitems;
console.log($scope.items.length);
$timeout($window.print, 0);
console.log($scope.items.length);
};
}
Fiddle
For a comprehensive explanation as to why, please see DVK's answer (2nd one) here: Why is setTimeout(fn, 0) sometimes useful?
TL:DR;
When you call $window.print() the old HTML is still present since the browser hasn't rendered it yet. It's waiting to finish the javascript function run. setting a $timeout 0 will queue the print at the end of execution queue and will guarantee it happens after the HTML has been rendered. (I still strongly recommend to read his answer)
http://jsfiddle.net/WcJbu/
When I select a person, I want the favoriteThing selector to display their current selection.
<div ng-controller='MyController'>
<select ng-model='data.selectedPerson' ng-options='person.name for person in data.people'></select>
<span ...> likes </span>
<select ... ng-model='data.favoriteThing' ng-options='thing.name for thing in data.things'></select>
</div>
$scope.data.people = [{
name: 'Tom',
id: 1,
favorite_thing_id: 1
}, {
name: 'Jill',
id: 2,
favorite_thing_id: 3
}];
$scope.data.things = [{
name: 'Snails',
id: 1
}, {
name: 'Puppies',
id: 2
}, {
name: 'Flowers',
id: 3
}];
Do I need to set up a service and add watches, or is there a [good] way to use the favorite_thing_id directly in the select?
Change the second select to this:
<select ng-show='data.selectedPerson' ng-model='data.selectedPerson.favorite_thing_id'
ng-options='thing.id as thing.name for thing in data.things'></select>
Adding the thing.id as to the ng-options will allow you to select the data.things entries based on their id's instead of their references. Changing the ng-model to data.selectedPerson.favorite_thing_id will make angular automatically change to the correct option based on selectedPerson.favorite_thing_id.
jsfiddle: http://jsfiddle.net/bmleite/4Qf63/
http://jsfiddle.net/4Qf63/2/ does what I want - but it's pretty unsatisfying.
$scope.$watch(function() {
return $scope.data.selectedPerson;
}, function(newValue) {
if (newValue) {
$scope.data.thing = $filter('filter')($scope.data.things, {id: newValue.favorite_thing_id})[0];
}
})
I'd like to see all of that be possible from within the select statement.
Maybe I'll try to write a directive.
association = {key: matchValue}
So that I can do
<select ... ng-model='data.thing' ng-options='t.name for t in data.things' association='{id: "data.selectedPerson.favorite_thing_id"}'></select>