Angular resource - bind to Rails RESTful API - ruby-on-rails

I was looking at AngularJs Resource documentation and it states that default actions
for accessing API are:
{'get': {method:'GET'},
'save': {method:'POST'},
'query': {method:'GET', isArray:true},
'remove': {method:'DELETE'},
'delete': {method:'DELETE'} };
This is a bit different from Rails RESTful API where we have index,show,new,create,edit,update and discard. Is there an "automatic" way
to bind these two without writing the path manually? Thanks!
ps. why remove and delete, where's put for update?

ngResource simply uses different names for usual REST conventions. So for example:
var User = $resource('/user/:userId', {userId:'#id'});
var user = User.get({userId:123}, function() {
// do something with user
});
In this example User.get()sends the following request GET /user/123 which Rails routing logic passes to UserController#show action.
Regarding the update method, you can simply create one yourself:
var User = $resource('/user/:id', {}, {
update: {
method: 'PUT'
}
}

Related

Rails Frontend Trying to save autogenerated data to database without form

I'm new to ruby on rails. I'm trying to save data that is generated by itself to the database. i have looked into and found I was meant to use ajax, however all the videos/forums i have seen are example of ajax that use form and not refreshing page. i want to save data automatically without pressing submit.
Assume that the project is fresh project with postgresql as the database. I have created a database that can hold geo points by using postgis. i have created another page where it has map implemented where i can manully pin location. I want to save the manuuly pinned location to the database.
function onMapClick(e) {
alert("You clicked the map at " + e.latlng);
}
mymap.on('click', onMapClick);
var popup = L.popup();
function onMapClick(e) {
popup
.setLatLng(e.latlng)
.setContent("You clicked the map at " + e.latlng.toString())
.openOn(mymap);
}
mymap.on('click', onMapClick);
The e.latlng holds the geopoint, but i dont know how to save it the database if the user clicks anywhere on the map.
You don't need submit form to use ajax.
Basically what you want is add event listener to the map, and when user click then send ajax request to the controller.
For example, let's say that your map is inside div with id my-map.
If you use jQuery you can write something like this:
$('#my-map').on('click', function() {
# add your logic here
$.ajax({
url: 'your-url',
type: 'POST',
dataType: 'json',
contentType: "application/json; charset=utf-8",
data: JSON.stringify({
'let': data you want to send to backend
})
}
Hope it works!
EDIT:
After I looked your code I found that you can not have jQuery in your project so you can not use jQuery ajax. You need use vanilla javascript. So instead this snippet above, you can write this.
var xhttp = new XMLHttpRequest();
const params = { saving_location: { geoPoints: e.latlng } }
xhttp.onreadystatechange = function() {//Call a function when the state changes.
if(xhttp.readyState == 4 && xhttp.status == 200) {
alert(http.responseText);
}
}
xhttp.open("POST", "/saving_locations", true);
xhttp.setRequestHeader('Content-Type', 'application/json', 'Accept', 'application/json');
xhttp.send(JSON.stringify(params));
Also add protect_from_forgery with: :null_session in your application controller and skip_before_action :verify_authenticity_token in your Saving Location controller.(under before_action).
Here is good blog post why you need this https://blog.nvisium.com/understanding-protectfromforgery
Please notice that you wan't save your database, because your geoPoints type in database is type of point and you send string to rails controller. I never work with points in rails so I can not help you here.(You can always add two columns in db, one for longitude and one for latitude and then store numbers instead point)

EmberJS getting user profile information

In my Rails API I am using JSONAPI structure which Ember expects by default.
I have a Rails route http://localhost:3000/profile which will return the currently logged in user JSON.
How do I make an arbitary request to this /profile endpoint in Emberjs so I can get my logged in user's JSON in my router's model() hook?
I tried following this guide here:
https://guides.emberjs.com/v2.10.0/models/finding-records/
And have this code:
return this.get('store').query('user', {
filter: {
email: 'jim#gmail.com'
}
}).then(function(users) {
return users.get("firstObject");
});
It is returning the incorrect user however. It also seems like it doesn't matter what the value of 'email' is, I can pass it 'mud' and it will return all users in my database.
Is there no way for me to make a simple GET request to /profile in my model() hook of my profile route in Ember?
Update
It has come to my attention that the filter thing in Ember is actually just appending a query parameter onto the end of the request URL.
So having my above filter, it would be like making a request:
GET http://localhost:3000/users?filter['email']=jim#gmail.com
Which doesn't help because my Rails doesn't know anything about filter query parameter.
I was hoping Ember will automatically find the user and do some black magic to filter the user to match email address for me, not me having to manually build extra logic in my Rails API to find a single record.
Hurrmmmmmmm...sure feels like I'm fighting against the conventions of Ember at the moment.
Update
Thanks to Lux, I finally got it working with the following approach:
Step 1 - Generate the User adapter:
ember generate adapter user
Step 2 - write the AJAX request in the queryRecord method override for User adapter
import ApplicationAdapter from './application';
import Ember from 'ember';
export default ApplicationAdapter.extend({
apiManager: Ember.inject.service(),
queryRecord: function(store, type, query) {
if(query.profile) {
return Ember.RSVP.resolve(
Ember.$.ajax({
type: "GET",
url: this.get('apiManager').requestURL('profile'),
dataType: 'json',
headers: {"Authorization": "Bearer " + localStorage.jwt}
})
);
}
}
});
Step 3 - make the model() hook request like so:
import Ember from 'ember';
export default Ember.Route.extend({
model() {
return this.get('store').queryRecord('user', {profile: true});
}
});
Well, query is for server side filtering. If you want it client-side use something like store.findAll('user').then(users => users.findBy('email', 'bla#bla.bla'));.
But this is not what you want. You have your server side filter. It's just under /profile. Not under /user.
However interesting is what /profile actually responds. A single-record-response or a multi-record-response. The best would probably a single-record-response since you only want to return one user. So how can we do this with ember? Well, we use store.queryRecord().
And because ember does not know anything about /profile we have to tell it ember in the user-adapter with something like this:
queryRecord: function(store, type, query) {
if(query.profile) {
return Ember.RSVP.resolve(Ember.$.getJSON('/profile'));
}
}
And then you can just return store.queryRecord('user', { profile: true })

How do I save an Angular form to my ruby on rails backend?

I'm new to Angular. I've tried everything I know how and Google searches have surprisingly few tutorials on this particular question. Here's the last code I tried:
index.html
<form ng-submit="addArticle(articles)">
<input type="text" id="title" ng-model="newPost.title">
<input type="text" id="body" ng-model="newPost.body">
<input type="submit" value="Submit">
</form>
articles controller
app.controller('ArticlesCtrl', function($scope, Article) {
$scope.articles = Article.query();
$scope.newPost = Article.save();
});
articles service (rails backend)
app.factory('Article', function($resource) {
return $resource('http://localhost:3000/articles');
});
I can retrieve data just fine. But I can't submit any new data to the rails backend. On page load, the rails server error is:
Started POST "/articles" for 127.0.0.1 at 2015-02-08 18:26:29 -0800
Processing by ArticlesController#create as HTML
Completed 400 Bad Request in 0ms
ActionController::ParameterMissing (param is missing or the value is empty: article):
app/controllers/articles_controller.rb:57:in `article_params'
app/controllers/articles_controller.rb:21:in `create'
Pressing the submit button does nothing at all. The form basically does not work and the page is looking for a submission as soon as it loads.
I understand what the error says, that it's not receiving the parameters from the form. What I don't understand is what that should look like in my controller and/or form.
What am I doing wrong and how do I fix this?
Angular has a feature called services which acts as a model for the application. It's where I'm communicating with my Rails backend:
services/article.js
app.factory('Article', function($resource) {
return $resource('http://localhost:3000/articles/:id', { id: '#id'},
{
'update': { method: 'PUT'}
});
});
Even though the :id is specified on the end, it works just as well for going straight to the /articles path. The id will only be used where provided.
The rest of the work goes into the controller:
controllers/articles.js
app.controller('NewPostCtrl', function($scope, Article) {
$scope.newPost = new Article();
$scope.save = function() {
Article.save({ article: $scope.article }, function() {
// Optional function. Clear html form, redirect or whatever.
});
};
});
Originally, I assumed that the save() function that's made available through $resources was somewhat automatic. It is, but I was using it wrong. The default save() function can take up to four parameters, but only appears to require the data being passed to the database. Here, it knows to send a POST request to my backend.
views/articles/index.html
<form name="form" ng-submit="save()">
<input type="text" id="title" ng-model="article.title">
<input type="text" id="body" ng-model="article.body">
<input type="submit" value="Submit">
</form>
After getting the service setup properly, the rest was easy. In the controller, it's required to create a new instance of the resource (in this case, a new article). I created a new $scope variable that contains the function which invokes the save method I created in the service.
Keep in mind that the methods created in the service can be named whatever you want. The importance of them is the type of HTTP request being sent. This is especially true for any RESTful app, as the route for GET requests is the same as for POST requests.
Below is the first solution I found. Thanks again for the responses. They were helpful in my experiments to learn how this worked!
Original Solution:
I finally fixed it, so I'll post my particular solution. However, I only went this route through lack of information how to execute this through an angular service. Ideally, a service would handle this kind of http request. Also note that when using $resource in services, it comes with a few functions one of which is save(). However, this also didn't work out for me.
Info on $http: https://docs.angularjs.org/api/ng/service/$http
Info on $resource: https://docs.angularjs.org/api/ngResource/service/$resource
Tutorial on Services and Factories (highly useful): http://viralpatel.net/blogs/angularjs-service-factory-tutorial/
articles.js controller
app.controller('FormCtrl', function($scope, $http) {
$scope.addPost = function() {
$scope.article = {
'article': {
'title' : $scope.article.title,
'body' : $scope.article.body
}
};
// Why can't I use Article.save() method from $resource?
$http({
method: 'POST',
url: 'http://localhost:3000/articles',
data: $scope.article
});
};
});
Since Rails is the backend, sending a POST request to the /articles path invokes the #create method. This was a simpler solution for me to understand than what I was trying before.
To understand using services: the $resource gives you access to the save() function. However, I still haven't demystified how to use it in this scenario. I went with $http because it's function was clear.
Sean Hill has a recommendation which is the second time I've seen today. It may be helpful to anyone else wrestling with this issue. If I come across a solution which uses services, I'll update this.
Thank you all for your help.
I've worked a lot with Angular and Rails, and I highly recommend using AngularJS Rails Resource. It makes working with a Rails backend just that much easier.
https://github.com/FineLinePrototyping/angularjs-rails-resource
You will need to specify this module in your app's dependencies and then you'll need to change your factory to look like this:
app.factory('Article', function(railsResourceFactory) {
return railsResourceFactory({url: '/articles', name: 'article');
});
Basically, based on the error that you are getting, what is happening is that your resource is not creating the correct article parameter. AngularJS Rails Resource does that for you, and it also takes care of other Rails-specific behavior.
Additionally, $scope.newPost should not be Article.save(). You should initialize it with a new resource new Article() instead.
Until your input fields are blank, no value is stored in model and you POST empty article object. You can fix it by creating client side validation or set default empty string value on needed fields before save.
First of all you should create new Article object in scope variable then pass newPost by params or access directly $scope.newPost in addArticle fn:
app.controller('ArticlesCtrl', function($scope, Article) {
$scope.articles = Article.query();
$scope.newPost = new Article();
$scope.addArticle = function(newPost) {
if (newPost.title == null) {
newPost.title = '';
}
// or if you have underscore or lodash:
// lodash.defaults(newPost, { title: '' });
Article.save(newPost);
};
});
If you want use CRUD operations you should setup resources like below:
$resource('/articles/:id.json', { id: '#id' }, {
update: {
method: 'PUT'
}
});

Ember.js route for the current user's /settings page

A common pattern for a user's settings page would be for it to live at /settings.
In my Rails app, I'm accomplishing this on the API side by mapping get 'settings' to Settings#show and looking for the current_user's settings.
But on the Ember side, I'm stumped. There's no ID to use for the GET request, so I can't use the typical pattern of this.store.find('setting', params.id) within my route.
What's the "Ember way" of handling this sort of use case?
This has been discussed here: http://discuss.emberjs.com/t/fetching-single-records/529/3
The issue with loading a single record not based on an ID, is that you need to get back a DS.Model object as a promise. If you get back a record that's already in the client's memory you would now have two different objects representing the same record (type and id combination). Take this example:
var user123 = App.User.find(123);
var currentUser = App.findByUrl('/users/current'); //This is an imaginary method, i.e. Ember Data don't support it
notEqual(user123, currentUser, "The user objects can't be the same cause we don't know what the current user is yet");
Now we get this response from the server:
{
"user": {
"id": 123,
"name": "Mufasa"
}
}
Now currentUser and user123 both have id 123, but they are essentially different objects = very bad. This is why this approach wouldn't work.
Instead you will want to load a record array of users, listen for it to load, and then take the firstObject from the loaded records. Like this:
var users = App.User.find({ is_current: true });
users.one('didLoad', function() {
App.set('currentUser', users.get('firstObject');
});
$.ajax({
type: 'GET',
url: '/users/current',
success: function(payload) {
var store = this.store;
var userReference = store.load(App.User, payload.user);
App.set('currentUser', store.recordForReference(userReference));
}.bind(this)
});

Using the Symfony admin generator to let a user manage a subset of record

My first post here, hopefully It will be right! =)
I am creating a site to manage web application development using symfony 1.4 and doctrine.
My records consist for this problem of Project and ProjectFeatures
Now what I want to do is use the admin generator to let users manage the features for one project thru a link constraining all the returned features by project_id, that would look like: http://mysite/member/project/:project_id/features
in my routing.yml configuration, I have:
member_project_feature:
class: sfDoctrineRouteCollection
options:
model: ProjectFeature
module: memberProjectFeature
prefix_path: /member/project/:project_id/features
with_show: true
column: id
with_wildcard_routes: true
project_id is an existing column in the model ProjectFeature,
I will use a custom query to retrieve features only by that project_id.
Now I can generate a url to link to that admin generator module without error using:
url_for('member_project_feature', array('project_id' => $project['id']))
And the routing system does recognise the url:
May 04 14:30:59 symfony [info] {sfPatternRouting} Match route "member_project_feature" (/member/project/:project_id/features.:sf_format) for /member/project/1/features with parameters array ( 'module' => 'memberProjectFeature', 'action' => 'index', 'sf_format' => 'html', 'project_id' => '1',)
But the admin generator can't generate it's links inside it's templates with that prefix_path and returns error InvalidArgumentException with message The "/member/project/:project_id/features/:action/action.:sf_format" route has some missing mandatory parameters (:project_id).
Any idea?
Well I found my answer at this url: http://www.blogs.uni-osnabrueck.de/rotapken/?s=symfony
But I will give it here and shorten it because, stackoverflow is awesome and it should be there for a long time =)
1st - The routing configuration I used in my question is valid.
2nd - You need to add a method in the action file generated by the admin
public function execute($sfRequest)
{
// taken from http://www.blogs.uni-osnabrueck.de/rotapken/?s=symfony
$this->forward404Unless(
$project_id = $sfRequest->getUrlParameter('project_id'));
$this->forward404Unless(
$this->project = Doctrine::getTable('ttcWebProject')->find($project_id));
$this->getContext()->getRouting()
->setDefaultParameter('project_id', $project_id);
if ($id = $sfRequest->getUrlParameter('id'))
{
$this->getContext()->getRouting()->setDefaultParameter('id', $id);
}
$result = parent::execute($sfRequest);
return $result;
}
At this point the url gets generated correctly but here is the last step to get to the end result you most probably want to achieve:
3rd - To get the list by project_id I can either provide a table method in the generator.yml, a default value to the getFilterDefaults or this method in the action file:
protected function buildQuery ()
{
$q = parent::buildQuery();
$rootAlias = $q->getRootAlias();
$q->andWhere("{$rootAlias}.project_id = ?",
$this->getRequest()->getUrlParameter('project_id'));
return $q;
}
I'm not 100% certain about what you're trying to do here, but it sounds like you need the ProjectFeature::toParams method return the project_id.

Resources