creating a record with Ember.js & Ember-data & Rails and handling list of records - ruby-on-rails

I'm building an app which has layout like below.
I want to create a new post so I pressed 'new post button' and it took me to 'posts/new' route.
My PostsNewRoute is like below (I followed the method described here)
App.PostsNewRoute = Ember.Route.extend({
model: function() {
// create a separate transaction,
var transaction = this.get('store').transaction();
// create a record using that transaction
var post = transaction.createRecord(App.Post, {
title: 'default placeholder title',
body: 'default placeholder body'
});
return post;
}
});
It immediately create a new record, updates the list of the posts, and displays forms for new post.
(source: sunshineunderground.kr)
Now I have two problems.
One is the order of post list.
I expected new post will be on top of the list, but It's on the bottom of the list.
I'm using rails as my backend and I set the Post model order to
default_scope order('created_at DESC')
so old Post sits below within existing posts. but newly created one is not. (which isn't commited to backend yet)
Another is When I click created post in the list
I can click my newly created post in the posts list. and It took me to a post page with URL
/posts/null
and It's very weird behavior that I must prevent.
I think there will be two solutions.
When I click 'new post button' create a record and commit to server immediately and when server successfully saved my new record then refresh the posts list and enter the edit mode of newly created post.
or initially set Route's model to null and create a record when I click 'submit' button in a PostsNewView.
or show show only posts whose attribute is
'isNew' = false, 'isDirty' = false,
in the list..
But sadly, I don't know where to start either way...
for solution 1, I totally get lost.
for solution 2, I don't know how to bind datas in input forms with not yet existing model.
for solution 3, I totally get lost.
Please help me! which will be ember's intended way?
(I heared that Ember is intended to use same solution for every developer)
Update
now I'm using solution 3 and still having ordering issue. Here is my Posts template code.
<div class="tools">
{{#linkTo posts.new }}new post button{{/linkTo}}
</div>
<ul class="post-list">
{{#each post in filteredContent}}
<li>
{{#linkTo post post }}
<h3>{{ post.title }}</h3>
<p class="date">2013/01/13</p>
<div class="arrow"></div>
{{/linkTo}}
</li>
{{/each}}
</ul>
{{outlet}}
Update
I solved this problem by filtering 'arrangedContent' ,not 'content'
App.PostsController = Ember.ArrayController.extend({
sortProperties: ['id'],
sortAscending: false,
filteredContent: (function() {
var content = this.get('arrangedContent');
return content.filter(function(item, index) {
return !(item.get('isDirty'));
});
}).property('content.#each')
});

We use a variation of solution 3 in several places on our app. IMHO it's the cleanest of the 3 as you don't have to worry about setup/teardown on the server side This is how we implement it:
App.PostsController = Ember.ArrayController.extend({
sortProperties: ['id'],
sortAscending: true,
filteredContent: (function() {
return this.get('content').filter(function(item, index) {
return !(item.get('isDirty'));
});
}).property('content.#each')
});
Then in your Posts template you loop through controller.filteredContent instead of controller.content.
For solution 1, there are many possibilities. You could define the following event:
createPost: function() {
var post,
_this = this;
post = App.Post.createRecord({});
post.one('didCreate', function() {
return Ember.run.next(_this, function() {
return this.transitionTo("posts.edit", post);
});
});
return post.get("store").commit();
}
It creates the post then sets up a promise that will be executed once "didCreate" fires on the post. This promise transitions to the post's route only after it has come back from the server, so it will have the correct ID.

Indeed, very nice write up. Thx for that.
Doesn't your filteredContent have to use the isNew state i.o. isDirty, otherwise the Post that is being edited will not be visible.
In either case, the filteredContent property does not work in my case. I also noticed that, since I use an image as part of every element, all images will be refreshed when filteredContent is changed. This means that I see a request for every image.
I use a slightly different approach. I loop through the content and decide whether or not to display the Post in the template:
# posts.handlebars
<ul class='posts'>
{{#each controller}}
{{#unless isNew}}
<li>
<h3>{{#linkTo post this}}{{title}}{{/linkTo}}</h3>
<img {{bindAttr src="imageUrl"}}/>
<a {{action deletePost}} class="delete-post tiny button">Delete</a>
</li>
{{/unless}}
{{/each}}
</ul>
This will only show the Post object after it is saved. The url in the H3-tag also contain the id of the newly created object i.o. posts/null.
One other thing I noticed in your question: instead of passing default values to createRecord, you can use the defaultValues property on the model itself:
So, instead of:
# App.PostsNewRoute
var post = transaction.createRecord(App.Post, {
title: 'default placeholder title',
body: 'default placeholder body'
});
you can do this:
# App.Post
App.Post = DS.Model.extend({
title: DS.attr('string', {
defaultValue: "default placeholder title"
}),
body: DS.attr('string', {
defaultValue: "default placeholder body"
})
});
# App.PostsNewRoute
var post = transaction.createRecord(App.Post);

I actually wrote a function to reverse the content array a while back:
http://andymatthews.net/read/2012/03/20/Reversing-the-output-of-an-Ember.js-content-array
That's a pretty old article, almost a year old, so it probably won't work as is, but the framework is there...
You can see it in action in this mini-app I wrote:
http://andymatthews.net/code/emberTweets/
Search for users in the input field at the top and watch the left side as they appear in order from newest to oldest (instead of oldest to newest).

Related

My Stimulus checkbox script in Rails 7 doesn't work

I'm learning stimulus and trying to get add a checkbox feature where you can mark an order as complete from the show page without using a form. I followed this tutorial, but am not getting the correct results. The checkbox does nothing when clicked and unchecks when refreshed; however if I manually set the complete attribute to true, the checkbox is automatically checked when loading the page, as it should.
I have a model "Order" with a boolean attribute "complete". Here's my show.html.erb section
<tr data-controller="todo" data-todo-update-url="<%= order_path(#order.id) %>">
<td>
<div>
<input type="checkbox"
data-action="todo#toggle"
data-target="todo.completed"
<% if #order.complete %> checked <% end %> >
</div>
</td>
</tr>
Here's my stimulus todo_controller
import { Controller } from "#hotwired/stimulus"
export default class extends Controller {
static targets = [ "completed" ]
toggle(event) {
// Inside the toggle(event) function, let’s start by getting the value of the checkbox,
// and put it into a FormData object
let formData = new FormData()
formData.append("#order[complete]", this.completedTarget.completed);
// Let’s post that data to the "update-url" value we set on the Todo row.
// We’ll set the method to PATCH so that it gets routed to our todo#update on our controller.
// The credentials and headers included ensure we send the session cookie and the CSRF protection token and
// and prevent an ActionController::InvalidAuthenticityToken error.
fetch(this.data.get("update-url"), {
body: formData,
method: 'PATCH',
dataType: 'script',
credentials: "include",
headers: {
"X-CSRF-Token": getMetaValue("csrf-token")
}
})
// We can take the Response object and verify that our request was successful.
// If there was an error, we’ll revert the checkbox change.
.then(function(response) {
if (response.status != 204) {
event.target.complete = !event.target.complete
}
})
}
}
Can someone tell me where my code is going wrong?
This is more a long comment than a solution but few things I see :
You create an empty form and append a single input with name
"#order[complete]" though your Stimulus controller is Javascript
and has no knowledge of # the such way you use in Ruby. Also params
names are usually model[field] then I think you don't need the #.
"order[complete]" should be fine.
Also you grab the value from a specific target for the aforementionned value with this.completedTarget.completed. Should you not rather pick the value of the input field ? and rather grab this.completedTarget.value or maybe the checked status this.completedTarget.checked
You are getting the URL to your fetch from a data attribute. I am not a stimulus expert but it doesn't look like anything Stimulus related. As of now you have written it this.data.get("update-url") but in regular javascript, something like this.element.dataset.todoUpdateUrl should work.
And just to be sure there is no confusion to about where you will
call this , just declare it at the top of your Stimulus methd like
this : var backUrl = this.element.dataset.todoUpdateUrl. And fill the url to your fetch as just fetch(backUrl,...
you pass the formData directly to your fetch body. If this doesn't work, try to stringify it and extract the entries like : JSON.stringify(Object.fromEntries(formData)). Also I am not too sure about the dataType: 'script', you may just omit that alltogether.
There may be other problems that I don't see. Also when you are dealing with JS, don't only look to your Rails console, especially if nothing hits the backend. Open the developper / inspect tool in your browser and monitor the console there, you should see all the XHR (async) requests made to your app.
If nothing happens, then your fetch is not firing and there need to be more investigations made..

JQuery Mobile - How can I pass data context from a listview item to another page?

I'm generating a listview of "products" dynamically with some JSON data I'm getting using ajax request. When an user clicks on a item I want to take them to another page where all of the products details would be displayed.
$(document).delegate("#store", "pageinit", function() {
getProducts('',
function(data){
let products = data.products;
storeProducts(products);
$(".product-list").html('');
$.each(products, function(i, item){
$(".product-list").append(`
<li>
<a href="#product">
<img src="${item.picture}" alt="">
<h2>${item.prod_name}</h2>
<p>${item.prod_desc}</p>
<small>Price: <strong>€${item.unit_price}</strong></small>
</a>
</li>
`).listview("refresh");
});
},
function(e){
console.log(e);
},
function(data){
console.log('always');
});
});
Now the problem is that I don't see how to pass the data context of a list item to another page (#product) in order to populate it with the corresponding information. I have seen a few approaches to similar use cases but they seem a bit ugly to me.
I believe this is a very basic feature for a web/mobile application nowadays; However JQuery Mobile doesn't seem to make it easy for a developer to implement such feature. Any plugin/library I can use to implement this?
Thanks in advance.
As you are creating the list items dynamically, this could be easily done by adding your own custom data attribute, like this:
'<li data-id="+${item.prod_id}+">'
Then, you can get the product context this way:
$(document).on("vclick", ".product-list li>a", function() {
var id = $(this).data("id");
...get the product context by id
});
Here is an example with working demo: https://stackoverflow.com/a/43915438/4845566
BTW, just two small hints aside :
if you create the html token by using [].join("") you will avoid the extra spaces/tabs in markup.
You can also refresh the listview just one time, out of the products loop.

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'
}
});

Updating value in DOM from Javascript with Ember.js

I am new to Ember.js. I have the following experiment in my HTML:
<p>Username: {{view Ember.TextField valueBinding="MyApp.loginController.bound.username" id="view-login-username" placeholder="Your username"}}</p>
The username field is: {{MyApp.loginController.bound.username}}
{{#view Ember.Button
classBinding="isActive"
target="MyApp.loginController"
action="submitLogin"}}
Login {{/view}}
And I have the following in my Javascript:
MyApp = SC.Application.create();
MyApp.loginController = Ember.Object.create({
bound : { username:'12345' },
submitLogin: function() {
MyApp.loginController.bound.username = "abcdef";
alert(MyApp.loginController.bound.username);
}
});
When I push my 'login' button, the change to "abcdef" does not appear in the DOM nor in the alert() immediately following the assignment. The alert appears but still displays "12345".
What am I missing?
Thank you!
The following should probably be put in 120pt font at the top of the Ember homepage:
When using properties on Ember objects, ALWAYS use Ember's universal accessors (get, getPath, set, and setPath)!
Change MyApp.loginController.bound.username = "abcdef"; to MyApp.loginController.setPath('bound.username', "abcdef");

jQuery Dialog posting of form fields

I'm trying to do some data entry via a jQuery modal Dialog. I was hoping to use something like the following to gather up my data for posting.
data = $('#myDialog').serialize();
However this results in nothing. If I reference just the containing form instead myDialog then I get all the fields on the page except those within my dialog.
What's the best way to gather up form fields within a dialog for an AJAX submission?
The reason this is happening is that dialog is actually removing your elements and adding them at root level in the document body. This is done so that the dialog script can be confident in its positioning (to be sure that the data being dialog'd isn't contained, say, in a relatively positioned element). This means that your fields are in fact no longer contained in your form.
You can still get their values through accessing the individual fields by id (or anything like it), but if you want to use a handy serialize function, you're going to need to have a form within the dialog.
I've just run into exactly the same problem and since I had too many fields in my dialog to reference them individually, what I did was wrap the dialog into a temporary form, serialize it and append the result to my original form's serialized data before doing the ajax call:
function getDialogData(dialogId) {
var tempForm = document.createElement("form");
tempForm.id = "tempForm";
tempForm.innerHTML = $(dialogId).html();
document.appendChild(tempForm);
var dialogData = $("#tempForm").serialize();
document.removeChild(tempForm);
return dialogData;
}
function submitForm() {
var data = $("#MyForm").serialize();
var dialogData = getDialogData("#MyDialog");
data += "&" + dialogData;
$.ajax({
url: "MyPage.aspx",
type: "POST",
data: data,
dataType: "html",
success: function(html) {
MyCallback(html);
}
});
}
Form element inside dialog is removed from form and moved to the end of the body. You need something like this.
$("#dialog_id").dialog().parent().appendTo($("#form_id"));
jQuery("#test").dialog({
autoResize:true,
width:500,
height:600,
modal: true,
bgiframe: true,
}).parent().appendTo("form");
This works like charm

Resources