I inherited an Angular application that calls a Web API service. I am trying to keep the existing structure as close to the original as possible. A new requirement is to pass in a number of days, and I have modified the call:
(function () {
'use strict';
angular
.module('myApp')
.factory('summaryService', ['$resource', summaryService]);
function summaryService($resource) {
return $resource('/api/summary/?days=:days', { days: '#days' }, {
getUserSummary: { method: 'POST' }
});
}
}());
The code that calls the getUserSummary service is below. I have a drop down list that is populated with days at load time. The updateByDays function is what is called when the days DDL is changed. This fires correctly, and the "daysToReport" parameter is being updated to the correct value.
summaryService.getUserSummary({ Name: currentUser, days: 21 },
function (value) {
$scope.daysList = [{
name: '30 Days',
value: '30'
}, {
name: '60 Days',
value: '60'
}];
$scope.updateByDays = function (daysToReport) {
$scope.days = days;
...
};
},
function (httpResponse) {
...
}
);
As it is now, the call to the Web API method is successfully made, but it always has a "days" parameter value of 21", which is expected - it's the hard-coded value in the parameter list. Somehow, when the updateByDays method fires, I need to update the "days" parameter so the call to the Web API is made correctly. How can this be accomplished?
Thank you,
Scott
Pay attention on what are you trying to call resource with. Look at this docs: AngularJS $resource.
So, you call POST resource. You have to call resource functions with this params:
summaryService.getUserSummary(
{getParams},
{postParams},
function successCallback(){},
function errorCallback() {}
);
So, your call will be:
summaryService.getUserSummary(
{days: selectedDaysValue},
{},
function onSuccess() {}
);
Note to selectedDaysValue. It is model from ngModel directive of your select in html. It must be dynamic. You send 21 days all time because you call resource with static object { Name: currentUser, days: 21 }. Also, notice that Name will be added to query like &Name=currentUserValue.
Also, you want to use query in your request. You don't need to specify query in your resource parameters.
Resource definition will be:
return $resource('/api/summary');
Resource call:
myResource.get({days: 21}, function onSuccess() {});
Actual request will be: /api/summary?days=21
I probably didn't explain all the various pieces of the puzzle, but I tried the solution, and for my situation, it wasn't working. So I ended up cheating and using plain JavaScript.
In the method, I found the Days DDL and set the selected option to the matching text value:
$scope.updateByDays = function (days) {
var desiredValue = days.name;
var el = document.getElementById("Days");
for (var i = 0; i < el.options.length; i++) {
if (el.options[i].text == desiredValue) {
el.selectedIndex = i;
break;
}
}
There goes my "Coder of the Year" award.
Related
I explain myself,
The application i'm making right now come from a derivative of phonegap. I must absolutely waiting ajax answer to set de ng-init. I know i can do something like this ng-init = "ng-model = arr[0]" or i can use a variable for le position of the array like ng-init = "ng-model = arr[position]". That is simple in most case, but me because i'm working on a derivative phonegap i must call my function from a focus method with an ajax call, plus waiting after the result of another ajax call. See what i mean:
HTML select code:
<select id="usSelect" class="geotabFormEditField" ng-init="usCycle = usCyclesArr[usPos]" ng-model="usCycle" ng-options="x.descEn for x in usCyclesArr" ng-change="usSel()"}></select>
Function where i call my angular function to define which option must be selected. I must absolutely call it in this function.
focus: function (freshApi, freshState) {
freshApi.getSession(session => {
database = session.database;
freshApi.call('Get', {
typeName: 'User',
search: {
name: session.userName
}
}, function(user){
lang = user[0].language;
glScope.getHosRules(database);
}, function(){
//Oops can't get user
});
});
},
function called is getHosRule.
The angular array:
$scope.usCyclesArr = [{cycleId: 'America8Day', descEn: 'USA Property 70-hour/8-day', descFr: 'Propriété É.-U. 70 heures/8 jours'}, {cycleId: 'America7Day', descEn: 'USA Property 60-hour/7-day', descFr: 'Propriété É.-U. 60 heures/7 jours'}, {cycleId: 'America7DayBig', descEn: 'USA Property 60-hour/7-day (16-hour exemption)', descFr: 'Propriété É.-U. 60 heures/7 jours (exemption de 16 heures)'}, {cycleId: 'America7DayNo34h', descEn: 'USA Property 60-hour/7-day without 34-hour reset', descFr: ''}, ...];
function to get which option must be selected:
$scope.getHosRules = function(dat) {
console.log(dat, url);
$http({
method: 'GET',
url: $scope.urlPath + 'borderCross/' + dat + '/getHosRules',
responseType: 'json',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
}).then(function successCall(response) {
//console.log(response.data);
for(var i = 0; i < $scope.usCyclesArr.length; i++) {
//console.log($scope.usCyclesArr[i].cycleId);
if($scope.usCyclesArr[i].cycleId === response.data[0].us_id) {
$scope.usPos = i;
}
}
}, function errorCall() {
console.log('Unexpected error');
});
}
The problem is usPos get is value after ng-model is loaded and i must waiting for ajax answer, so it desn't working. I tried ng-init="{{usCycle = usCyclesArr[usPos]}}" It seem working but an error appear in the console and i can't take the risk on a professional app. So Anybody knows how can i bind usPos variable ?
From what I can understand from your question, you need to set the value of $scope.usCycle which will be shown as the selected value of dropdown.
Rather than setting the value in ng-init, why are you not setting in for loop:
for(var i = 0; i < $scope.usCyclesArr.length; i++) {
//console.log($scope.usCyclesArr[i].cycleId);
if($scope.usCyclesArr[i].cycleId === response.data[0].us_id) {
$scope.usCycle = usCyclesArr[i]; // <-- directly assigning value
}
}
Few points I would like to highlight:
You are facing issues because your ng-init works with AngularJS lifecycle where as your Business logic is triggered from focus function which is not at all connected with AngularJS. So, its a bad choice to mix code like this. To provide better UX, you can use spinner to make the user wait unless the $scope.usCycle value is assigned.
Rather than iterating through entire loop of for(var i = 0; i < $scope.usCyclesArr.length; i++) , you can break break the loop once you find the value. It will save extra looping cycles.
I found the way myself and it's simple. Refreshing my ng-model with the simple code below and removing ng-init attribute in html file.
$scope.usCycle = $scope.usCyclesArr[$scope.usPos];
Dropdown take right position.
Of course like Vivek said, breaking the loop when position is found is better.
I have a Sitesand a Positionscollection. Each time the user selects a new site, the id is sent to the refreshPositions method which is in charge of doing the fetch call.
The route to get the positions look like this '.../sites/1/positions'
view.js
refreshPositions: function(siteId) {
this._positions.fetch({
success: this.onPositionsFetchSuccess.bind(this),
error: this.onPositionsFetchError.bind(this)
});
},
So refreshPositions is called whenever I need to update the positionson the page and the siteId parameter has the id, I just don't know to tell fetch to route to something like .../sites/n/positions where n would be the siteId .
Sorry if I missed relevant informations for my question, I'm pretty new to backbone.
I see, so you are calling fetch from your Positions Collection. The out-of-the-box functionality there is to fetch the whole collection (every Position object) if you have a RESTfull api set up. If you want more specific behaviour from your collection, you can probably write it into the Collection object definition.
var PositionCollection = Backbone.Collection.extend({
initialize: function(models, options) {
this.siteId = (options && options.siteId) || 0;
},
url: function() {
if (!this.siteId) {
return '/positions'; // or whatever
}
return '/sites/' + this.siteId + '/positions';
},
// etc...
});
Then, assuming that _positions refers to an instance of PositionCollection you can do:
refreshPositions: function(siteId) {
this._positions.siteId = siteId; // or wrap in a setter if you prefer
this._positions.fetch({
success: this.onPositionsFetchSuccess.bind(this),
error: this.onPositionsFetchError.bind(this)
});
},
In a angular factory I have a method to create a new item, which has a connection to a user and a price to add to that users "items" array (like a shopping cart). So I have to see if the user is present in my the local users array if not then on the server and if not then create the user.
Code looks like this:
var saveItem = function (item) {
var user = filterUserById(item.ownerId);
if (user) {
user.createItem(item);
} else {
repository.getUserById(item.ownerId).then(
function (serverUser) {
var userViewModel = repository.getUserViewModel(serverUser);
userViewModel.createItem(item);
users.push(userViewModel);
}
, function () {
user = {
id: item.ownerId,
items: [
createItemDto(item)
]
};
repository.createUser({ id: user.id }, user);
users.push(repository.getUserViewModel(user));
});
}
};
No matter which of the "cases" occurs (user was found localy, on the server or was created and added) I get an error:
Error: [$rootScope:inprog] $apply already in progress
http://errors.angularjs.org/1.3.0-beta.18/$rootScope/inprog?p0=%24apply
I recon this may have to do with the fact that I'm using resources in my repository, but I don't think resource should (since it's a part of angular..). Here's the user.createItem method, code:
user.createItem = function (item) {
var resource = userResource
, itemDto = createItemDto(item)
, command = [{
Type: 'add',
Name: 'items',
Value: itemDto
}];
resource.createItem({ id: item.ownerId }, command);
this.items.push(itemDto);
};
Y U NO WERK!? PLS HLP! :'(
P.S. I don't have any explicit calls to apply, compile or digest anywhere in my code.
Found the problem! I had put a small code line to set focus on the correct input after the item was added and form was emptied. This consisted of a
$('selector').focus();
This was colliding with digest cycle... Solution:
$timeout($('selector').focus());
Try wrapping your call to user.createItem(item) in a $timeout function:
$timeout(function() {
user.createItem(item);
}, 0);
It's possible you could be triggering some other call to $scope.$apply() some other way.
Alternatively, try using $scope.$evalAsync(function())
Here's some good info: inprog
I'm building an app, that is backed with node-mysql combo, and angularjs on the frontend part. The backend REST service is ready, but I'm struggling with modeling my relational data. There are some questions regarding this like : $resource relations in Angular.js or $resource relations in Angular.js [updated] . Are those approaches still the best approaches, or were there any significant changes in $resource ? Or maybe Restangular is the way to go?
Here is my technique:
I declare a factory called dataService, which is a wrapper around Restangular, extended with some other features.
First let me gave some code and then explain:
.factory('identityMap',
var identityMap = {};
return {
insert: function(className, object) {
if (object) {
var mappedObject;
if (identityMap[className]) {
mappedObject = identityMap[className][object.id];
if (mappedObject) {
extend(mappedObject, object);
} else {
identityMap[className][object.id] = object;
mappedObject = object;
}
} else {
identityMap[className] = {};
identityMap[className][object.id] = object;
mappedObject = object;
}
return mappedObject;
}
},
remove: function(className, object) {
if (identityMap[className] && identityMap[className][id]) delete identityMap[className][id];
},
get: function(className, id) {
return identityMap[className] && identityMap[className][id] ? identityMap[className][id] : null;
},
flush: function(){
identityMap = {};
}
};
}
.factory('modelService', ['Restangular', 'identityMap', '$rootScope', '$log', function(Restangular, identityMap, $rootScope, $log) {
var ENUM1 = {STATE:0, OTHER_STATE:1, OTHER_STATE2: 2},
ENUM2 = {OK:0, ERROR:1, UNKNOWN:2};
function extendModel(obj, modelExtension, modelName){
angular.extend(obj, modelExtension);
obj.initExtension();
obj = identityMap.insert(modelName, obj);
}
function broadcastRestEvent(resourceName, operation, data){
$rootScope.$broadcast(resourceName + $filter('capitalize')(operation), data);
}
var resource1Extension = {
_extensionFunction1: function() {
// ... do something internally ...
if (this.something){
// this.newValue ....
;
}
else {
// ....;
}
},
publicExtensionFunction: function(param1) {
// return something
},
initExtension: function() {
this._extensionFunction2();
extendModel(this.resource2, resource2Extension, 'resource2');
}
};
var resorce2Extension = {
_extensionFunction1: function() {
// do something internally
},
publicExtensionFunction = function(param1) {
// return something
},
initExtension: function(){
this._extensionFunction1;
}
};
var modelExtensions = {
'resource1': resource1Extension,
'resource2': resorce2Extension
};
var rest = Restangular.withConfig(function(RestangularConfigurer) {
RestangularConfigurer.setBaseUrl('/api');
RestangularConfigurer.setOnElemRestangularized(function(obj, isCollection, what, Restangular){
if (!isCollection) {
if (modelExtensions.hasOwnProperty(what)) {
extendModel(obj, modelExtensions[what], what);
}
else {
identityMap.insert(what, obj);
}
if (obj.metadata && obj.metadata.operation) {
broadcastRestEvent(what, obj.metadata.operation, obj);
}
}
return obj;
});
RestangularConfigurer.addResponseInterceptor(function(data, operation, what, url, response, deferred) {
var newData;
if (operation === 'getList') {
newData = data.objects;
newData.metadata = {
numResults: data.num_results,
page: data.page,
totalPages: data.total_pages,
operation: operation
};
data = newData;
}
else if (operation === 'remove') {
var splittedUrl = url.split('/');
var id = splittedUrl.pop();
var resource = splittedUrl.pop();
identityMap.remove(resource, id);
broadcastRestEvent(resource, operation, id);
}
else {
data.metadata = {operation: operation};
}
return data;
});
});
return {
rest: rest,
enums: {
ENUM1: ENUM1,
ENUM2: ENUM2
},
flush: identityMap.flush,
get: identityMap.get
}
}]);
1) Let me explain identityMap (it's the code from this blog post with some extended features):
Let's consider a REST model which looks like this (each resource represents a database table):
resource 1:
id = Integer
field1 = String
field2 = String
resource2s = [] (List of resources2 which points to this resource with their foreign key)
resource 2:
id = Integer
field1 = String
field2 = String
...
resource1_idfk = Foreign Key to resource 1
Resource API is so smart that it returns resource1 relationships with resources2 with GET /api/resource1/1 to save the overhead that you would get with GET to resource2 with some query parameters to resource1_idfk...
The problem is that if your app is doing the GET to resource1 and then somewhere later GET to resource2 and edits the resource2, the object representing the resource2 which is nested in resource1 would not know about the change (because it is not the same Javascript object reference)
The identity map solves this issue, so you hold only one reference to each resource's instance
So, for example, when you are doing an update in your controller the values automatically updates in the other object where this resource is nested
The drawback is that you have to do memory management yourself and flush the identity map content when you no longer need it. I personally use Angular Router UI, and define this in a controller which is the root of other nested states:
$scope.$on("$destroy", function() {
modelService.flush();
});
The other approach I use within the Angular Router UI is that I give the id of the resource which i want to edit/delete within that controller as the parameter of nested state and within the nested state i use:
$scope.resource1instance = modelService.get('resource1', $stateParams.id);
You can than use
resource1.put(...).then(
function(){
// you don't need to edit resource1 in list of resources1
$state.go('^');
}
function(error){
handleError(error);
});
2) When I need to use some new functionality over resources I use `Restangular's setOnElemRestangularized. I think the code above is self explanatory and very similar to the one mentioned in blog post I have mentioned above. My approach is slightly different from the one in that post, that I don't use the mixin initialization before, but after I mix it to the object, so one could reference the new functions in initializer. The other thing I don't use, for example, he creates single factory for every resource, for example Proposal for extended logic and the other factory ProposalSvc for manipulating the instances. For me that's a lot of code you don't have to write and personally I think that Javascript is not suited very well for this object oriented approach, so I return just the whole Restangular object and do operations with it.
3) Another thing I have there is the broadcast of events when something in my model changes with Restangular, this is something I needed when I used ng-table. For example, when the model changed and rows in my table needed to be updated to reference the changes, so in the controller which manages the table I use $scope.on('eventName') and then change appropriate row. These events are also great when you have a multiuser live application and you use websockets for server notifications (code not included here in modelService). For example somebody deletes something in a database, so the server sends a notification to everyone who is alive through websocket about the change, you then broadcast the same event as used in Restangular and the controller does the same edits on its data.
This blog post should help you make your choice http://sauceio.com/index.php/2014/07/angularjs-data-models-http-vs-resource-vs-restangular/
I agree that there are a lot of good practices using http headers in Restangular, but you can pick them in the source and use them directly.
What you have to wonder is, will you be able to wrap your nested resources within a $resource and make instance calls while modifying the parent object. And that's not a given.
Your question seems to be asking whether you should be using ngResource, Restangular or some other framework or drop down to the low-level and use $http directly.
$resource is still widely used because it's included in the official docs and in all the popular tutorials and articles but Restangular is fairly popular.
The website ngModules shows a listing of REST API modules for AngularJS.
If you have a simple REST API, go with $resource for now and then switch to Restangular if you're doing lots of custom coding and filtering. It is a much nicer framework and more extensible.
I have a project where I'm using BreezeJS to fetch data from my webserver. I'm using AngularJS with the ui-select2 module. Currently, I have it where when I load my page, breezejs makes a call to fetch the data that I dump into a scope variable. From there, select2 can easily make the reference to it and build accordingly.
If I want to ajaxify things, it gets really tricky. I want to have the ability to use select2's ajax or query support, but instead of using it to fetch data, I want to use breezejs to do it. So during a page load, nothing is loaded up until I start typing in X minimum characters before it makes an ajax fetch.
Constraints:
I do not want fetch data using select2's "ajax". I want BreezeJS to handle the service calls. When I use ajax, it makes an ajax call everytime I press a character in order to filter the results (and resemble autocomplete). I just want the list to load up once and use the native filtering after that.
Here is what I have so far:
breezejs - StateContext.JS
m.app.factory('StateContext', ['$http', function ($http) {
configureBreeze();
var dataService = new breeze.DataService({
serviceName: "/Map/api",
hasServerMetadata: false
});
var manager = new breeze.EntityManager({ dataService: dataService});
var datacontext = {
getAllStates: getAllStates
};
return datacontext;
function getAllStates() {
var query = breeze.EntityQuery
.from("States");
return manager.executeQuery(query);
}
function configureBreeze() {
breeze.config.initializeAdapterInstances({ dataService: "webApi" });
}
}]);
This works and returns my json object correctly.
Here is how I call the service:
m.app.controller('LocationCtrl', ['$scope', 'StateContext', function ($scope, StateContext) {
$scope.getAllStates = function () {
StateContext.getAllStates().then(stateQuerySucceeded).fail(queryFailed);
}
$scope.getAllStates();
$scope.states = [];
function stateQuerySucceeded(data) {
data.results.forEach(function (item) {
$scope.states.push(item);
});
$scope.$apply();
console.log("Fetched States");
}
function queryFailed(error) {
console.log("Query failed");
}
$scope.select2StateOptions = {
placeholder: "Choose a State",
allowClear: true,
minimumInputLength: 2
};
}
and here is my html:
<div ng-app="m" id="ng-app">
...
...
<select ui-select2="select2StateOptions" ng-model="LocationModel.State">
<option value=""></option>
<option ng-repeat="state in states" value="{{state.id}}">{{state.name}}</option>
</select>
</div>
Currently the html select2 control loads up when the page loads. But I want to have it so when I type in more than 2 characters, I'll be able to make the call to $scope.getAllStates(); as an ajax call. BreezeJS already uses ajax natively when configuring the BreezeAdapter for webapi.
I was thinking about using select2's ajax, or query calls.. but I'd rather use breeze to fetch the data, since it makes querying extendable, and I don't want to violate my design pattern, or make the code harder to maintain, and I don't want the ajax calls to be made everytime I enter a new character into the textbox, I just want it to occur once.
Close attempt:
changed my html to:
<!-- Select2 needs to make this type="hidden" to use query or ajax, then it applies the UI skin afterwards -->
<input type="hidden" ui-select2="select2StateOptions" ng-model="LocationModel.State" /><br />
in my controller, changing select2StateOptions:
$scope.select2StateOptions = {
placeholder: "Choose a State",
allowClear: true,
minimumInputLength: 2,
query: function (query) {
debugger;
var data = StateContext.getAllStates().then(stateQuerySucceeded).fail(queryFailed);
}
};
Here's the problem. BreezeJS uses a Q library, which makes use of a thing called a "promise"; which is a promise that data will be returned after making the ajax call. The problem with this, the query function is expecting data to be populated, but the promise to call the "stateQuerySucceeded" function is made after returning from the query function.
So it hits the query function first. Then hits getAllStates(). Returns from the query (nothing is populated), then "stateQuerySucceeded" is called after that.
In otherwords, even though I have been able to fetch data, this is done too late.. select2's query function did not receive the data at the right time, and my html select is hanging on "Searching ... " with a search spinner.gif.
I don't really know this angular-ui-select2 control. I think the relevant part of the documentation is this example:
$("#e5").select2({
minimumInputLength: 2,
query: function (query) {
var data = {results: []}, i, j, s;
// simulate getting data from the server
for (i = 1; i < 5; i++) {
s = "";
for (j = 0; j < i; j++) {s = s + query.term;}
data.results.push({id: query.term + i, text: s});
}
query.callback(data);
}
});
I will leave aside the fact that you don't seem to be interested in using the two-or-more characters that the user enters in your query (maybe you just left that out). I'll proceed with what seems to me to be nonsense, namely, to fetch all states after the user types any two letters.
What I think you're missing is the role of the query.callback which is to tell "angular-ui-select2" when the data have arrived. I'm guessing you want to call query.callback in your success function.
$scope.select2StateOptions = {
placeholder: "Choose a State",
allowClear: true,
minimumInputLength: 2,
query: function (query) {
StateContext.getAllStates()
.then(querySucceeded).catch(queryFailed);
function querySucceeded(response) {
// give the {results:data-array} to the query callback
query.callback(response);
}
function queryFailed(error) {
// I don't know what you're supposed to do.
// maybe return nothing in the query callback?
// Tell the user SOMETHING and then
query.callback({results:[]});
}
}
};
As I said, I'm just guessing based on a quick reading of the documentation. Consider this answer a "hint" and please don't expect me to follow through and make this actually work.