ember.js - unable to commit form data - ember.js

I'm relatively new to ember.js and still having a painful time learning it. I am using the ember-rails gem, and Rails as a JSON API.
First I just wanted to build something that would display a list of records (in this case, emails) with the ability to add new ones. I have the UI setup and functioning, and ember is pulling the list of emails from the Rails API. I can click an email title and it takes me to the full email etc.
My problem is adding new records. I have a Compose Email form set up, but I still have not managed to add a new record, after several days of re-arranging code in routers and controllers. Every example I have found seems to suggest a different way of juggling transactions, createRecords, .get('store'), .get('content') and then committing the details through an action of some sort. None of them have worked for me. Here's what feels like the closest I've got:
Store
App.Adapter = DS.RESTAdapter.extend
serializer: DS.RESTSerializer.extend
primaryKey: (type) -> '_id'
App.Store = DS.Store.extend
revision: 11
adapter: 'App.Adapter'
Email model
App.Email = DS.Model.extend
from: DS.attr('string')
to: DS.attr('string')
subject: DS.attr('string')
body: DS.attr('string')
created_at: DS.attr('string')
Compose Route
App.ComposeRoute = Ember.Route.extend
model: () ->
transaction = this.get('store').transaction()
email = transaction.createRecord(App.Email, {})
return email
events: {
save: (email) ->
email.get('store').transaction()
}
Compose Template (trimmed of some HTML)
<label class="control-label" for="to">To</label>
{{view Ember.TextField valueBinding="to" id="to" placeholder="Your friend" required="true"}}
<label class="control-label" for="subject">Subject</label>
{{view Ember.TextField valueBinding="subject" id="subject" placeholder="What's it about?" required="true"}}
<label class="control-label" for="notes">Your message</label>
{{view Ember.TextArea valueBinding="body" id="body" placeholder="Your message"}}
<button class="btn btn-primary btn-large" {{action save this}}>Send email</button>
Rails emails_controller.rb
class EmailsController < ApplicationController
# GET /emails.json
def index
render json: Email.all
end
# GET /emails/1.json
def show
email = Email.find(params[:id])
render json: email
end
# POST /emails.json
def create
email = Email.new(params[:email])
email.save
render json: email, status: :created
end
So with the above code I fill in the textboxes, click the submit button, and nothing seems to happen. However, if I go back to my Inbox page I can see the record I just added (woohoo!) but it's not persisted through Rails (hrmmm) - as soon as I refresh it's gone. Looking at the Rails logs, ember doesn't seem to be sending Rails anything.
I feel like I am going around in circles just trying different lines from various blog posts without really understanding the basic process. How should I be doing this? I've read through the Ember docs and while they explain the basic concepts, there are very few working examples using the RESTAdapter.

You never called commit anywhere in your code. Keep in mind that store.transaction() only gives you an instance of transaction, but you still have to commit (or rollback in Route#deactivate, if the user leaves that route). You might want to do your Route somewhat like this:
App.ComposeRoute = Ember.Route.extend
model: ->
transaction = this.get('store').transaction()
transaction.createRecord(App.Email, {})
deactivate: ->
## not super sure about modelFor in this case, but I think it should work
record = #modelFor('email')
## There's more to check here, like is record 'inflight' and all
## but you get the idea.
if ((record.get('isNew')) or (record.get('isDirty')))
#get('store.defaultTransaction').rollback()
events: {
save: (email) ->
#get('store.defaultTransaction').commit()
## or #get('store').commit()
}

The way you save your record is still not correct.
You create a new transaction for your email, it is this transaction you want to commit, not the store's one.
App.ComposeRoute = Ember.Route.extend
model: () ->
transaction = this.get('store').transaction()
email = transaction.createRecord(App.Email, {})
return email
events: {
save: (email) ->
// email.get('store').transaction() //This is only creating a new transaction
// #get('store.defaultTransaction').commit() // this is committing the store's transaction
email.get('transaction').commit()
}

Related

Ember - #each pass the instance of the model

For a small webapp I'm trying to do the following:
I have a list of objects (achievement-model)that's being served through a json api
Router
export default Ember.Route.extend({
model:function(){
return this.store.find('achievement');
});
});
Model
export default DS.Model.extend({
name: DS.attr('string'),
description: DS.attr('string'),
});
Template
{{#each a in model}}
<div>
<h4>{{a.name}}</h4>
<p>{{a.description}}</p>
<button {{action 'addThis'}}/>
</div>
{{/each}}
The setup of the app is that there is a list of achievements. I want one list of achievements in a database. Every user that logs in can add with the button his own achievements to his profile. If a user logs in he should see the list of all the achievements but the one he already added to his profile should have a green background color and the button removed. I know this can be done with if-statements etc.
The problem however is, how do i pass the specific model to the controller so i can log this to the userprofile? I tried the following:
<button {{action 'addThis' a}}/>
and then in the controller
actions:
addThis: function(obj){
console.log(obj);
});
which logs the object, but somehow I can't acces it to get let's say the name or id to copy it to the user-profile.
I also don't know if this is the best approach for what I'm trying to achieve?
Edit
I think this has something to do with promises. I can see the data is logged in the above console.log. I just don't know how to target it. it's wrapped in _data. I tried the afterModel to wait untill everything's loaded, but that doesn't seem to work.
What you could is to use an ItemController, e.g. which handles each item in the ArrayController,
e.g.
{{#each a in model itemController="achievement"}}
<div>
<h4>{{a.name}}</h4>
<p>{{a.description</p>
<button {{action 'addThis'}}/>
</div>
{{/each}}
Since the itemController is "achievement", by naming convention, the controller becomes
App.AchievementController = Ember.ObjectController.extend({
init: function() {
var name = this.get('name');
var description = this.get('description');
}
});

Creating a new record not pulling data from template fields

I am attempting to create a new record, however none of the data from the fields is being passed automatically, as I expected Ember to (from what I've read).
My template:
<form {{action save content on="submit"}}>
{{input value=name}}
<button type="submit"}}>Next</a>
From what I've read content is an alias for model and interchanging these makes no difference.
My route:
App.CampaignsNewRoute = Ember.Route.extend({
actions: {
save: function(campaign) {
console.log(campaign.name);
}
},
model: function(controller) {
return this.store.createRecord('campaign');
}
});
And my controller:
App.CampaignsNewController = Ember.ObjectController.extend({
pageTitle: 'New Campaign Setup'
});
When I hit 'Next' it logs undefined. Logging just the campaign shows it's an Ember model, but without the name attribute. name is defined on the campaign model. Setting the input to {{input value=content.name}} places the name attribute within the model returned, but it's still undefined. Am I missing anything in this process? The EmberJS site doesn't show how to do this, from what I can find.
--
As a side note: I was originally using App.CampaignsNewController = Ember.Controller.extend as my model was returning a hash of promises, one of which is an array and Ember didn't like me using either array or object controller. I simplified it to the above to verify it wasn't that which was causing the issue. So any solution taking this into account would be wonderful.
Edit: I can access the template fields by doing this.get('controller').get('name') but surely that is not necessary? Changing my controller to a Ember.Controller.extend also stops that from working, would love to know why. Clarification on best practice here would still be wonderful!
Edit2: this.get('controller.content').get('name') works if the controller is simply an Ember.Controller as opposed to Ember.ObjectController and the template has {{input value=content.name}}. I'll work with but hopefully someone can clarify this is the correct way.
ObjectController is the way to go here. You would have it backed by one particular model, your new model, and you would add additional properties to the controller for use in the template.
Code
App.IndexRoute = Ember.Route.extend({
actions: {
save: function(campaign) {
console.log(campaign.get('color'));
}
},
model: function() {
return Ember.RSVP.hash({
record: this.store.createRecord('color'),
all: this.store.find('color')
});
},
setupController: function(controller, model){
this._super(controller, model.record);
controller.set('allColors', model.all);
}
});
App.IndexController = Em.ObjectController.extend({
});
Template
In the template any time you want to access anything on the model backing the template, you can just access it as if the model is the current scope.
{{name}}
if you want to access any of the properties that exist on the controller you would use the property name that it is on the controller.
{{allColors.length}}
Here's an example:
<form {{action save model on="submit"}}>
Color:{{input value=color}}<br/>
<button type="submit">Next</button>
</form>
<ul>
{{#each item in allColors}}
{{#unless item.isNew}}
<li>{{item.color}}</li>
{{/unless}}
{{/each}}
</ul>
One last tip, always use getters and setters ;)
Ember Data hides the properties, they don't live right on the object, so campaign.name will return undefined forever and ever. If you do campaign.get('name') you'll get a real response.
With the example: http://emberjs.jsbin.com/OxIDiVU/792/edit

Adding needs: ['ApplicationController'] makes my view dissapear

My Ember.js app is set up roughly like this:
Router:
App.Router.map ->
#resource('site', { path: '/' }, ->
#resource('dashboard')
#resource('account')
#resource('pages', { path: '/:page_slug'}))
Routes:
App.ApplicationRoute = Ember.Route.extend
model: ->
return App.Site.find(1)
App.PagesRoute = Ember.Route.extend
model: (params)->
return App.Page.find(params.page_slug)
EDIT:
Controller (JS not Coffee):
App.PagesController = Ember.ObjectController.extend({
needs: 'ApplicationController',
...
});
I have an ApplicationController and a PagesController. When I'm on a page, I want to call an action to delete the current page. If I place it in the PagesController it works kind of ok, but a navigation menu with a list of pages in the ApplicationView doesn't get updated until I refresh the page. So... I assume I need to place the action in the ApplicationController and add needs: ['ApplicationController'] to my PagesController.
However, when I do that, everything in my Pages template disappears.
I have an {{outlet}} in my Application template and another on in the Site template which ultimately displays the Pages template. Yeah, complicated, I know and there's probably a better way to do it, so any suggestions would be greatly appreciated. Oh and BTW, I'm a real Ember.js newb, so examples need to be spelled out explicitly. (Drawing them with pretty colors in crayon would actually be ideal.)
Thanks in advance for any help.
Here's the refresh issue. In the root resource site I'm loading in one Site model which the Rails backend returns using the url being sent in. Eventually, this app will be used with multiple domains, and each domain will be served it's own website (roughly based on the WP-Multisite concept). Then in the pages route, I'm loading one page of the site based on its slug attribute. That's all working fine. So, now I want to allow the user to be able to add and remove pages as they want.
So, the issue is this. When I delete a page at the PagesController level, the page deletes just fine, but the ApplicationController doesn't get notified. Namely, my nav menu:
<ul class="left">
{{#each page in page}}
<li>{{#link-to 'pages' page.slug}}{{page.menu_name}}{{/link-to}}</li>
{{/each}}
<li id="add-page-button"><a data-tooltip title="Click here to add a new page to your site." href="#" data-reveal-id="addPageModal" data-reveal>+</a></li>
</ul>
doesn't get notified that a page is missing and so it doesn't update the nav menu by removing that page from the list. Adding works fine, the nav list is updated when a new page is added, but I'm doing that at the ApplicationController level like so:
addNewPage: ->
# Get the site
site = #get('model')
# Get all the pages associated with the site
pages = site.get('page')
title = this.get('newTitle')
if (!title.trim())
return
slug = this.get('newSlug')
if (!slug.trim())
return
menu_name = this.get('newMenuName')
if (!menu_name.trim())
return
# Create a new page passing in our title, slug and menu_name
pages.create({title: title, slug: slug, menu_name: menu_name})
# Save the pages
pages.save()
if (pages.isError)
console.log(pages.errors)
else
#set('newTitle', '')
#set('newSlug', '')
#set('newMenuName', '')
$('#addPageModal').foundation('reveal', 'close')
And this is my deletePage code on the PagesController (sorry, I've got a mix of JS and CoffeeScript):
deletePage: function (slug) {
var page = this.get('model');
this.get('ApplicationController').deletePage(page);
this.toggleProperty('isEditingTitle');
this.toggleProperty('isShowingDeleteConfirmation');
this.transitionToRoute('site.index');
}
When I try this, Ember rather helpfully lets me know I need to add needs but again, when I do that, my Pages template is blank.
Oh yes, I have even tried setting the target attribute on the button calling the deletePage action.
I realize what I need to do is somehow access the Site model in my PagesController, but how should I go about that.
This post has taken on epic proportions. Sorry about that. Thanks again for any help.
needs doesn't do anything on the route, if you want to access a controller from a route you can just use this.controllerFor('application')
Additionally when using needs in a controller definition it should be like this needs: 'application' or needs: ['foo', 'application']...
Okay, all, I figured it out. For posterity's sake (and others who go looking for this). Here's how to delete an object in a model that has a belongsTo association.
ApplicationController:
deletePage: function () {
# First we find the Site which all pages belongTo
# Just ignore the hard-coded '1' it's something I have to do to get it to work
site = App.Site.find(1);
# Then we get the current model, i.e., the 'page' we're on and about to delete
page = this.get('model');
# Then, we get all 'pages'
pages = site.get('page');
# We remove the object from the page which updates the navigation menu
pages.removeObject(page);
# We delete the record and save the 'pages' model
page.deleteRecord();
pages.save();
}
Pages template:
{{#if isShowingDeleteConfirmation}}
<div class="large-7 large-offset-5">
<label class="inline left">Are you sure? (Can't be undone)</label>
<button {{action 'deletePage'}} class='button tiny alert'>Yes</button>
<button {{action 'cancelDeletePage'}} class='button tiny'>Cancel</button>
</div>
{{else}}
<div class="large-2 right">
<button {{action 'showDeleteConfirmation'}} class='button tiny alert'>Delete Page</button>
</div>
{{/if}}
I can show you the attributes and logic behind the if...else statement if you need me to.
Basically, the user clicks on the 'Delete' button, showing the other buttons 'yes' and 'cancel'. Once 'yes' is clicked, the deletePage action gets called.
I found very little documentation on the deleteRecord method and basically had to piece this all together myself. Perhaps there's a better way and this can be refactored. Just let me know in the comments.

Ember-data transactions and resubmitting forms

I'm using ember(+data) and have built a simple signup form, but I'm having trouble getting the form (and ember-data transaction) to work properly after a server-side validation error.
I'm following https://github.com/dgeb/ember_data_example as a guide for how to use transactions, and it mostly works for the Happy Path, but after a server-side validation error, I can't get ember-data to resubmit the API request when I click Submit.
I did some digging around, and I think I'm missing some step related to resetting the transaction after the model becomes invalid or something like that...
Github and App
You can try the app and reproduce the problem at http://emb.herokuapp.com/
You can also examine the full source code at https://github.com/justinfaulkner/ember-data-resubmit
Repro
You can trigger a fake server-side error by completing the form in my app using an e-mail address at #example.com. The Rails API will respond 422 and an errors object keyed to the username/email field.
The field should be highlighted in red with an error -- edit the email address to something that should be valid, and click Submit again. With developer tools open in chrome, I don't see ember-data sending an http request with the click (but a console.log in the controller indicates that the click is indeed being received).
What should happen
After modifying the field with the error, ember-data (I think) changes the model's from invalid to uncommitted so that it gets submitted the next time commit is called. When I click Submit, ember-data should send the HTTP request to my api, and ember should transition to the "Congrats!" page.
Instead, however, the form just sits there. No http request. No transition to "Congrats!".
Here's a screenshot after I clicked Submit a few (18) times after having updated the inputs:
Snippets
Here are some snippets of my ember app:
IndexRoute
My route, which uses startEditing like ember_data_example does:
App.IndexRoute = Ember.Route.extend({
model: function() {
return null;
},
setupController: function(controller) {
controller.startEditing();
},
deactivate: function() {
this.controllerFor('index').stopEditing();
}
});
IndexController
My IndexController is modeled after ember_data_example's ContactsNewController
App.IndexController = Ember.ObjectController.extend({
startEditing: function() {
this.transaction = this.get('store').transaction();
this.set('content', this.transaction.createRecord(App.User));
},
submit: function(user){
console.log("submitting!");
this.transaction.commit();
this.transaction = null;
},
_transitionOnSuccess: function(stuff) {
if (this.get('content.id') && this.get('content.id').length > 0) {
console.log("_transitionOnSuccess");
this.transitionToRoute('success');
}
}.observes('content.id'),
stopEditing: function() {
if (this.transaction) {
this.transaction.rollback();
this.transaction = null;
}
}
});
User
Here's my model. I'm using ember-validations.
App.User = DS.Model.extend(Ember.Validations.Mixin);
App.User.reopen({
username: DS.attr('string'),
password: DS.attr('string'),
profile: DS.belongsTo('App.Profile'),
validations: {
username: {
presence: true
},
password: {
presence: true,
length: { minimum: 6 }
}
}
});
index.hbs
And here's the form from my handlebars template. I'm using ember-easyForm.
{{#formFor controller}}
<div class="row">
<div class="large-12 columns">
{{input username placeholder="Email address"}}
</div>
</div>
<div class="row">
<div class="large-12 columns">
{{input password placeholder="Password"}}
</div>
</div>
{{submit "Sign Up" class="button"}}
{{/formFor}}
In case the library authors see this post, I have made a couple local modifications to the app's copy of ember-easyForm and ember-validations in this commit: https://github.com/justinfaulkner/dockyard-example/commit/f618b0e4fb72314d56bb3a9d95e1325925ba6ad0 . I don't think my changes are causing my problem, though.
becameInvalid and rollback?
I did run across a similar-looking question here: Ember Data and dirty records but when I added a User.becameInvalid to rollback the transaction, this caused the form to empty when trying to re-submit (and still no success having ember-data resubmit the http request).
Thanks!
I'm sure I'm following ember_data_example poorly (or failing to extend it to my use-case) or am making some simple mistake somewhere...
Thanks in advance.
Edit Apr 5
So far, I can find at least two main problems:
this.transaction = null; Do I need this? What should I do instead?
I tried removing this.transaction = null; and ember-data tries to actually commit now (but still won't submit the ajax request). Now, when I type in the invalid field, I can see ember-data try to update the record back to uncommitted/created... but it does it in a different transaction.
I pushed up a no-null branch to the repo that has some console.logs in ember-data that prints out what the transaction ID is... here's a snapshot of my console:
recordBecameDirty is called when I type in the field. Ember-data updates the record to be ready to be committable again. But, it's doing it in some other transaction (458)
But my submit button is tied to the original transaction (316), and there's no records ready to commit in that transaction.... hmm....
This is a known issue with ember data.
See: https://github.com/emberjs/data/pull/539
The simple change in Ember Data in that commit
https://github.com/Cyril-sf/data/commit/fe9c63beb02e9f16051e59a9f7c0a918152a0231
should solve your problem.

Has no method 'addArrayObserver' during {{#linkTo}} click

Hey not sure if anyone can help me, but I have been struggling with this error for a long time:
"Uncaught TypeError: Object <App.AssetType:ember408:2> has no method 'addArrayObserver'"
Here is the template with the {{#linkTo}}'s that produce this error when clicked
<div class="row">
<div class="twelve columns">
<h2>{{title}} - Assets</h2>
</div>
</div>
<div class="row">
<div class="three columns">
<ul>
{{#each assetTypes}}
{{#linkTo 'product.filter' this}}{{title}}{{/linkTo}}
{{/each}}
</ul>
</div>
<div class="nine columns">
{{outlet}}
</div>
</div>
and the Application code
window.App = Ember.Application.create
rootElement: '.solution_products_documents'
App.ApplicationRoute = Ember.Route.extend
model: ->
App.Product.find()
App.ApplicationController = Ember.ArrayController.extend
sortProperties: ['title']
App.ProductRoute = Ember.Route.extend
model: (params) ->
App.Product.find params.product_id
setupController: (controller, model) ->
controller.set 'documents', model.get 'document_ids'
App.ProductController = Ember.ObjectController.extend
assetTypes: (->
docs = #get('documents')
docs.getEach 'asset_type_id'
).property('documents')
App.ProductFilterRoute = Ember.Route.extend
model: (params) ->
type = App.AssetType.find params.asset_type_id
product = this.modelFor 'product'
docs = product.get 'document_ids'
model = docs.filterProperty 'asset_type_id', type
App.ProductFilterController = Ember.ArrayController.extend()
App.Router.map ->
#route 'index', { path: '/' }
#resource 'product', { path: '/products/:product_id' }, ->
#route 'filter', { path: '/filter-by/:asset_type_id' }
##
# MODELS / EMBER-DATA
##
serializer = DS.JSONSerializer.create()
serializer.configure 'App.Document',
sideloadAs: 'documents'
serializer.configure 'App.AssetType',
sideloadAs: 'asset_types'
serializer.configure 'App.Product',
sideloadAs: 'products'
App.RestAdaptor = DS.RESTAdapter.extend
serializer: serializer
namespace: URL.slice 1
DS.Store.extend(
adapter: App.RestAdaptor
revision: 11
).create()
App.Product = DS.Model.extend
title: DS.attr 'string'
document_ids: DS.hasMany 'App.Document'
App.Document = DS.Model.extend
title: DS.attr 'string'
product_id: DS.belongsTo 'App.Product'
asset_type_id: DS.belongsTo 'App.AssetType'
App.AssetType = DS.Model.extend
title: DS.attr 'string'
document_ids: DS.hasMany 'App.Document'
######### /> END MODELS #################
Everything works as planned if I put the URL #/products/4/filter-by/2 into the address bar. It's only when I click the {{#linkTo}}'s that I get this error and the content is not displayed. The error is thrown before it get's to the App.ProductFilterRoute because the debugger statement in the route is not executed, but it is on page refresh.
Any help or direction is greatly appreciated, as I don't really know where to look.
UPDATE:
If I do not use the {{#linkTo}} helper and instead manually construct the url
{{title}}
everything works fine. What is different between the linkTo and manual href?
The error basically says that Ember expects an Array, when you navigate to the ProductFilterRoute.
Why does Ember expect an Array here?
The Controller for your Route (ProductFilterController) is of type ArrayController.
I am not very familiar with coffeescript, but your model hook seems to return an array too. Important Notice: The model hook is just executed when entering your App via Url. (This is why your manual navigation by Url and the href both work. Cite from EmberDoc: "A hook you can implement to convert the URL into the model for this route.")
Why is the error thrown?
So your route revolves around an array. You are passing just a plain object. So the golden rule is: Pass the same data structure (an array in this case) to your {{linkTo}} helper, which is returned by your model hook implementation.
A possible solution:
Use an action instead of {{linkTo}}
Implement an action in your route that finds all document with the given asset_type and pass it to your route.
Modifiations to template:
<a {{action 'filterProductByAsset' this}}> {{title}} </a>
Extensions to ProductFilterRoute:
events:{
filterProductByAsset : function(assetTypeId){
type = App.AssetType.find(asset_type_id);
product = this.modelFor('product');
docs = product.get('document_ids');
models = docs.filterProperty('asset_type_id', type);
this.transitionTo("product.filter", models)
}
}
I suspect this is the problem:
assetTypes: (->
docs = #get('documents')
docs.getEach 'asset_type_id'
).property('documents')
this looks like it will produce an array like this:
[1,2,3,4,5]
When really you need an array of objects that respond to id, e.g.
object1 = Em.Object.create id: 1
object2 = Em.Object.create id: 2
[object1, object2] #etc
If you want to change this behaviour, you will need to look into the serialize hook of the route you are linking to, in this case product.filter
With regard to {{linkTo}} vs. manually created links, the linkTo helper is js enabled, you should always use it instead of a manual link. This becomes more of a real problem when using the HistoryLocation / pushState support as it forces a full page reload.
There is another problem here: you can't observe documents like this. You must use something like property('documents.#each')