How to reinitialize Angular model with nested data? - javascript

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

Related

How to use v-for to dynamically display data table with nested loops

I have a table component called from my entry file. The Table component gets the documents object passed on as prop. Inside the table component I have a table which should render the object data one under another in order the data comes in. Each row should have the User name, Document name, and date of view. (Please see bottom of the question for current & expected result). I am facing the issue because the object is structured in a way that the views are nested inside the parent, and I don't know how to loop over the outer object to get into the inner one without affecting the layout (Hence the use of <span>)
Object:
data() {
return {
documents: [
{
id: 1,
name: "Doc 1",
date: "01.01.1995",
doc_website: "linktodocument1.com",
views: [
{
id: 4,
user_name: "Jon",
doc_id: 1,
view_date: "01.02.1996",
},
{
id: 1,
user_name: "Bob",
doc_id: 1,
view_date: "05.02.1996",
},
],
},
{
id: 2,
name: "Doc 2",
date: "01.01.2000",
doc_website: "linktodocument2.com",
views: [
{
id: 1,
user_name: "Bob",
doc_id: 2,
view_date: "01.02.1996",
},
{
id: 1,
user_name: "Jon",
doc_id: 2,
view_date: "05.02.1996",
},
],
},
{
id: 3,
name: "Doc 3",
date: "01.01.2000",
doc_website: "linktodocument3.com",
views: [
{
id: 1,
user_name: "Bob",
doc_id: 3,
view_date: "01.02.1996",
},
{
id: 1,
user_name: "Jon",
doc_id: 3,
view_date: "05.02.1996",
},
{
id: 3,
user_name: "Rob",
doc_id: 3,
view_date: "05.02.1996",
},
],
},
],
};
},
Entry File:
<template>
<table>
<thead>
<tr>
<th>Name</th>
<th>Document</th>
<th>Date</th>
</tr>
</thead>
<table-component :docObject="documents"></table-component>
</table>
</template>
<script>
import tableComponent from "./table.vue";
export default {
components: { tableComponent },
}
</script>
Component:
<template>
<tbody>
<span v-for="doc in docObject" :key="doc.name">
<td v-for="view in doc.views" :key="view.id">
<p>
{{ view.user_name }}
</p>
</td>
<td>
<p>
{{ doc.name }}
</p>
</td>
<td v-for="view in doc.views" :key="view.name">
<p>
{{ view.view_date }}
</p>
</td>
</span>
</tbody>
</template>
<script>
export default {
components: {},
props: ["docObject"],
};
</script>
<style scoped>
td {
padding: 15px 15px;
border: 1px solid black;
}
</style>
Current Output:
Name Document Date
Jon Bob Doc 1 1.02.1996 05.02.1996
Bob Jon Doc 2 01.02.1996 05.02.1996
Bob Jon Rob Doc 3 01.02.1996 05.02.1996 05.02.1996
Expected Result:
Name Document Date
Jon Doc 1 1.02.1996
Bob Doc 1 05.02.1996
Bob Doc 2 01.02.1996
Bob Doc 2 05.02.1996
Bob Doc 3 01.02.1996
Jon Doc 3 05.02.1996
Rob Doc 3 05.02.1996
Codesandbox link: https://codesandbox.io/s/zealous-cerf-rfmf2?file=/src/components/table.vue:0-585
You should use template instead of span and there is no need to complicate it further with multiple td tags, it can be simple as this:
<template>
<tbody>
<template v-for="doc in docObject">
<tr v-for="view in doc.views" :key="view.id">
<td>{{ view.user_name }}</td>
<td>{{ doc.name }}</td>
<td>{{ view.view_date }}</td>
</tr>
</template>
</tbody>
</template>
<script>
export default {
components: {},
props: ["docObject"],
};
</script>
<style scoped>
td {
padding: 15px 15px;
border: 1px solid black;
}
</style>
Please check this codesanbox: https://codesandbox.io/s/nice-ives-b33u5?file=/src/components/table.vue
Output:

Rivets JS data-show not working

I am trying to do a simple show/hide using Rivets JS (previously using Aurelia). I thought i had the right code to do the job after looking through various sites however it doesnt seem to be working.
my HTML:
<div id="Payments">
<h1 rv-text="data.title"></h1>
<div class="col-md-4">
<select rv-value="data.selectedVal" class="form-control">
<option value="0" selected></option>
<option rv-each-country="data.countries" rv-value="country.id" rv-text="country.name"></option>
</select>
</div>
<div class="container" style="padding-top:40px;">
<div class="row">
<table class="table .table-responsive table-hover">
<tbody>
<tr rv-each-product="data.products" data-show="data.selectedVal | eq product.id" >
<td rv-text="product.title" ></td>
<td rv-text="product.id"></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
my js:
var data = {
title: 'Welcome to Payments',
products: [
{
title: 'MasterCard',
id: 'CH'
},
{
title: 'Visa',
id: 'UK'
},
{
title: 'PayPal',
id: 'US'
},
{
title: 'Cheque',
id: 'UK'
},
{
title: 'Cash',
id: 'US'
}],
countries: [
{
name: 'China',
id: 1
},
{
name: 'USA',
id: 'US'
},
{
name: 'UK',
id: 'UK'
}
],
selectedVal: ''
};
rivets.formatters.eq = function (value, args) {
debugger;
return value === args;
};
rivets.bind(
document.querySelector('#Payments'), // bind to the element with id "candy- shop"
{
data: data // add the data object so we can reference it in our template
});
The List of payment should show or hide based on the selected country.
Any help would be great!
Thanks
Needed to use rv-show and not data-show. Must have read the wrong documentation.

Angular select all checkboxes from outside ng-repeat

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

Filter Data in nested json

My angular controller is following -
how to filter nested data like first row of json-
<script>
var myApp = angular.module('myApp', ['angular.filter']);
function VersionFilterCtrl($scope) {
$scope.limit=6;
$scope.orders = [
{ id:1, customer:[{ name: 'John1', id: 10 },{ name: 'John2', id: 100 }] },
{ id:2, customer: { name: 'William', id: 20 } },
{ id:3, customer: { name: 'John', id: 10 } },
{ id:4, customer: { name: 'William', id: 20 } },
{ id:5, customer: { name: 'Clive', id: 30 } }
];
}
</script>
and my html
<div ng-app="myApp">
<div ng-controller="VersionFilterCtrl">
<input type='text' name='name' ng-model='search.customer.name'>
<input type='text' name='name' ng-model='search.customer.id'>
<li ng-repeat="order in orders| filterBy: ['customer.name']: search.customer.name| filterBy: ['customer.id']: search.customer.id">
{{order.id}}{{order.customer.name}}
</li>
</div>
</div>
JSFIDDLE EXAMPLE
You approached this correctly in how you set up your search models to match the structure of the data you are filtering, i.e. search.customer.name and search.customer.id.
All you need to do is to apply the search object as the filter:
<li ng-repeat="order in orders | filter: search">
{{order.id}} | id: {{order.customer.id}} name: {{order.customer.name}}
</li>
plunker
A few other issues:
There is no filterBy (unless you create your own)
Your first order record has an array of customers - was this intentional? This plunker handles that case

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