What is the best practice of doing CRUD operations via REST with AngularJS?
Specially what is the Angular-Way here. By this I mean the way using the least code and the most default angular settings to achive this.
I know $resource and it's default operations. Where I'm not sure is how to implement/name the endpoints and which controllers to use.
For this example I would like to implement a simple user-management system which creates / updates /deletes / lists users. Since I'm implementing the Server-Endpoints by myself I'm completely free in doing it in the most angular friendly way.
What I like as answer is something like:
Server-Endpoints:
GET /service/users -> array of users
GET /service/user/new -> return an empty user with default values which has no id
POST /service/user/new -> store a new user and create an id. return the saved user.
POST /service/user/:ID -> save an existing user. Return the saved user
DELETE /service/user/:ID -> delete an existing user
Angular-Services:
.factory( 'User', [ '$resource', function( $resource ){
return $resource( '/service/user/:userId', { userId: '#id' } )
[...]
}])
Routing:
.when( '/users', {
templateUrl: BASE + 'partials/user-list.html',
controller: 'UserListCtrl' } )
.when( '/user/new', {
templateUrl: BASE + 'partials/user-edit.html',
controller: 'UserNewCtrl' } )
.when( '/user/:userId', {
templateUrl: BASE + 'partials/user-edit.html',
controller: 'UserEditCtrl' } )
...
Controllers:
UserListCtrl:
$scope.data = User.get(...)
UserNewCtrl:
$scope.user = User.get( { userId: "new" } )
...
Note that I'm not interessted in opinion what is the best (tm) way to do this but I'd like to know what is the Angular intended way (which I think should produce the least code because it can use the most default).
EDIT:
I'm looking for the whole picture. What I would love would be an answer like e.g.: "You can do this using online 3 Endpoints [...], 2 routes [...] and 2 controllers [...] if you do it this way using that defaults ..."
There is no Angular prescribed way for what you are asking. It's up to you to determine the implementation detail.
Typically I only use two controllers and templates per resource:
ListController
FormController
The Form controller is used for both Edit and Create operations. Use the resolve option in your route definitions to pass in either User.get() or User.new() and a flag indicating if this is an edit or create operation. This flag can then be used inside your FormController to decide which save method to call. Here's a simple example:
.when( '/users', {
templateUrl: BASE + 'partials/user-list.html',
controller: 'UserListCtrl' } )
.when( '/user/new', {
templateUrl: BASE + 'partials/user-form.html',
resolve: {
data: ['User', function(User) { return User.new(); }],
operation: 'create'
}
controller: 'UserFormCtrl' } )
.when( '/user/:userId', {
templateUrl: BASE + 'partials/user-form.html',
resolve: {
data: ['User', '$route', function(User, $route) { return User.get($route.current.params.userId); }],
operation: 'edit'
}
controller: 'UserFormCtrl' } )
And your form controller:
app.controller('UserFormCtrl', ['$scope', 'data', 'operation', function($scope, data, operation){
$scope.data = data;
$scope.save = function() {
if (operation === 'edit') {
// Do you edit save stuff
} else {
// Do you create save stuff
}
}
}]);
You can go a step further and create a base list and form controller to move stuff like error handling, server-side validation notifications etc. In fact for the majority of CRUD operations you can even move the save logic to this base controller.
My research into a similar quest has lead me to this project "angular-schema-form" https://github.com/Textalk/angular-schema-form.
For this approach...
You make a JSON-Schema that describes your data. Then augment it with another little JSON-struct that describes a "form" (ie. view specific info that does not belong in the data schema) and it makes a UI (form) for you.
One cool advantage is that the Schema is also useful in validating the data (client and server side), so that is a bonus.
You have to figure out what events should fire off GET/POST/... back to your API. but that would be your preference, eg. Hit the API for every key stroke OR the classic [Submit] button POST back style OR something in between with a timed Auto Save.
To keep this idea going, I think that it is possible to use StrongLoop to make a quick API, which (again) uses your data's schema (augmented with some storage details) to define the API.
no <3 uses of that schema, see [http://json-schema.org/] which is central to this approach.
(read: no less than three :)
You maybe mixing things up. CRUD operations at API level are done using $resource and these may or may not map to UI.
So using $resouce if you define resource as
var r = $resource('/users/:id',null, {'update': { method:'PUT' }});
r.query() //does GET on /users and gets all users
r.get({id:1}) // does GET on /users/1 and gets a specific user
r.save(userObject) // does a POST to /users to save the user
r.update({ id:1 }, userObject) // Not defined by default but does PUT to /users/1 with user object.
As you see the API is resource full but is in no way linked to any UI view.
For view you can use the convention you have defined, but nothing specific is provided by Angular.
I think what you are looking for can be found in http://www.synthjs.com/
Easily create new RESTful API resources by just creating folders and
naming functions a certain way.
Return data or promises from these
functions and they'll be rendered to the client as JSON.
Throw an
error, and it'll be logged. If running in dev mode, the error will
also be returned to the client.
Preload angular model data on page load (saving an extra
roundtrip).
Preload html view on page load (saving another extra
roundtrip!)
A simplified project structure
where front-end code (angular code, html, css, bower packages, etc)
is in the 'front' folder and back-end code (node code and node
packages) are in the 'back' folder.
A command-line tool for
installing third party packages, using npm + bower, that auto-updates
manifest files.
Auto compilation of assets on request for dev, and
pre-compilation for prod (including minification and ngmin).
Auto-restarts the server when changes are detected.
Support for
various back-end and front-end templates to help get a new project
going quickly.
Related
I have created a directive which wraps angular-translate and can also turns into input fields for easy Translating for admin users.
When I update a single translation I don't really want to load an entire translation table from my DB after updating 1 row in the DB, because it seems incredibly inefficient.
My problem is that I can't seem to find anything in the angular-translate API that will allow me to have access to the front-end Cache. I want to modify the translation map directly without having to bother the DB for an entire mapping of my translations after I updated 1 row successfully.
Things that I have tried: $translationCache, $translateLocalStorage, $translateCookieStorage.
Can someone enlighten me please.
Also, as a bonus, I wonder if anyone figured out where they can expose the translation mapping in angular translate.
Note, I don't want the translated values from $translate in the controller because that's already interpolated.
A short view into the source offers no simple way for this. I solved it finally caching a reference from $translateProvider.translations using a own provider:
app.provider('translationHelper', function () {
this.translations = {};
this.$get = function () {
return {
translations: this.translations
}
};
});
In your app do
app.config([
'$translateProvider',
'translationHelperProvider',
function (
$translateProvider,
translationHelperProvider
) {
translationHelperProvider.translations = $translateProvider.translations();
}]);
and use it later like
app.component('myComponent', {
templateUrl: 'views/components/myComponent.html',
controller: [
'translationHelper',
function (
translationHelper
) {
// query translation
var translation = translationHelper.translations['de']["Your.Key.Here"];
// modify translation
translationHelper.translations['de']["Your.Key.Here"] = 'A new value';
}]
});
Alternatively, you can modify the angular translate source and made 'translations' from provider accessible through its $get method.
This question already has answers here:
AngularJS : Initialize service with asynchronous data
(10 answers)
Closed 7 years ago.
I have a link generator service that is able to generate links to specific content types (users' details page, content items' details pages etc).
This service is really easy to use and has synchronous functions:
links.content(contentInstance); // /items/123
links.user(userInstance); // /users/234
I now have to introduce separate routing for logged in user to change from /users/id to /users/me.
The only change I'd need to add to my link generator service is to check whether userInstance.id == loggedInUser.id and return a different route URL. This is not a problem as long as my logged-in user's info would be synchronously available. but it's not...
I have a userService.getMyInfo() that returns a promise. The first time it's called it actually makes a server request but subsequent calls return a resolved promise with already cached data.
So how should I implement my user link URL generation in my link generator service?
Edit
Ok. So to see better what I have at the moment and where I'm having the problem. I'm pretty aware that async will stay async and that it can't be converted to synchronous (and it shouldn't be).
This is some more of my code, that will make it easier to understand.
linkGenerator.user
angular
.module("App.Globals")
.factory("linkGenerator", function(userService) {
...
user: function(userInstance) {
// I should be calling userService.getMyInfo() here
return "/users/{0}/{1}".format(userInstance.id, userInstance.name);
},
...
});
userService.getMyInfo
angular
.module("App.Globals")
.service("userService", function($q, cacheService, userResource) {
...
getMyInfo: function() {
if (cacheService.exists("USER_KEY"))
// return resolved promise
return $q.when(cacheService.get("USER_KEY"));
// get data
return userResource
.getCurrentUser()
.$promise
.then(function(userData) {
// cache it
cacheService.set("USER_KEY", userData);
});
},
...
});
Controller
angular
.module("App.Content")
.controller("ItemDetailsController", function(linkGenerator, ...) {
...
this.model = { ... };
this.helpers = {
...
links: linkGenerator,
...
};
...
});
View
View uses ItemDetailsController as context notation.
...
<a ng-href="{{::context.helpers.links(item.author)}}"
ng-bind="::item.author.name">
</a>
...
Notes
As you can see my view generates links to item authors. The problem is that my linkGenerator (as you can see from the code may not have the information yet whether it should generate one of the correct links to user details view.
I know I can't (and don't want to) change my async code to synchronous, but what would be the best way to make this thing work as expected?
One possible solution
For the time being I've come up with a solution that does the trick, but I don't really like it, as I have to supply my logged in user's ID to linkGenerator.user(userInstance, loggedInUserId) function. Then I set up my routing so that I add resolve to my route where I call userService.getMyInfo() which means that my controller is not being instantiated until all promises are resolved. Something along this line:
routeProvider
.when("...", {
templateUrl: "path/to/my/details/template",
controller: "ItemDetailsController".
controllerAs: "context",
resolve: {
myInfo: function(userService) {
return userService.getMyInfo();
}
}
})
...
Then I also add an additional helper to my controller
this.helpers = {
...
links: linkGenerator,
me: myInfo.id,
...
};
And then I also change link generator's function by adding the additional parameter that I then supply in the view.
linkGenerator.user = function(userInstance, loggedInUserId) {
if (userInstance.id === loggedInUserId)
return "users/me";
return "users/{0}/{1}".format(userInstance.id, userInstance.name);
}
and in the view
<a ng-href="{{::context.helpers.links.user(item.author, context.helpers.me)}}"...
And I don't to always supply logged in user's ID. I want my service to take care of this data on its own.
There is no way to make anything in JavaScript that is asynchronous at some point synchronous again. This is a ground rule of how concurrency works - no blocking for waiting for stuff is allowed.
Instead, you can make your new method return a promise and use the regular tools for waiting for it to resolve.
links.me = function(){
var info = userService.getMyInfo();
return info.then(info => { // or function(info){ if old browser
// generate link here
return `/users/${info.id}`; // or regular string concat if old browser
});
}
Which you'd have to use asynchronously as:
links.me().then(function(link){
// use link here
});
I'm new to angular-meteor and trying to make an application similar to Reddit for excercise. But I'm not sure what's the right approach to subscribe and publish. Here's my code.
angular.module('app')
.config(($stateProvider) ->
$stateProvider
.state('boards-list',
url: '/board'
templateUrl: 'client/boards/boards-list.view.ng.html'
controller: 'BoardsListCtrl'
)
.state('board-detail',
url: '/board/:symbol'
templateUrl: 'client/boards/board-detail.view.ng.html'
controller: 'BoardDetailCtrl'
)
)
I want to show popular post on /board and if you go to specific board I want to list all the post in the board.
This is board-list controller
angular.module('app')
.controller('BoardsListCtrl', ($scope, $meteor, $modal, $log) ->
$meteor.subscribe('getPopularPosts')
$meteor.subscribe('getAllBoards')
$scope.boards = $scope.$meteorCollection(->
Boards.find({})
)
$scope.posts = $scope.$meteorCollection(->
Posts.find({}, _.defaults(DEFAULT_QUERY_OPTIONS, limit: 5))
)
$scope.getHref = (link) -> if link then "http://#{link}" else "#"
$scope.remove = (board) ->
$scope.boards.remove(board)
$scope.open = ->
modalInstance = $modal.open(
templateUrl: 'client/common/modal-new-board.view.ng.html'
controller: 'ModalNewBoardCtrl'
)
)
And this is board-detail controller
angular.module('app')
.controller('BoardDetailCtrl', ($scope, $stateParams, $meteor, $modal) ->
boardSymbol = $stateParams.symbol
$scope.posts = []
$scope.$meteorSubscribe('getBoardBySymbol', boardSymbol)
$scope.board = $meteor.object(Boards, symbol: boardSymbol, false)
$scope.$meteorSubscribe('getPostsByBoard', boardSymbol)
$scope.posts = $scope.$meteorCollection(->
Posts.find(board: boardSymbol, DEFAULT_QUERY_OPTIONS)
)
$scope.open = ->
modalInstance = $modal.open(
templateUrl: 'client/common/modal-post.view.ng.html'
controller: 'ModalPostCtrl'
resolve:
symbol: -> $scope.board.symbol
)
)
My question is if I write Posts.find({}, DEFAULT_QUERY_OPTIONS) for boardDetailCtrl's $scope.posts, It actually shows all the posts as if the routes is /board when the state is changed. When you directly access the route like http://localhost:3000/angular it gets proper posts. Not sure why this is happening but I changed it to Posts.find({board: boardSymbol}, DEFAULT_QUERY_OPTIONS) Then it works as expected.
Considering that I only subscribe getPostByBoard which is about getting post only at given board, I should be able to get only a few of them. I checked console doing like Posts.find({}).count() and surprisingly it's having every post.
So my question is should I add query to find even after I subscribe the proper publish function? Or am I wrong at some point?
Whenever you start a subscription in Meteor you are adding those subscribed collections to a local collection. So if you have a boards collection and in your home page you subscribe to see ALL collections like:
$meteor.subscribe('getAllBoards')
You are adding ALL of the boards to the local collection. This local collection will be persistent until you explicitly stop the subscription. So in your board details page you still have access to that full collection.
If you notice you are using 2 different methods for subscribing. $scope.$meteorSubscribe and $meteor.subscribe. The first one will automatically remove the subscription when the $scope is destroyed. The second one will keep it.
So, if you wanted to remove the collection and re-subscribe when entering the board details page, you would want to use $scope.$meteorSubscribe in your main page as well.
Here's some food for thought. If you are on your main page and you use $meteor.subscribe, then you already have that information for those posts locally, correct? So, when you click on a link to view the board details, you don't actually need to subscribe again. You can just call:
$scope.board = $meteor.object(Boards, symbol: boardSymbol, false);
And you'll have access to that boards information. ( I just realized you were talking about Posts and not Boards, but the idea is still the same).
Short Answer: $meteor.subscribe() does not cancel the subscription on it's own, you have to tell it to. $scope.$meteorSubscribe does cancel the subscription.
Best practice? I would set the options for the subscription and then just create a local collection using find() like:
$scope.$meteorSubscribe('getPopularPosts', _.defaults(DEFAULT_QUERY_OPTIONS, limit: 5).then(-> {
$scope.posts = $scope.$meteorCollection(->
Posts.find();
)
})
Then in your publish add in the options:
Meteor.publish('getPopularPosts', function(options) {
return Posts.find({}, options);
});
This would limit the number of posts sent to the client rather than sending all of them and the only using 5.
When you switch to the board details view it will no longer have this information available to trip you up, because we used $scope.$meteorSubscribe and $scope.$meteorCollection.
I develop an application with Angularjs. In order to access to the application, the user has to be identified thanks to an external system (not developed by myself).
When the user is correctly identified, this external system redirected to my application.
I have implemented a general remote service for retrieving data (thanks to Coldfusion and an Oracle database) about a user (thanks to the login). This service is equally used in other applications. It returns general user information: firstname, lastname,..., and the user role for the specific application.
I would like to authorize the access to the sections of my application when the user has the corresponding role.
Thus I have called once the function of this service before to implement the routes in order to avoid many calls to the functions in the controllers. It works and I retrieve the user role.
var app=angular.module('ContactsApp', ['ngRoute', 'RemoteService']);
// CALL the method from THE REMOTE SERVICE - used $http.get and retrieve JSON data
app.factory('RemoteServiceFunction', ['RemoteServiceFactory', function (RemoteServiceFactory) {
return RemoteServiceFactory.Auth_getUserFromLogin(userid)
}]);
app.config(function($routeProvider, $httpProvider){
// FUNCTION USING THE REMOTE SERVICE IN ORDER TO INJECT DATA IN THE ROUTES
var wait = ['RemoteServiceFunction', function(RemoteServiceFunction) {
return RemoteServiceFunction;
}];
$routeProvider.when('/all-contacts',
{
templateUrl: 'template/allContacts.html',
controller: 'ctrlContacts',
resolve: {
personInfo: wait
}
})
.when('/view-contacts/:contactId',
{
templateUrl: 'template/viewContact.html',
controller: 'ctrlViewContacts',
resolve: {
personInfo: wait
}
})
.when('/search-contacts',
{
templateUrl: 'template/fastSearch.html',
controller: 'ctrlContactSearch',
resolve: {
personInfo: wait
}
})
.when('/add-contacts',
{
templateUrl: 'template/manageContact.html',
controller: 'ctrlAddContacts',
resolve: {
personInfo: wait
}
})
.otherwise({redirectTo:'/all-contacts'});
});
//THE CONTROLERS WITH DEPENDANCIES INJECTED
app.controller('ctrlContacts', function ($scope, ContactService, personInfo){
// alert(personInfoRole.data["VALUES"][0]["ROLES"]); OK the role is retrieved
}
Now I would like to enable or not the access to the various sections ( for example the role "admin" is mandatory for example in order to add a contact: ctrlAddContacts) . I do not know if I have to add a condition or a function in the resolve part to give the access.
Could you please tell me how to do that? Or tell me if another solution is better for using roles with Angularjs.
Thanks
Your best bet is probably to "intercept" the route change, that is: listening to the $routeChangeStart event on your rootScope. There you can check against whatever model variable you store the user priviledge in, and stop the routeChange from happening or redirect to an authentication screen.
Keep in mind though that such client-side authentication in an angular (or any javascript) application is just a user interface thing, since the user's browser will have full access to all the resources, and can even rewrite the validating parts of the javascript code, bypassing any route restrictions you set up. Sensitive information shouldn't travel to the client in the first place without proper authentication.
edit: see a full code solution here:best way to limit access to 'logged in' users
I am working on a log in for my backbone application and came on an issue I am not sure how to solve without making a call to the server. This brought up a discussion in my team about what the way other folks are handing this kind of thing in backbone because we think we will be running into a similar thing moving forward.
It's Friday and I'm probably just brain dead, but here goes...
We have a User Model. The login method of the View creates a new user Model and call's it's login method passing in the user's credentials and a callback function which has an object that contains the users information.
Here is the login method for our View:
login: function(event){
event.preventDefault();
var user = new App.User;
user.login($('#username').val(), $('#password').val(),
(function(msg) {
// success callback
if (msg.loggedIn) {
console.log("Authenticate successful: " + JSON.stringify(msg));
var data = { user : msg, bob : "bob", trigger:true };
console.log("Prepared data: " + JSON.stringify(data));
App.router.navigate('home',data);
} else {
console.log("Authenticate unsuccessful: " + JSON.stringify(msg));
}
}).bind(this),
function(msg) {
// failure callback
console.log("Authenticate communication failure: " + JSON.stringify(msg));
alert("Communication fail!");
App.router.navigate('login',{trigger:true});
});
}
What we are trying to figure out is how to best make this Model data available to another route (home) so we can use it in the View.
So I have this router:
routes: {
'': 'home',
'home': 'home',
'login': 'login'
},
home: function(data){
console.log(data);
}
Once we have logged the user in we need to update the route and have access to that users data, but don't want to have to make a trip back to the server to fetch it.
I am concerned because we are building a kind of "wizard" where the user may need to move forward and backward through some steps and I don't want to have to hit the server every time they navigate through the application, but it's seeming like we are going to either need to save stuff to a global variable (don't want to do this) or make a trip back to the server every time. I'm sure others have had to deal with similar issues. Just looking for some insight.
Thanks!
"it's seeming like we are going to either need to save stuff to a global variable (don't want to do this)"
To state the obvious: You're going to need to preserve state. Your options are either to transfer the state to the server and back, or hold state on the client. Since you've already identified that you don't want to pass the state via a server, you're left with preserving state between different pages (routes) on the client.
And that's what global variables are for. It sounds icky, I know, but it's also one of the main benefits that Single-Page Applications bring to the table. Statefulness. And that state will always be held by some global object.
There are better and worse ways of managing state. Having a global variable called data that you keep assigning and reassigning is obviously the worst way. You should figure out a pattern that makes sense for your requirement.
If I understood your code sample correctly, what you want to store is the information about the current user. It also seems that you already have a global variable App. It occurs to me that it would be a good idea to keep some kind of session info:
login: function(data){
doLogin({
success: function(userData) {
App.session.currentUser = userData;
App.router.navigate('home', { trigger:true });
}
});
},
home: function(data){
if(!App.session || !App.session.currentUser) {
App.router.navigate('login', { trigger:true });
return;
}
var user = App.session.currentUser;
//do something with user
}
State is not necessarily evil. What's evil is depending on global state throughout the application, which leads easily to untestable spaghetti code. But if you resolve the state dependency as "high up" in the chain as possible (e.g. in the Router), and pass the values down using constructors and method arguments, you can still keep the testability and side-effectlessness in the rest of the codebase.
Sorry that I don't have a silver bullet for you. There are some libraries, Backbone.StateManager among them, which can help in managing state, transitions and such, but essentially they don't do anything you can't do for yourself.
Use localStorage!
Modify your code to do the following:
// success callback
if (msg.loggedIn) {
console.log("Authenticate successful: " + JSON.stringify(msg));
var data = { user : msg, bob : "bob", trigger:true };
var dataString = JSON.stringify(data);
console.log("Prepared data: " + dataString;
window.localStorage.setItem("userdata",dataString);
App.router.navigate('home',data);
Now whenever you need to check if the user is logged in, do the following:
try {
var userData = window.localStorage.getItem ("userdata");
} catch (e) {
// Do something
}
The try-catch is necessary to make sure that your code doesn't barf if the authentication has never been successful.
Its too late to reply but there is another much better way to do this depending on the router engine you are using and with no local variables.
I would try to give a general example using backbone which applies to all.
Generally your router would be in one place where things are being handled. Assuming its backbone lets have the routes defined as follows.
var router = Backbone.Router.extend({
routingData: {}, // this will have the routing data
routes: {
'': 'home',
'home': 'home',
'login': 'login'
},
navigate: function(url, data) {
this.routingData["data"] = data; // whenever navigation is done, this will be reset
//This is the routing code whichever the f/w may be.
Backbone.Router.prototype.navigate(route, { trigger: true });
},
home: function(data) {
var params = this.routingData["data"]; //retreiving the routing params
console.log(params);
}
})
Now if you want to pass data, you can do
router.navigate(<URL>,<SOME DATA>)