How to make dynamic content-editable table using angular JS? - javascript

Caveat: I've just started with client side scripting and Angular JS is the first thing I'm learning and now I feel I should've started with javascript.
PS: I don't wanna use any third party libraries. I wanna learn to code.
Anyway,I have dynamic table which I want to make editable using content-editable=true attribute of HTML.
Problem: How to I get the edited data? whenever I click on submit and pass the this object to the check() function. I doesn't contain edited values. is there a possible way to pass only edited value if it's dirty. It has pagination so If g to the next page the edited values are gone. I know I've give unique Id to every td element with $Index concatenated to it. But I don't know how should I proceed.
Any help or guidance will be appreciated. Controllers and others are defined in my route.
<div>
<form ng-submit="check(this)">
<table class="table table-striped table-hover">
<tbody>
<tr ng-repeat="data in currentItems">
<td contenteditable="true >{{data.EmpNo}}</td>
<td contenteditable="true">{{data.isActive}}</td>
<td contenteditable="true">{{data.balance}}</td>
<td contenteditable="true">{{data.age}}</td>
<td contenteditable="true">{{data.eyeColor}}</td>
<td contenteditable="true">{{data.fname}}</td>
</tr>
</tbody>
<tfoot>
<td>
<div class="pagination pull-right">
<li ng-class="{'disabled': previousPage}">
<a ng-click="previousPage()" >Previous</a>
</li>
<li ng-repeat="page in pageLengthArray track by $index">
<a ng-click="pagination($index)">{{$index+1}} </a>
</li>
<li disabled="disabled">
<a ng-click="nextPage()" ng-class="{'disabled':nextPage}>Next </a>
</li>
</div>
</td>
</tfoot>
</table>
<input type="submit" value="Submit">
</form>
$scope.currentPage=0;
$scope.pageSize=10;
$scope.currentItems;
$scope.tableData;
$http.get('../json/generated.json').then(function(response){
$scope.tableData=response.data;
$scope.pageLength=Math.ceil($scope.tableData.length/$scope.pageSize);
$scope.currentItems=$scope.tableData.slice($scope.currentPage,$scope.pageSize);
$scope.pageLengthArray= new Array($scope.pageLength);
});
$scope.pagination=function(currentPage){ $scope.currentItems=$scope.tableData.slice($scope.pageSize*currentPage,$scope.pageSize*currentPage+$scope.pageSize);
$scope.currentPage=currentPage;
}
$scope.nextPage=function nextPage(argument) {
$scope.currentPage++; $scope.currentItems=$scope.tableData.slice(($scope.pageSize*$scope.currentPage),($scope.pageSize*($scope.currentPage)+$scope.pageSize));
}
$scope.previousPage=function previousPage(argument) {
$scope.currentPage--;
$scope.currentItems=$scope.tableData.slice(($scope.pageSize*$scope.currentPage),($scope.pageSize*($scope.currentPage)+$scope.pageSize));
}

In the usual case, you can not get a change model for contenteditabe because to change the model used ngModel.
But we can create a directive that we have updated the value of the model.
Live example on jsfiddle.
angular.module('ExampleApp', [])
.controller('ExampleController', function($scope, $timeout) {
$scope.data = {
EmpNo: "123"
};
})
.directive('contenteditable', function($timeout) {
return {
restrict: "A",
priority: 1000,
scope: {
ngModel: "="
},
link: function(scope, element) {
element.html(scope.ngModel);
element.on('focus blur keyup paste input', function() {
scope.ngModel = element.text();
scope.$apply();
return element;
});
}
};
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="ExampleApp">
<div ng-controller="ExampleController">
<table>
<tr>
<td ng-model="data.EmpNo" contenteditable="true"></td>
</tr>
</table>
<pre>{{data|json}}</pre>
</div>
</div>

I would store any object that gets modified in a seperate array using the ng-keyup directive. When the form is submitted, you will have an array of only elements which have been modified. You may have some UX issues if your pagination is done by server as when you change page and come back, it will show your old data, but hopefully this helps.
$scope.check = function () {
// check modifiedItems
console.log(modifiedItems);
};
// store modified objects in a seperate array
var modifiedItems = [];
$scope.modifyItem = function (data) {
// check if data has already been modified and splice it first
for(var i = 0, j = modifiedItems.length; i < j; i++) {
var currentItem = modifiedItems[i];
if (currentItem.id === data.id) {
modifiedItems.splice(i, 1);
break;
}
}
// add to modified
modifiedItems.push(data);
console.log('modifiedItems: ', modifiedItems);
};
HTML
<form ng-submit="check()">
<table class="table table-striped table-hover">
<tbody>
<tr ng-repeat="data in currentItems">
<td ng-repeat="(key, value) in data" contenteditable="true"
ng-keyup="modifyItem(data)">
{{data[key]}}
</td>
</tr>
</tbody>
<tfoot>
</table>
<input type="submit" value="Submit">
</form>

Related

Angular Editable table not working with ng-repeat

<table class="table table-bordered">
<tbody>
<tr ng-repeat="playerOrTeam in template.editableTable track by $index">
<td style="text-align: center;" ng-repeat="playerOrTeamCat in playerOrTeam track by $index">
<input ng-model="playerOrTeamCat" type="text" class="form-control input-sm">
</td>
</tr>
</tbody>
</table>
template.editableTable is a multi dimensional array just filled with standard variables. when I change one of the values in the input box and then i look at the output of the template.editable table, i don't see the changes. Am I missing something obvious?
EDIT with more details because i'm getting no responses =\
//Template Class
app.factory('Template', function () {
var Template = function () {
/*
* Public Variables
*/
this.editableTable = someMultiDimensionalTable;
}
/*
* Functions
*/
Template.prototype.SeeEditableTableContents = function () {
console.log(this.editableTable);
}
}
//Main Controller
app.controller('MainController', function () {
$scope.template = new Template();
//etc
}
You cannot perform direct in-line modifications within ng-repeat. You can update your array entry using a function.
You'd want something like:
$scope.saveEntry = function (idx) {
console.log("Saving entry");
$scope.template.editableTable[idx] = angular.copy($scope.model.selected);
$scope.reset();
};
See JSFiddle for sample.
okay i actually got it to work so that i CAN make direct in-line modifications with ng-repeat by making my multidimensional table full of objects rather than soley a value.
so by doing that, i modified the table to look like this
<table class="table table-bordered">
<tbody>
<tr ng-repeat="playerOrTeam in template.editableTable track by $index">
<td style="text-align: center;" ng-repeat="playerOrTeamCat in playerOrTeam track by $index">
<input ng-model="playerOrTeamCat.value" type="text" class="form-control input-sm">
</td>
</tr>
</tbody>
</table>
got the idea looking here
Modifying objects within Angular Scope inside ng-repeat

directive data sometimes does not appear after broadcast

I'm getting this annoying bug with Angular where I broadcast data to a directive but the directive's $on doesn't receive it.
Therefore, my table doesn't populate at all and looks terrible to users.
test_results.html (contains an instance of the directive):
<div>
<h1>Test Results</h1>
...
<results></results>
</div>
resultsCtrl.js controller:
$timeout(function () {
$rootScope.$broadcast('show-results', test_session.question_objects);
}, 100);
results.html directive template (most fields stripped out):
<div class="results">
<table class="table table-bordered table-striped">
<thead>
<tr>
<td ng-if="average_times && !$root.is_mobile">
<span ng-click="sortType = 'question.average_timing'; sortReverse = !sortReverse">
Avg. Time
</span>
</td>
</tr>
</thead>
<tbody>
<tr ng-repeat="q in questions | orderBy:sortType:sortReverse | filter:searchQuestions track by $index">
<td ng-if="q.average_timing && !$root.is_mobile" ng-click="$root.openDirections('question', q)">{{ q.average_timing }}</td>
</tr>
</tbody>
</table>
</div>
results.js directive:
scope.$on('show-results', function(event, test_session) {
setTestSessionData(test_session);
});
...
function setTestSessionData (test_session) {
scope.questions = test_session;
}
I cannot figure out when exactly this happens. At first I thought it's when I load the site for the first time, but I've tried that since and the data is rendered.
Whenever you do a $rootScope.$broadcast all the child scopes who have registered to those events via $scope.$on will catch the event. Same goes with directive scope also. If you update the model in the event listener , it will be updated in the view.
Working plunker : https://plnkr.co/edit/4iokGsfPH5IqNdecjaGd?p=preview

Angularjs ng-repeat to repeat depending on a variable in the controller

This is a partial view. Ng-repeat repeats over a json file that contains 50 email records.
<table class="table table-hover" ng-controller="emailViewController">
<tbody data-ng-controller="settingsController">
<tr ng-repeat="email in emails" >
<td><input type="checkbox" ng-checked="checkAllEmail" ng-model="selectedEmail"/>
<a href="#">
<span class="glyphicon glyphicon-star-empty"></span>
</a></td>
<td><label ng-bind="email.from"></label></td>
<td><label ng-bind="email.subject"></label></td>
<td><label ng-bind="email.time"></label></td>
</tr>
</tbody>
</table>
settingsController.js
(function() {
'use strict';
var settingController = function (fetchDataService, $scope, savePreferenceService, $localStorage) {
$scope.url = 'app/mock/settings.json';
$scope.save = {};
fetchDataService.getContent($scope.url)
.then(function(response){
$scope.contacts = response.data.contacts;
$scope.languages = response.data.languages;
$scope.conversations = response.data.conversations;
$scope.undoSend = response.data.undoSend;
$scope.save = response.data.userPreferences;
});
$scope.setPreference = function () {
savePreferenceService.setPreferences($scope.save.selectedLang, $scope.save.converse, $scope.save.selectedNumber, $scope.save.selectedNumberContact, $scope.save.reply, $scope.save.signature);
}
$scope.conversation = $localStorage.selectedNumber;
};
angular.module('iisEmail')
.controller ('settingsController',
['fetchDataService', '$scope', 'savePreferenceService', '$localStorage', settingController]);
}());
I am having trouble figuring out how to get ng-repeat to iterate the JSON file depending on the value of $scope.conversation. So, for example, if $scope.conversation is 10, I want ng-repeat to iterate only 10 times. I don't want to display the remaining 40 emails. Does anyone have any ideas on how to achieve this functionality?
UPDATE
With the help of #Prashank's comment, I figured it out. Here is the code using the limitTo filter.
<table class="table table-hover" ng-controller="emailViewController">
<tbody data-ng-controller="settingsController">
<tr ng-repeat="email in emails | limitTo: conversation" >
<td><input type="checkbox" ng-checked="checkAllEmail" ng-model="selectedEmail"/>
<a href="#">
<span class="glyphicon glyphicon-star-empty"></span>
</a></td>
<td><label ng-bind="email.from"></label></td>
<td><label ng-bind="email.subject"></label></td>
<td><label ng-bind="email.time"></label></td>
</tr>
</tbody>
</table>
You can use limitTo or slice and do the following,
here is a sample,
Using limitTo:
<div ng-repeat="item in items | limitTo:needed">
{{item.name}}
</div>
limitTo
Well there are two ways of doing this.
The easier way would be to use $index in your ng-repeat to hide/show the items as follows:
<tr ng-repeat="email in emails" ng-hide="conversation.length() < $index">
//same as in the question
</tr>
The way I would do it(although its a little lengthy) is using a filter as follows:
HTML:
<tr ng-repeat="email in emails | conversationFilter: conversation">
//same as in the question
</tr>
Filter:
app.filter('conversationFilter', function() {
return function(collection, conversation) {
return collection.slice(0, conversation);
}
})

How to dynamically change the content of a table column

Given the following HTML fragment, how can I create the content of the td depending on the column.
<div ng-app="" ng-controller="controller" >
<table>
<tr>
<th ng-repeat="column in columns">
{{ column.header }}
</th>
<tr ng-repeat="row in rows">
<td ng-repeat="column in columns">
<!-- TODO -->
</td>
</tr>
</table>
</div>
Each column can show a different kinds of data. For example, one might just show a string, another might contain a text input field that is bound to a property of the row.
I would like to call a function on the column (column.createCell(row)) that creates that necessary HTML and then put the result in place of <!-- TODO -->.
In WPF, I would just put a ContentPresenter with a DataTemplateSelector, but I don't know what the equivalent is in Angular. I read about something called "ng-bind-html", is that the way to go?
It's not given what kind of custom element you want to build for each column, but for DOM manipulation in AngularJS best practise is to keep it in a directive. Something like this:
in your html:
<body ng-controller="MainCtrl">
<table>
<tr ng-repeat="row in rows">
<td ng-repeat="column in row">
<custom-column="column"></custom-column>
</td>
</tr>
</table>
</body>
app.js
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
// whatever you wanted to define here...
$scope.rows = ...
$scope.columns = ...
});
app.directive('customColumn', function() {
return {
scope: {
obj: '=customColumn',
},
link: function(scope, element, attrs) {
var watcher = scope.$watch('obj', function(obj) {
if (!obj) return;
// build custom element
var html = '<div>'+scope.obj.name+'</div>';
element.html(html);
// delete watch if you only need to draw once
watcher();
});
}
}
});

Ng-init if array not yet created

I'm trying to build a template for a application and want to display a dynamic list with names. so i got this code to show the list and add/remove rows;
<table ng-init="page.businessRows = []">
<thead>
<tr>
<th>Company</th>
<th>Contact</th>
<th>Phone</th>
</tr>
</thead>
<tr ng-repeat="row in page.businessRows">
<td>
<input type="text" ng-model="row.name" />
</td>
<td>
<input type="text" ng-model="row.contact" />
</td>
<td>
<input type="text" ng-model="row.phone" />
</td>
<td>
<button ng-click="page.businessRows.splice($index,1)">
Remove
</button>
</td>
</tr>
</table>
<button class="btn" ng-click="page.businessRows.push({})">addRow</button>
the thing as that when this template is loaded page.busnessRows will most likely be loaded with rows so i want to change the ng-init to only create the empty array if businessRows is not initialised.
I have tried ng-init="page.businessRows = page.businessRows.length < 1 ? [] : page.businessRows but it did not work. How am i inteded to do conditions in jsangular expressions?
All help appreciated. Thanks in advance
You can do this instead:
<table ng-init="page.businessRows = page.businessRows || []">
Update
I look at the parser code of AngularJS and notice that version 1.2 (currently RC) supports ternary expression. So if you use AngularJS 1.2, this will also work (although more verbose than the above code):
<table ng-init="page.businessRows = page.businessRows == null ? [] : page.businessRows">
See demo here.
However, your original code might not work if page.businessRows is null, because the parser will fail to dereference length property of null. So just be careful there.
I don't think the ng-init will evaluate conditional statements properly. But you could refactor the condition into a controller function and call the function from ng-init.
<table ng-init="initializeBusinessRows(page.businessRows)">
The just put your conditional evaluation in the function on the controller scope.
I think you're trying to solve the wrong problem.
The problem is that you're allowing an action to occur before the data is loaded or ready. A secondary problem is you're using an expression in an ng-click where a scope function or controller function should be.
So...
Disable that button if the form isn't ready.
Use your controller to control these interactions.
So here's an example of the controller. The $timeout was added to simulate a delayed load of data into your $scope.page variable.
app.controller('MyCtrl', function($scope, $timeout, $window) {
//Timeout to simulate the asynchronous load
//of the page object on the $scope
$timeout(function(){
$scope.page = {
businessRows: []
};
}, 2000);
//scope method to add a row.
$scope.addRow = function (){
//for safety's sake, check to see if the businessRows array is there.
if($scope.page && angular.isArray($scope.page.businessRows)) {
$scope.page.businessRows.push({});
}
};
//scope method to remove a row
$scope.removeRow = function(index, row) {
if($window.confirm('Are you sure you want to delete this row?')) {
$scope.page.businessRows.splice(index, 1);
}
};
});
... and the HTML view (notice the ng-disabled and the ng-click) (and lack of ng-init):
<div ng-controller="MyCtrl">
<table>
<thead>
<tr>
<th>Company</th>
<th>Contact</th>
<th>Phone</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="row in page.businessRows">
<td>
<input type="text" ng-model="row.name" />
</td>
<td>
<input type="text" ng-model="row.contact" />
</td>
<td>
<input type="text" ng-model="row.phone" />
</td>
<td>
<button ng-click="removeRow($index, row)">
Remove
</button>
</td>
</tr>
</tbody>
</table>
<button class="btn" ng-disabled="!page" ng-click="addRow()">addRow</button>
</div>
Also, here's the obligatory Plunker for you to see this in action.

Categories