Angular-DataTables custom filter - angularjs

I am trying to add a custom filter to angular-DataTables with server side processing, which works perfectly with sorting and built in search of datatables.
I was following example Angular-DataTables, to build the server side processing and setup the DataTable, in searching around i have found some info but haven't been able to make it work.
What i am trying to get is to redraw the table with filtered data once the checkbox [Player] has been triggered.
Does anyone know a solution for this or has a working example for this?
have found this example Custom Table Filter, but it seems it doesn't work either.
HTML:
<div ng-app="showcase"><div ng-controller="ServerSideProcessingCtrl">
<label><input type="checkbox" id="customFilter" value="player"> Player</label>
<table datatable="" dt-options="dtOptions" dt-columns="dtColumns" class="row-border hover"></table>
JS part:
'use strict';
angular.module('showcase', ['datatables'])
//.controller('ServerSideProcessingCtrl', ServerSideProcessingCtrl);
.controller('ServerSideProcessingCtrl',["$scope", "DTOptionsBuilder", "DTColumnBuilder", function($scope, DTOptionsBuilder, DTColumnBuilder) {
//function ServerSideProcessingCtrl(DTOptionsBuilder, DTColumnBuilder) {
console.log($scope);
$scope.dtOptions = DTOptionsBuilder.newOptions()
.withOption('ajax', {
// Either you specify the AjaxDataProp here
// dataSrc: 'data',
url: 'getTableData.php',
type: 'POST'
})
// or here
.withDataProp('data')
.withOption('serverSide', true)
.withPaginationType('full_numbers');
$scope.dtColumns = [
DTColumnBuilder.newColumn('id').withTitle('ID'),
DTColumnBuilder.newColumn('name').withTitle('First name'),
DTColumnBuilder.newColumn('position').withTitle('Position'),
DTColumnBuilder.newColumn('type').withTitle('Type')
];
$scope.$on('event:dataTableLoaded', function(event, loadedDT) {
console.log(event);
console.log(loadedDT);
$('#customFilter').on('change', function() {
loadedDT.DataTable.draw();
} );
});
}]);
JSON on load:
{"draw":"1","recordsTotal":8,"recordsFiltered":8,"data":[{"id":"1","name":"Raul","position":"front","type":"player"},{"id":"2","name":"Crespo","position":"front","type":"player"},{"id":"3","name":"Nesta","position":"back","type":"player"},{"id":"4","name":"Costacurta","position":"back","type":"player"},{"id":"5","name":"Doc Brown","position":"staff","type":"medic"},{"id":"6","name":"Jose","position":"staff","type":"manager"},{"id":"7","name":"Ferguson","position":"staff","type":"manager"},{"id":"8","name":"Zinedine","position":"staff","type":"director"}]}

After searching and browsing, combined few examples and came up with this.
HTML :
<label><input type="checkbox" id="customFilter" value="player" ng-click="reload()" > Player</label>
JS:
'use strict';
angular.module('showcase', ['datatables'])
//.controller('ServerSideProcessingCtrl', ServerSideProcessingCtrl);
.controller('ServerSideProcessingCtrl',["$scope", "DTOptionsBuilder", "DTColumnBuilder","DTInstances", function ($scope, DTOptionsBuilder, DTColumnBuilder, DTInstances) {
//function ServerSideProcessingCtrl(DTOptionsBuilder, DTColumnBuilder) {
console.log($scope);
$scope.dtOptions = DTOptionsBuilder.newOptions()
.withOption('ajax', {
// Either you specify the AjaxDataProp here
// dataSrc: 'data',
url: 'getTableData.php',
type: 'POST',
// CUSTOM FILTERS
data: function (data) {
data.customFilter = $('#customFilter').is(':checked');
}
})
// or here
.withDataProp('data')
.withOption('serverSide', true)
.withPaginationType('full_numbers');
$scope.dtColumns = [
DTColumnBuilder.newColumn('id').withTitle('ID'),
DTColumnBuilder.newColumn('name').withTitle('First name'),
DTColumnBuilder.newColumn('position').withTitle('Position'),
DTColumnBuilder.newColumn('type').withTitle('Type')
];
DTInstances.getLast().then(function (dtInstance) {
$scope.dtInstance = dtInstance;
});
$scope.reload = function(event, loadedDT) {
$scope.dtInstance.reloadData();
};
}]);
and on the backend just go through the $_POST and check for custom filter, hopefully this will help someone

You can use withFnServerData with fromSource functions instead of
withOption:
This API allows you to override the default function to retrieve the data (which is $.getJSON according to DataTables documentation) to something more suitable for you application.
It's mainly used for Datatables v1.9.4. See DataTable documentation.
$scope.dtOptions = DTOptionsBuilder.fromSource('data.json')
.withFnServerData(serverData);
function serverData (sSource, aoData, fnCallback, oSettings) {
oSettings.jqXHR = $.ajax({
'dataType': 'json',
'type': 'POST',
'url': sSource,
'data': aoData,
'success': fnCallback
});
:)

Ok sorry its not a full blown example. This only works with angular and datatables, if you do a filter on the ng-repeat eg | aFilter:this The this transfers the scope. The filtering applied can now be quite complex. Within the ng-controller <div> you can have an html partial containing drop downs or input texts, all having an ng-model value.
When these change they kick off the filter routineaFilter an angular.filter('aFilter'.... js routine. The records are piped through the afilter routine allowing the ones wanted to be pushed onto an array and this is what is returned with the return. It doesn't work with breeze, yet. Be aware it is unlikely to be server side. To deal with server side maybe an SQL call in the service....another day.
eg in the ng-table id="test" :
<tr ng-repeat="edRec in aSetOfJSonRecords | aFilter:this | orderBy:'summat'">
{{edRec.enCode}} etc
</tr>
in the aFilter, the fltEnCode represents the ng-model values, the test variable allows freedom from nulls causing issues upon comparison, good idea to test for undefined first:
app.filter('aFilter', [function () {
return function (items, $scope) {
var countItems = 0;
var filtered = [];
var isOK = 0;
angular.forEach(items, function (item) {
isOK = 1;
// some conditions
if ($scope.fltEnCode !== "") {
if (item.enCode === null) { test = ""; } else { test = item.enCode; }
if (test.indexOf($scope.fltEnCode) < 0) isOK = 0;
}
// end of conditions
if (isOK > 0) {
filtered.push(item);
countItems++;
}
});
// alert(countItems);
return filtered;
};
}]);
Hope its of some use. I've avoided boolean variables as they have given grief before. Odd occasions have needed an ng-change in the html items pointing to an angular function resetting the data by calling the getTheItemsForTest() in the controller. This redraws the list. Having
$scope.dtOptions = {
stateSave: false, .......
in your controller, keeps the sorting columns correct.
$(document).ready(function() {
var table = $('#test').DataTable();
table.draw();
};
might also be useful if its recalcitrant. I need to know how to make it work for breeze??? Enjoy..

here is what I really missed after I searched alot
bower install datatables-light-columnfilter

Related

AngularJS and DataTables refreshing data

My project outputs results to a DataTable from an AngularJS controller function, but I'm running into some strangeness when I try to modify my search params. The first rendering of the table works as expected. But when I select different options and run the search again, extra rows appear in the table, but the info section shows the previous search's row count, and changing the number of rows shown via the length menu causes the new rows to disappear. Here's my table declaration, using attributes to wire up DataTables:
<table ui-jq="dataTable" ui-options="dataTableOptions" id="search-results" class="display nowrap datatable cell-borders" style="width: 100%;">
And this is my AngularJS controller code:
$scope.dataTableOptions = {
dom: "lfBrtip",
lengthMenu: [[25, 50, -1], [25, 50, "All"]],
language: {
emptyTable: 'No items matched your search criteria'
},
buttons: [
{
text: 'Export',
className: 'button button:hover',
extend: 'csv'
}
]
};
$scope.getItemInfo = function (model) {
$http({
method: 'POST',
url: $scope.getUrl('/My/ServerSide/Url'),
data: { model: $scope.model }
}).then(function successCallBack(response) {
$scope.model.SearchResults = response.data;
}, function errorCallback(response) {
alert("There was an error gathering the entity information. Please try again");
});
};
I'm not sure why submitting new queries with different params doesn't simply update the data in the DataTables table. Any suggestions?
I ended up using a bit of an ugly hack to get this to work. Even DataTables author wasn't sure how to get around the issue of using AngularJS with DataTables, so I had to force a reinitialization every time the form posted. I persisted the search params to localStorage, and called location.reload(). Then when the page loads and the AngularJS init() function runs, I pick up the search params and call the search function from inside an Angular document ready function, like this:
$scope.init = function () {
$scope.ValidationErrors = [];
$scope.model = {};
$scope.model.SearchResults = [];
$scope.model.ItemNumber = localStorage.getItem("itemNumber");
$scope.model.StartDate = localStorage.getItem("startDate");
$scope.model.EndDate = localStorage.getItem("endDate");
angular.element(document).ready(function () {
if ($scope.model.ItemNumber) {
$scope.getItemRecords();
}
});
localStorage.clear();
};
And then of course I clear the localStorage after the query. Not terribly elegant, but it'll have to do for now.

Passing the URL/route parameter from laravel and using in angular

I am new to Laravel 5 and angular.
I am using Laravel routing for traversal and backend operations and angular for just UI operations like fetching data and binding UI grid, etc.
I have following route defined in routes.php file below
routes.php
Route::pattern('clientid', '[0-9]+');
//used for AJAX call from angularjs and populating ui-grid
Route::get('getclients/{clientid?}', 'ClientController#getClients');
//used for displaying Laravel view with ui-grid
Route::get('client/{clientid?}', 'ClientController#showClients');
Please find the angular files:
app.js
var appClients = angular.module('getclients', ['clientsService', 'ui.grid', 'ui.grid.exporter', 'ui.grid.selection']);
clientController.js
appClients.controller('ClientsController', ['$scope', '$http', 'Client', '$interval', '$q', function ($scope, $http, Client, $interval, $q) {
/* Defining UI grid options*/
.
.
/* Calling service to fill the grid*/
Client.get(clientid)
.success(function (data, status, headers, config) {
if (data.length > 0) {
$scope.gridOptions.data = data;
}
});
}
clientsService.js
angular.module('clientsService', [])
.service('Client', function ($http) {
return {
// Get all the photos
get: function (clientid) {
if (clientid !== '') {
return $http.get('/myproject/public/getclients/' + clientid);
}
else {
return $http.get('/myproject/public/getclients/');
}
}
}
});
/*
**Note:**
Have already defined route in routes.php for using the same above:
Route::get('getclients/{clientid?}', 'ClientController#getClients');
*/
EXAMPLE:
Step 1:
Say I am hitting URL: http://<domain>/public/myproject/client/2
The following route would catch it and redirect to view where the ui-grid is present
Route::get('client/{clientid?}', 'ClientController#showClients');
Step 2:
Now, somehow need to figure out how to pass that **2** to angular so that I could pass that parameter while making ajax call and get grid data
I am confused as to how we could use the the url parameter from Laravel in angular?
I reckon that I am missing some concept or doing something wrong here.
Could anyone help me out?
Just a workaround to make it work with angular and without jquery.
From routes.php, the control is transferred to showClients action in ClientsController.php
ClientsController.php (Laravel Controller):
Passed the variable to Laravel view from controller using following statement:
public function showClients($clientid = '') {
return view('masters.clients', compact('clientid'));
}
Clients.php (Laravel View)
Added clientidmodel as ng-model and initialized it with passed clientid from Laravel controller using ng-init
<div ng-app="">
<div ng-controller="ClientsController">
<input type="text" name="txtClientId" ng-model="clientidmodel" style="display: none;" ng-init="clientidmodel = '{!!$clientid!!}'"/>
</div>
</div>
clientController.js
Added the watch to the angular model so that we can capture the initial value passed.
$scope.$watch("clientidmodel", function () {
Client.get($scope.clientidmodel)
.success(function (data, status, headers, config) {
if (data.length > 0) {
$scope.gridOptions.data = data;
}
});
});
Not sure whether this is the efficient way but as of now got the things working with this workaround.
Please let me know in case of any better way to approach the same.
You can achieve this in jquery by
var pathname = window.location.href;
var lastItem = pathname.split("/").pop(-1);
Note : Here you will get the last element
i.e.,
If your url is like yourapp.com/app#/product/15 then the script will return 15. That's the last element after / . You can change this according to your wish.
Then you can pass the value directly inside your Laravel Controller.
$.ajax({
type: "POST",
dataType: 'text',
crossOrigin : true,
data: {param : lastItem},
url: serviceUrl+'/getReceipeDetails',
})
.done(function( data ) {
var result = jQuery.parseJSON(data);
if(result.success==1)
{
$(".yourresult").html('Controller return success');
}
else
{
$(".yourresult").html('Controller return failure');
}
})
.fail( function(xhr, textStatus, errorThrown) {
console.log(errorThrown);
});

Is it possible to use parameterized URL templates with angular $http service

I'm using $resource for my RESTful api's and love the parameterized URL template for example 'api/clients/:clientId'
This works great for CRUD operations. Some of my api's however are just reports or read-only end points without the need for the full RESTful treatment. I felt it was overkill to use $resource for those and instead used a custom data service with $http.
The only drawback is I lose the parameterized URL templates. I would love to define a url like'api/clients/:clientId/orders/:orderId' and just pass { clientId: 1, orderId: 1 }. I realize I can build the url dynamically but was hoping $http supported the parameterized template and I just haven't found it yet.
All the best
UPDATE 7/5
The word I was missing in my searches is 'Interpolate'. More information comes up when I search for 'url interpolation in angular $http'. The short answer looks to be 'No' $http doesn't support url interpolation. There are a few fairly easy ways to accomplish this however.
1. Use $interpolate:
Documentation for $interpolate here
var exp = $interpolate('/api/clients/{{clientId}}/jobs/{{jobId}}', false, null, true);
var url = exp({ clientId: 1, jobId: 1 });
2. Write your own url interpolation function
Ben Nadel has a great post on this exact topic here.
3. Steal the functionality right out of angular-resource
Check out setUrlParams on Route.prototype in angular-resource.js. It is fairly straightforward.
Sample data service using $interpolate
(function () {
'use strict';
var serviceId = 'dataservice.jobsReports';
angular.module('app').factory(serviceId, ['$http', '$interpolate', function ($http, $interpolate) {
var _urlBase = 'http://localhost:59380/api';
var _endPoints = {
getJobsByClient: {
url: 'Clients/{{clientId}}/Jobs',
useUrlInterpolation: true,
interpolateFunc: null
}
};
// Create the interpolate functions when service is instantiated
angular.forEach(_endPoints, function (value, key) {
if (value.useUrlInterpolation) {
value.interpolateFunc = $interpolate(_urlBase + '/' + value.url, false, null, true);
}
});
return {
getJobsByClient: function (clientId) {
var url = _endPoints.getJobsByClient.interpolateFunc({ clientId: clientId });
return $http.get(url);
}
};
}]);
})();
To prevent this being "unanswered" when it has been answered ...
1. Use $interpolate:
Documentation for $interpolate here
var exp = $interpolate('/api/clients/{{clientId}}/jobs/{{jobId}}', false, null, true);
var url = exp({ clientId: 1, jobId: 1 });
2. Write your own url interpolation function
Ben Nadel has a great post on this exact topic here.
3. Steal the functionality right out of angular-resource
Check out setUrlParams on Route.prototype in angular-resource.js. It is fairly straightforward.
Sample data service using $interpolate
(function () {
'use strict';
var serviceId = 'dataservice.jobsReports';
angular.module('app').factory(serviceId, ['$http', '$interpolate', function ($http, $interpolate) {
var _urlBase = 'http://localhost:59380/api';
var _endPoints = {
getJobsByClient: {
url: 'Clients/{{clientId}}/Jobs',
useUrlInterpolation: true,
interpolateFunc: null
}
};
// Create the interpolate functions when service is instantiated
angular.forEach(_endPoints, function (value, key) {
if (value.useUrlInterpolation) {
value.interpolateFunc = $interpolate(_urlBase + '/' + value.url, false, null, true);
}
});
return {
getJobsByClient: function (clientId) {
var url = _endPoints.getJobsByClient.interpolateFunc({ clientId: clientId });
return $http.get(url);
}
};
}]);
})();
For URL templateing, there is a clearly defined recommandation: RFC 6570
You can find one implementation on Github : bramstein/url-template
It is quite simple. Here is an AngularJS service making use of a library implementing RFC 6570 standard:
var app=angular.module('demo',[]);
app.service('UserStore',function () {
var baseUrl=urltemplate.parse('/rest/v1/users{/_id}');
return {
load:function(id){
return $http.get(baseUrl.expand({_id:id}));
},
save:function (profile) {
return baseUrl.expand(profile);
//return $http.post(baseUrl.expand(profile),profile);
},
list:function (id) {
}
}
});
app.controller('demoCtrl',function(UserStore){
this.postUrlOfNewUser=UserStore.save({name:"jhon"});
this.postUrlOfExistingUser=UserStore.save({_id:42,name:"Arthur",accessory:"towel"});
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<script src="https://cdn.rawgit.com/bramstein/url-template/master/lib/url-template.js"></script>
<div ng-app="demo">
<div ng-controller="demoCtrl as ctrl">
<div>New user POST URL: {{ctrl.postUrlOfNewUser}}</div>
<div>Existing user POST URL: {{ctrl.postUrlOfExistingUser}}</div>
</div>
</div>
<script>
</script>
As you can see, the standard even handle optional PATH component. It make it a breeze !
And you can also use "." or ";" -- for matrix notation and even expand query strings!

Rerender angular-datatables when switching language with angular-translate

I use angular-translate with angular-datatables and implemented a language switch between german and english (Explained here Switching between languages. Switching language works well but not with angular-datatables. When i switch angular-datatables keeps the old translations for the table header.
angular-datatables Controller:
It's loading the datatables data via json with a promise and then draws the table. It also refreshes the table every 5 minutes. I implemented a public function "rerenderTable" which i call when switching the app language.
.controller('DashboardCtrl', ['$scope', '$rootScope', 'DTOptionsBuilder', 'DTColumnBuilder', 'DTInstances', '$resource',
'$interval', '$translate',
function($scope, $rootScope, DTOptionsBuilder, DTColumnBuilder, DTInstances, $resource, $interval, $translate)
{
$scope.initTargetPackaging = function initTargetPackaging()
{
$scope.sourceUrl = 'json/v1.0.0/dashboard/datatables/myjson1.json';
};
$scope.initTargetConversion = function initTargetConversion()
{
$scope.sourceUrl = 'json/v1.0.0/dashboard/datatables/myjson2.json';
};
// Get the TargetPackaging JSON Data with promise AJAX call
var vm = this;
vm.dtOptions = DTOptionsBuilder.fromFnPromise( function()
{
return $resource($scope.sourceUrl).query().$promise;
})
.withOption('bInfo', false)
.withOption('paging', false)
.withOption('filter', false)
.withOption('rowCallback', rowCallback);
// Create the table columns
vm.dtColumns = [
DTColumnBuilder.newColumn('customer')
.withTitle($translate('DIRECTIVES.DASHBOARD.DATATALBE_TARGET_PACKAGING_COLUMN_CUSTOMER')),
DTColumnBuilder.newColumn('today')
.withTitle($translate('DIRECTIVES.DASHBOARD.DATATALBE_TARGET_PACKAGING_COLUMN_TODAY')),
DTColumnBuilder.newColumn('week')
.withTitle($translate('DIRECTIVES.DASHBOARD.DATATALBE_TARGET_PACKAGING_COLUMN_7DAYS')),
DTColumnBuilder.newColumn('month')
.withTitle($translate('DIRECTIVES.DASHBOARD.DATATALBE_TARGET_PACKAGING_COLUMN_30DAYS'))
];
vm.newPromise = newPromise;
vm.reloadData = reloadData;
vm.dtInstance = {};
function newPromise()
{
return $resource($scope.sourceUrl).query().$promise;
}
/**
* Reload the data
*/
function reloadData()
{
var resetPaging = false;
vm.dtInstance.reloadData(resetPaging);
}
// Trigger reloading - 5 mins
$interval(reloadData, 300000);
function rowCallback(nRow, aData)
{
// Add status CSS class if state is true
if (aData['state'] != undefined
&& aData['state'] === true)
{
$(nRow).addClass('ad-status-inactive');
}
}
$rootScope.rerenderTable = function()
{
vm.dtInstance.rerender();
};
}]);
Function to switch language:
$scope.changeLang = function(key)
{
$translate.use(key).then( function(key)
{
console.log("Sprache zu " + key + " gewechselt.");
$rootScope.rerenderTable();
},
function(key)
{
// Trigger log error message (failure of switching language)
});
};
Triggered here in html:
<div id="language-switch" class="col-xs-2" ng-controller="LanguageCtrl">
<div class="inner">
{{ 'MAIN_NAVIGATION.CHOOSE_LANGUAGE' | translate }}
<span ng-click="changeLang('en')" class="lang-sm" lang="en"></span>
<span ng-click="changeLang('de')" class="lang-sm" lang="de"></span>
</div>
</div>
Summary: Switching languages works well. But not in the case of angular-datatables. It does not switch the language. But translating the strings is fine.
How do i get angular-datatables to rerender the table by using the currently chosen language?
1- Listen to the language change to render the table afterwards.
$rootScope.$on('$translateChangeEnd', function (event, lang) {
$scope.dtInstance.rerender();
});
2-Inside constructor function of your table
var headerCallback = function( thead, data, start, end, display ) {
$compile(angular.element(thead).contents())($scope);
}
3-
$scope.dtOptions(your name) = DTOptionsBuilder
.newOptions()
.withOption('headerCallback', headerCallback)
..........your code
$scope.dtColumns = [
DTColumnBuilder.newColumn('code').withTitle(`${'<span translate>'}${'TAG'}${'</span>'}`).renderWith(your_code).withClass('center-text'),
.........
Works for me ;)
A little bit late but here is an answer, which is not the best imho:
$rootScope.$on('$translateChangeSuccess', function (event, lang) {
vm.dtOptions.withLanguageSource('http://cdn.datatables.net/plug-ins/1.10.11/i18n/'+(lang.language == 'de' ? 'German' : 'English')+'.json');
$rootScope.rerenderTable();
});
It's a shame, they doesn't provide language files named like the ISO-codes. So you have to convert them into the english "long" language names.

Proper place for data-saving logic in AngularJS

App design question. I have a project which has a very large number of highly customized inputs. Each input is implemented as a directive (and Angular has made this an absolute joy to develop).
The inputs save their data upon blur, so there's no form to submit. That's been working great.
Each input has an attribute called "saveable" which drives another directive which is shared by all these input types. the Saveable directive uses a $resource to post data back to the API.
My question is, should this logic be in a directive at all? I initially put it there because I thought I would need the saving logic in multiple controllers, but it turns out they're really happening in the same one. Also, I read somewhere (lost the reference) that the directive is a bad place to put API logic.
Additionally, I need to introduce unit testing for this saving logic soon, and testing controllers seems much more straightforward than testing directives.
Thanks in advance; Angular's documentation may be… iffy… but the folks in the community are mega-rad.
[edit] a non-functional, simplified look at what I'm doing:
<input ng-model="question.value" some-input-type-directive saveable ng-blur="saveModel(question)">
.directive('saveable', ['savingService', function(savingService) {
return {
restrict: 'A',
link: function(scope) {
scope.saveModel = function(question) {
savingService.somethingOrOther.save(
{id: question.id, answer: question.value},
function(response, getResponseHeaders) {
// a bunch of post-processing
}
);
}
}
}
}])
No, I don't think the directive should be calling $http. I would create a service (using the factory in Angular) OR (preferably) a model. When it is in a model, I prefer to use the $resource service to define my model "classes". Then, I abstract the $http/REST code into a nice, active model.
The typical answer for this is that you should use a service for this purpose. Here's some general information about this: http://docs.angularjs.org/guide/dev_guide.services.understanding_services
Here is a plunk with code modeled after your own starting example:
Example code:
var app = angular.module('savingServiceDemo', []);
app.service('savingService', function() {
return {
somethingOrOther: {
save: function(obj, callback) {
console.log('Saved:');
console.dir(obj);
callback(obj, {});
}
}
};
});
app.directive('saveable', ['savingService', function(savingService) {
return {
restrict: 'A',
link: function(scope) {
scope.saveModel = function(question) {
savingService.somethingOrOther.save(
{
id: question.id,
answer: question.value
},
function(response, getResponseHeaders) {
// a bunch of post-processing
}
);
}
}
};
}]);
app.controller('questionController', ['$scope', function($scope) {
$scope.question = {
question: 'What kind of AngularJS object should you create to contain data access or network communication logic?',
id: 1,
value: ''
};
}]);
The relevant HTML markup:
<body ng-controller="questionController">
<h3>Question<h3>
<h4>{{question.question}}</h4>
Your answer: <input ng-model="question.value" saveable ng-blur="saveModel(question)" />
</body>
An alternative using only factory and the existing ngResource service:
However, you could also utilize factory and ngResource in a way that would let you reuse some of the common "saving logic", while still giving you the ability to provide variation for distinct types of objects / data that you wish to save or query. And, this way still results in just a single instantiation of the saver for your specific object type.
Example using MongoLab collections
I've done something like this to make it easier to use MongoLab collections.
Here's a plunk.
The gist of the idea is this snippet:
var dbUrl = "https://api.mongolab.com/api/1/databases/YOURDB/collections";
var apiKey = "YOUR API KEY";
var collections = [
"user",
"question",
"like"
];
for(var i = 0; i < collections.length; i++) {
var collectionName = collections[i];
app.factory(collectionName, ['$resource', function($resource) {
var resourceConstructor = createResource($resource, dbUrl, collectionName, apiKey);
var svc = new resourceConstructor();
// modify behavior if you want to override defaults
return svc;
}]);
}
Notes:
dbUrl and apiKey would be, of course, specific to your own MongoLab info
The array in this case is a group of distinct collections that you want individual ngResource-derived instances of
There is a createResource function defined (which you can see in the plunk and in the code below) that actually handles creating a constructor with an ngResource prototype.
If you wanted, you could modify the svc instance to vary its behavior by collection type
When you blur the input field, this will invoke the dummy consoleLog function and just write some debug info to the console for illustration purposes.
This also prints the number of times the createResource function itself was called, as a way to demonstrate that, even though there are actually two controllers, questionController and questionController2 asking for the same injections, the factories get called only 3 times in total.
Note: updateSafe is a function I like to use with MongoLab that allows you to apply a partial update, basically a PATCH. Otherwise, if you only send a few properties, the entire document will get overwritten with ONLY those properties! No good!
Full code:
HTML:
<body>
<div ng-controller="questionController">
<h3>Question<h3>
<h4>{{question.question}}</h4>
Your answer: <input ng-model="question.value" saveable ng-blur="save(question)" />
</div>
<div ng-controller="questionController2">
<h3>Question<h3>
<h4>{{question.question}}</h4>
Your answer: <input ng-model="question.value" saveable ng-blur="save(question)" />
</div>
</body>
JavaScript:
(function() {
var app = angular.module('savingServiceDemo', ['ngResource']);
var numberOfTimesCreateResourceGetsInvokedShouldStopAt3 = 0;
function createResource(resourceService, resourcePath, resourceName, apiKey) {
numberOfTimesCreateResourceGetsInvokedShouldStopAt3++;
var resource = resourceService(resourcePath + '/' + resourceName + '/:id',
{
apiKey: apiKey
},
{
update:
{
method: 'PUT'
}
}
);
resource.prototype.consoleLog = function (val, cb) {
console.log("The numberOfTimesCreateResourceGetsInvokedShouldStopAt3 counter is at: " + numberOfTimesCreateResourceGetsInvokedShouldStopAt3);
console.log('Logging:');
console.log(val);
console.log('this =');
console.log(this);
if (cb) {
cb();
}
};
resource.prototype.update = function (cb) {
return resource.update({
id: this._id.$oid
},
angular.extend({}, this, {
_id: undefined
}), cb);
};
resource.prototype.updateSafe = function (patch, cb) {
resource.get({id:this._id.$oid}, function(obj) {
for(var prop in patch) {
obj[prop] = patch[prop];
}
obj.update(cb);
});
};
resource.prototype.destroy = function (cb) {
return resource.remove({
id: this._id.$oid
}, cb);
};
return resource;
}
var dbUrl = "https://api.mongolab.com/api/1/databases/YOURDB/collections";
var apiKey = "YOUR API KEY";
var collections = [
"user",
"question",
"like"
];
for(var i = 0; i < collections.length; i++) {
var collectionName = collections[i];
app.factory(collectionName, ['$resource', function($resource) {
var resourceConstructor = createResource($resource, dbUrl, collectionName, apiKey);
var svc = new resourceConstructor();
// modify behavior if you want to override defaults
return svc;
}]);
}
app.controller('questionController', ['$scope', 'user', 'question', 'like',
function($scope, user, question, like) {
$scope.question = {
question: 'What kind of AngularJS object should you create to contain data access or network communication logic?',
id: 1,
value: ''
};
$scope.save = function(obj) {
question.consoleLog(obj, function() {
console.log('And, I got called back');
});
};
}]);
app.controller('questionController2', ['$scope', 'user', 'question', 'like',
function($scope, user, question, like) {
$scope.question = {
question: 'What is the coolest JS framework of them all?',
id: 1,
value: ''
};
$scope.save = function(obj) {
question.consoleLog(obj, function() {
console.log('You better have said AngularJS');
});
};
}]);
})();
In general, things related to the UI belong in a directive, things related to the binding of input and output (either from the user or from the server) belong in a controller, and things related to the business/application logic belong in a service (of some variety). I've found this separation leads to very clean code for my part.

Resources