I'm using AngularJS to interact with a RESTful webservice, using $resource to abstract the various entities exposed. Some of this entities are images, so I need to be able to use the save action of $resource "object" to send both binary data and text fields within the same request.
How can I use AngularJS's $resource service to send data and upload images to a restful webservice in a single POST request?
I've searched far and wide and, while I might have missed it, I couldn't find a solution for this problem: uploading files using a $resource action.
Let's make this example: our RESTful service allows us to access images by making requests to the /images/ endpoint. Each Image has a title, a description and the path pointing to the image file. Using the RESTful service, we can get all of them (GET /images/), a single one (GET /images/1) or add one (POST /images). Angular allows us to use the $resource service to accomplish this task easily, but doesn't allow for file uploading - which is required for the third action - out of the box (and they don't seem to be planning on supporting it anytime soon). How, then, would we go about using the very handy $resource service if it can't handle file uploads? It turns out it's quite easy!
We are going to use data binding, because it's one of the awesome features of AngularJS. We have the following HTML form:
<form class="form" name="form" novalidate ng-submit="submit()">
<div class="form-group">
<input class="form-control" ng-model="newImage.title" placeholder="Title" required>
</div>
<div class="form-group">
<input class="form-control" ng-model="newImage.description" placeholder="Description">
</div>
<div class="form-group">
<input type="file" files-model="newImage.image" required >
</div>
<div class="form-group clearfix">
<button class="btn btn-success pull-right" type="submit" ng-disabled="form.$invalid">Save</button>
</div>
</form>
As you can see, there are two text input fields that are binded each to a property of a single object, which I have called newImage. The file input is binded as well to a property of the newImage object, but this time I've used a custom directive taken straight from here. This directive makes it so that every time the content of the file input changes, a FileList object is put inside the binded property instead of a fakepath (which would be Angular's standard behavior).
Our controller code is the following:
angular.module('clientApp')
.controller('MainCtrl', function ($scope, $resource) {
var Image = $resource('http://localhost:3000/images/:id', {id: "#_id"});
Image.get(function(result) {
if (result.status != 'OK')
throw result.status;
$scope.images = result.data;
})
$scope.newImage = {};
$scope.submit = function() {
Image.save($scope.newImage, function(result) {
if (result.status != 'OK')
throw result.status;
$scope.images.push(result.data);
});
}
});
(In this case I am running a NodeJS server on my local machine on port 3000, and the response is a json object containing a status field and an optional data field).
In order for the file upload to work, we just need to properly configure the $http service, for example within the .config call on the app object. Specifically, we need to transform the data of each post request to a FormData object, so that it's sent to the server in the correct format:
angular.module('clientApp', [
'ngCookies',
'ngResource',
'ngSanitize',
'ngRoute'
])
.config(function ($httpProvider) {
$httpProvider.defaults.transformRequest = function(data) {
if (data === undefined)
return data;
var fd = new FormData();
angular.forEach(data, function(value, key) {
if (value instanceof FileList) {
if (value.length == 1) {
fd.append(key, value[0]);
} else {
angular.forEach(value, function(file, index) {
fd.append(key + '_' + index, file);
});
}
} else {
fd.append(key, value);
}
});
return fd;
}
$httpProvider.defaults.headers.post['Content-Type'] = undefined;
});
The Content-Type header is set to undefined because setting it manually to multipart/form-data would not set the boundary value, and the server would not be able to parse the request correctly.
That's it. Now you can use $resource to save() objects containing both standard data fields and files.
WARNING This has some limitations:
It doesn't work on older browsers. Sorry :(
If your model has "embedded" documents, like
{
title: "A title",
attributes: {
fancy: true,
colored: false,
nsfw: true
},
image: null
}
then you need to refactor the transformRequest function accordingly. You could, for example, JSON.stringify the nested objects, provided you can parse them on the other end
English is not my main language, so if my explanation is obscure tell me and I'll try to rephrase it :)
This is just an example. You can expand on this depending on what your application needs to do.
I hope this helps, cheers!
EDIT:
As pointed out by #david, a less invasive solution would be to define this behavior only for those $resources that actually need it, and not to transform each and every request made by AngularJS. You can do that by creating your $resource like this:
$resource('http://localhost:3000/images/:id', {id: "#_id"}, {
save: {
method: 'POST',
transformRequest: '<THE TRANSFORMATION METHOD DEFINED ABOVE>',
headers: '<SEE BELOW>'
}
});
As for the header, you should create one that satisfies your requirements. The only thing you need to specify is the 'Content-Type' property by setting it to undefined.
The most minimal and least invasive solution to send $resource requests with FormData I found to be this:
angular.module('app', [
'ngResource'
])
.factory('Post', function ($resource) {
return $resource('api/post/:id', { id: "#id" }, {
create: {
method: "POST",
transformRequest: angular.identity,
headers: { 'Content-Type': undefined }
}
});
})
.controller('PostCtrl', function (Post) {
var self = this;
this.createPost = function (data) {
var fd = new FormData();
for (var key in data) {
fd.append(key, data[key]);
}
Post.create({}, fd).$promise.then(function (res) {
self.newPost = res;
}).catch(function (err) {
self.newPostError = true;
throw err;
});
};
});
Please note that this method won't work on 1.4.0+. For more
information check AngularJS changelog (search for $http: due to 5da1256) and this issue. This was actually an unintended (and therefore removed) behaviour on AngularJS.
I came up with this functionality to convert (or append) form-data into a FormData object. It could probably be used as a service.
The logic below should be inside either a transformRequest, or inside $httpProvider configuration, or could be used as a service. In any way, Content-Type header has to be set to NULL, and doing so differs depending on the context you place this logic in. For example inside a transformRequest option when configuring a resource, you do:
var headers = headersGetter();
headers['Content-Type'] = undefined;
or if configuring $httpProvider, you could use the method noted in the answer above.
In the example below, the logic is placed inside a transformRequest method for a resource.
appServices.factory('SomeResource', ['$resource', function($resource) {
return $resource('some_resource/:id', null, {
'save': {
method: 'POST',
transformRequest: function(data, headersGetter) {
// Here we set the Content-Type header to null.
var headers = headersGetter();
headers['Content-Type'] = undefined;
// And here begins the logic which could be used somewhere else
// as noted above.
if (data == undefined) {
return data;
}
var fd = new FormData();
var createKey = function(_keys_, currentKey) {
var keys = angular.copy(_keys_);
keys.push(currentKey);
formKey = keys.shift()
if (keys.length) {
formKey += "[" + keys.join("][") + "]"
}
return formKey;
}
var addToFd = function(object, keys) {
angular.forEach(object, function(value, key) {
var formKey = createKey(keys, key);
if (value instanceof File) {
fd.append(formKey, value);
} else if (value instanceof FileList) {
if (value.length == 1) {
fd.append(formKey, value[0]);
} else {
angular.forEach(value, function(file, index) {
fd.append(formKey + '[' + index + ']', file);
});
}
} else if (value && (typeof value == 'object' || typeof value == 'array')) {
var _keys = angular.copy(keys);
_keys.push(key)
addToFd(value, _keys);
} else {
fd.append(formKey, value);
}
});
}
addToFd(data, []);
return fd;
}
}
})
}]);
So with this, you can do the following without problems:
var data = {
foo: "Bar",
foobar: {
baz: true
},
fooFile: someFile // instance of File or FileList
}
SomeResource.save(data);
Related
I am doing an angular application with asp.net mvc and i made a registration form with identity, I have layout and index mvc view which i just write in it ng-view tag and i inject html pages in it, I am doing a http post request from angular controller to mvc action method but the request does not go to the mvc action, whereas when i change th views to mvc views and make a templateUrl in angular map to mvc method it works well.
Can any one help me in this problem.
[HttpPost]
[AllowAnonymous]
public async Task<JsonResult> Register(RegisterViewModel model)
{
string message = "";
if (ModelState.IsValid)
{
var user = new ApplicationUser
{
FirstName = model.FirstName,
MiddleName = model.MiddleName,
LastName = model.LastName,
UserName = model.Email,
Email = model.Email,
UserStatus = UserStatus.Waiting
};
var result = await UserManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
message = "Success";
}
else
{
AddErrors(result);
message = "InvalidEmail";
}
}
else
{
message = "Failed!";
}
return new JsonResult { Data = message, JsonRequestBehavior = JsonRequestBehavior.AllowGet };
}
and this is my angular controller
MyApp.controller('RegisterController', ['$scope','$http',function ($scope, $http) {
$scope.message = '';
$scope.isFormValid = false;
//Check Form Validation
$scope.$watch('f1.$valid', function (newValue) {
$scope.isFormValid = newValue;
});
//Save Data
$scope.SaveData = function (data) {
$scope.submitted = true;
$scope.message = '';
if ($scope.isFormValid) {
$http({
method: 'POST',
url: '/Account/Register',
data: data
}).then(function (response) {
// check your response (if a success status code was resolved)
console.log(response.data);
}, function (error) {
// check your error response
console.log(error);
});
} else {
$scope.message = "Please Fill the blanks";
}
}
}]);
and this is my html page:
<div ng-controller="RegisterController">
<form name="f1" ng-submit="SaveData(user)" novalidate>
controls here
</form
1) Check your browser console for any javascript errors, if you have any, resolve them and try again!
2) Check you have the correct ActionMethodSelectorAttribute attribute ([HttpPost]) over your controller method and that your method name is spelt correctly.
3) Check that you have the correct path in your request.
4) Check you are sending the correct data to the controller!!!
5) Check that the method is public.
6) Check that you are authorised to access that controller/method.
7) Check that you don't have any duplicate method names with either, a) the same parameters and name (if your not using an ActionMethodSelectorAttribute, or b) the same names and method select attributes.
8) Remove all parameters from your method, put a breakpoint at the start of the method, and try making the request and see if it hits the breakpoint. If it works without parameters and not with, then you are not passing the correct required data into the method.
9) Make your request and check the response!! (example below):
// make your request
$http({
method: 'POST',
url: '/Controller/Method',
data: {
foo: bar
}
}).then(function(response) {
// check your response (if a success status code was resolved)
console.log(response);
}, function(error) {
// check your error response
console.log(error);
});
If you have a 404 then your method was not found, if you have a 500 then something blew up in your code, if you have a 401 then you are unauthorised etc... This is really useful to actually know what is going on with your request...
10) Check your application is running!
I want to change some data before sending it to the server via ng-resource. I use the tranformRequest-Function like this:
update: {
method: 'PUT',
transformRequest: function (data) {
// modify data then
return data;
}
}
I can modify data this way but in the request my data is always serialized. I want keep my data as JSON. Is this possible with transformRequest or have this to be done in controller. I would prefer to do it in the service. Thx for help
Omg i feel like an idiot. You simply have to do
update: {
method: 'PUT',
transformRequest: function (data) {
// modify data then
return angular.toJson(data);
}
}
Here's an example I'm using in my app. Callers of the $resource methods pass in a simple list of parameters as JSON, and the transformRequest bundles the parameters into the format expected by the API I'm using.
var myServices = angular.module('myServices', ['ngResource']);
...
myServices.factory('MyServer', ['$resource', function($resource){
var formatLoginRequest = function(data) {
// input looks like: {username:"imauser", pw:"password"}
// output should be: {request: {cmd:"login", username:"imauser", pw:"password"}}
data.cmd="login";
data = {request: data};
data = angular.toJson(data);
return data;
};
...
return = $resource('https://api.server.com/', {}, {
login: {method:'POST', params:{}, isArray:false, transformRequest:formatLoginRequest },
...other methods defined here...
});
As noted elsewhere, angular.toJson() won't correctly serialize all data types, but it is sufficient for my case with JSON.
In case somebody else comes across this, Angular provides default transformations for objects. The $http service exposes defaults.transformRequest, which checks if the data property is an object and automatically serializes it to JSON.
For this particular case, i would do a simple check if data is an object and if not make it one and override the $http.defaults.transformRequest.
function appendTransform(defaults, transform) {
defaults = angular.isArray(defaults) ? defaults : [defaults];
return defaults.concat(transform);
};
update: {
method: 'PUT',
transformRequest:
appendTransform($http.defaults.transformResponse,function(data) {
data = angular.isObject(data) ? data : {data};
return data;
})
}
Yes it is. A bit troublesome and ulgy but here it is :
// from angular-resource
var toString= function() {
var value = [];
_.forEach(this, function(e) {
value.push('' + e);
});
return '[' + value.join(', ') + ']';
};
var isObject = function isObject(value) {
// http://jsperf.com/isobject4
return value !== null && typeof value === 'object';
};
var isFile = function(obj) {
return toString.call(obj) === '[object File]';
}
var isFormData = function(obj) {
return toString.call(obj) === '[object FormData]';
}
var isBlob = function(obj) {
return toString.call(obj) === '[object Blob]';
}
var defaultToJson = function(d) {
return isObject(d) && !isFile(d) && !isBlob(d) && !isFormData(d) ? angular.toJson(d) : d;
};
this.typeServiceProcessData = function(d){
//[put your code here for to change data this will be called before angular default serialization to the server]
};
this.typeServiceProcessJsData = function(d){
//[for data that come back from the server after getting parse by default angular json parsing]
};
// sample creation of a resource method, be really carefull about the order in transformResponse and transformRequest
'get': {method:'GET', transformResponse:[$http.defaults.transformResponse[0], this.typeServiceProcessData]},
'create': {method:'POST', url:baseResourceUrl, transformRequest:[this.typeServiceProcessJsData, defaultToJson]},
It's a big huge, it's a code i done some time ago and i copy/pasted some function definition from angular-resource because they weren't define in that scope and weren't accessible from outside of angular resource. To see why they're needed check the defaultToJson function i defined which is the angular default.
If someone has a better way to just copy paste that bunch of function i take too :)
I'm using staticFilesLoader with partialLoader wich is working really good! So the concept of addPart(home) method in partialLoader says that you do not need to load all the translate table.
Starting for that point. I'm gonna use a rest service now and my question is How to send to the rest service the parameter for just retrieving me the table translate for my specific section, in this example home.
Source Code from angular translate
function $translateUrlLoader($q, $http) {
'use strict';
return function (options) {
if (!options || !options.url) {
throw new Error('Couldn\'t use urlLoader since no url is given!');
}
var requestParams = {};
requestParams[options.queryParameter || 'lang'] = options.key;
return $http(angular.extend({
url: options.url,
params: requestParams,
method: 'GET'
}, options.$http))
.then(function(result) {
return result.data;
}, function () {
return $q.reject(options.key);
});
};
}
The code above doesn't have any extra parameter.
NOTE: Maybe in the specific case of urlLoader with partialLoader you should request all the table translate from the REST and only manage the parts in the frontend. Please tell me about this note if makes any sense.
I am having some trouble chaining promises in Angular. What I want to do is fetch my project object from the API, then check if the project owner has any containers, if they do, trigger the another GET to retrieve the container. In the end the container assigned to scope should either be null or the object retrieved from the API.
Right now, this example below resolves immediately to the second then function, and I get the error, TypeError: Cannot read property 'owner' of undefined. What am I doing wrong?
$http.get('/api/projects/' + id).then(function (data) {
$scope.project = data.project;
return data.project;
}).then(function (project) {
var containers = project.owner.containers;
if (containers.length) {
return $http.get('/api/containers/' + containers[0]);
} else {
return null
}
}).then(function (container) {
$scope.container = container;
});
Ah, turns out the data from passed into then is inside a field, so I needed to do
$scope.project = data.data.project;
return data.data.project;
Your example code works, but what if the $http call fails because of a 404? Or you want to later want to add some extra business logic?
In general you want to handle 'negative' cases using a rejecting promise, to have more control over the chaining flow.
$http.get('/api/projects/' + id).then(function (data) {
$scope.project = data.data.project;
return data.data.project;
}).then(function (project) {
var containers = project.owner.containers;
if (containers.length) {
return $q.reject('containers empty');
}
return $http.get('/api/containers/' + containers[0]);
}).then(function (container) {
$scope.container = container;
}).except(function (response) {
console.log(response); // 'containers empty' or $http response object
$scope.container = null;
});
I am new at AngularJS and I needed your help.
All I need just need is to POST my json to the API and recieve the proper response.
Here's my JSON where i don't know where to code this.
JSON
{
"userId" :"testAgent2",
"token" :"testAgent2",
"terminalInfo":"test2",
"forceLogin" :"false"
}
NOT SURE IF I'm doing this right.
CONTROLLER.JS
function UserLoginCtrl($scope, UserLoginResource) {
//Save a new userLogin
$scope.loginUser = function() {
var loggedin = false;
var uUsername = $scope.userUsername;
var uPassword = $scope.userPassword;
var uforcelogin = 'true';
UserLoginResource.save();
}
}
SERVICES.JS
angular.module('UserLoginModule', ['ngResource'])
.factory('UserLoginResource', function($resource, $http) {
$http.defaults.useXDomain = true;
delete $http.defaults.headers.common['X-Requested-With'];
$http.defaults.headers.post["Content-Type"] = "application/json"; //NOT WORKING
return $resource('http://123.123.123.123\\:1234/SOME/LOCATION/THERE', {}, {
save: {
method:'POST',
headers: [{'Content-Type': 'application/json'}]
} //NOT WORKING EITHER
});
});
INDEX.HTML
<html ng-app>
<head>
<script src="js/lib/angular/angular.js"></script>
<script src="js/lib/angular/angular-resource.js"></script>
</head>
<body ng-controller="UserLoginCtrl">
<form class="form-horizontal" name="form-horizontal" ng-submit="loginUser();">
<div class="button-login">
<!-- start: button-login -->
<button class="btn btn-primary" type="submit">Login</button>
</div>
</form>
</body>
</html>
I kept on getting a response like Unsupported Media Type. I don't know, what else to do.
Assuming you are able to use one of the more recent "unstable" releases, the correct syntax to change the header is.
app.factory('BarService', function ($resource) {
var BarService = $resource('/foo/api/bars/:id', {}, {
'delete': {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
}
});
return BarService;
});
I find the $resource service is a tremendously powerful tool for building applications and has matured to a point that you do not need to fall back to $http as much. Plus its active record like patterns are damn convenient.
Posting a JSON object is quite easy in Angular. All you need to do is the following:
Create a Javascript Object
I'll use your exact properties from your code.
var postObject = new Object();
postObject.userId = "testAgent2";
postObject.token = "testAgent2";
postObject.terminalInfo = "test2";
postObject.forceLogin = "false";
Post the object to the API
To post an object to an API you merely need a simple $http.post function. See below:
$http.post("/path/to/api/", postObject).success(function(data){
//Callback function here.
//"data" is the response from the server.
});
Since JSON is the default method of posting to an API, there's no need to reset that. See this link on $http shortcuts for more information.
With regards to your code specifically, try changing your save method to include this simple post method.
The right way to set 'Content-Type': 'application/json' is setting a transformRequest function for the save action.
angular.module('NoteWrangler')
.factory('NoteNgResource', function NoteNgResourceFactory($resource) {
// https://docs.angularjs.org/api/ngResource/service/$resource
return $resource("./php/notes/:id", {}, {
save : { // redefine save action defaults
method : 'POST',
url : "./php/notes", // I dont want the id in the url
transformRequest: function(data, headers){
console.log(headers);
headers = angular.extend({}, headers, {'Content-Type': 'application/json'});
console.log(headers);
console.log(data);
console.log(angular.toJson(data));
return angular.toJson(data); // this will go in the body request
}
}
});
});
It seems there isn't a method to clear query parameters, the request will have both...