The workflow is:
User is on the New Page.
Hits save, causing model validations to fail. Errors bound to the model are shown. Still on the same page.
The user now navigates to the Index page and sees the invalid record added to the list.
The ArrayController seems to be adding records which failed validations.
App.CompaniesNewRoute = Ember.Route.extend({
model: function(){
var company = App.Company.createRecord();
this.wireObservers(company, this);
return company;
},
events: {
save: function(){
var controller = this.controllerFor(this.routeName);
controller.get('transaction').commit();
}
},
wireObservers: function(company, router) {
company.on('becameInvalid', function(record){
// do something to remove it from the arraycontroller
// record.rollback();
});
company.on('didCreate', function(){
router.transitionTo('companies.index')
});
})
})
The becameInvalid event does get called. Doing a record.rollback() throws an exception:
Uncaught Error: Attempted to handle event `becameClean` on <App.Company:ember612:null> while in state rootState.loaded.created.invalid. Called with undefined ember-data.js:3495
DS.StateManager.Ember.StateManager.extend.unhandledEvent ember-data.js:3495
Is there a way to prevent ArrayController to add records which failed validation.
Try to rollback the transaction through the store instead.
wireObservers: function(company, router) {
var _self = this;
company.on('becameInvalid', function(record){
// do something to remove it from the arraycontroller
_self.store.rollback();
});
company.on('didCreate', function(){
router.transitionTo('companies.index')
});
})
You should consider creating a specific transaction for the purpose rather than using the default one. To create a new transaction inside a route, you can do the following
App.MyRoute = Ember.Route.extend({
transaction: this.store.transaction();
})
and then create your record and add it to the transaction using
var company = this.transaction.createRecord(App.Company);
and finally commit or rollback the transaction
this.transaction.commit();
this.transaction.rollback();
In your case, I think a rollback() is not the solution, because the transaction has already been committed ; even if the server validation fails, the record has been added to the ArrayController content.
The solution I could see would be to use the deleteRecord function, with something like this :
wireObservers: function(company, router) {
company.on('becameInvalid', function(record){
record.deleteRecord();
});
company.on('didCreate', function(){
router.transitionTo('companies.index');
});
})
I've just a question about your workflow. When you get an validation error, and leave the page, it means the user cancelled the record creation
Related
https://cask.scotch.io/2014/10/V70cSEC.png
^ According to this flow, I have to make a change in the store, which is then picked up by the view.
More specifically, I am trying to delete a user, but when the store gets an error from the DB, I want to show a modal saying the error occurred. Would the right way of transmitting the message be done through the store variables and then picked up in the view on the getStateFromFlux method?
userStore = {
initialize: function(options) {
// other variables
this.userDeletionError = false;
},
deleteUser: function(payload) {
Axios.delete(DBURL)
.then((response) => {
// succeeds
})
.catch((error) => {
// other error handling
this.userDeletionError = true;
});
}
}
If I understand well you are using an ajax call in a store, this is an antipattern. The right way to do is make the call in the action file then transmit it in the store.
To answer your question the flux-pattern should look like that (not sure if it match 100% your use case).
View => User want to delete a 'user', he clicks on the delete button
View triggers an action.
Action triggers an ajax call 'delete this user'
Action receives the answer and transmit it to the store (here you are using the react dispatcher , example below:
MyAjaxCall.then(function(answer) {
Dispatcher.handleViewAction({
actionType: Constants.ActionTypes.DELETE_USER,
result: answer
});
});
5.Your store is catching the ajax answer still through the dispatcher (example below:
MyStore.dispatcherIndex = Dispatcher.register(function(payload) {
var action = payload.action;
var result;
switch(action.actionType) {
case Constants.ActionTypes.USER_DELETE:
registerAnswer(action.result);
MyStore.emitChange();
break;
}
return true;
});
You can see that your store will trigger registerAnswer(), in this function you can check if the ajaxcall has been executed (I mean is the user deleted or not) and accordingly build the object. Here there is two way to tell your view about the answer status 1. you build an dataAnswer object with a field message for example and then your view can check it 2. you emit a special event.
I prefer the first way if find it more generic.
Store emitChange and your view catch the event (example below:
componentDidMount: function() {
MyStore.addChangeListener(address, this._onDeleteUser;
},
Then your view check the 'message' field you filled in the store accordingly to the answer and you can render whatever is appropriate.
I hope it's clear. Here is an example of store in case you need it. https://facebook.github.io/flux/docs/todo-list.html#creating-stores
To resume, your approach is good except doing the ajax call in the store. Don't do that it's really bad.
Hope it helps
I am building a data reporting application in Ember.js and I have two select boxes (both components) which choose the date range (only by month) for the currently displayed data.
What should happen is when either select box is changed, the server gets hit with another request and all the data gets reloaded from the given period. But I can't work out how to get the Route's model method to rerun - I guess effectively observe the change event on the selects?
I have tried doing something like this:
Dashboard.MonthlyReviewRoute = Ember.Route.extend({
query: {},
model: function( params ) {
console.log(params);
var args = { page: 'monthly_review' };
args = Ember.merge(args, {});
return this.store.find('report', args);
},
setupController: function(controller, model) {
controller.set('model', model);
controller.set('report', model.get('content')[0]);
}
});
and then
Dashboard.SelectComponent = Ember.Component.extend({
tagName: 'select',
change: function(e) {
Dashboard.MonthlyReviewRoute.query.value = 5;
}
}
But how do I get the model to reload?!
Still getting my head around this event driven stuff so any help greatly appreciated and please excuse me if I'm not getting something really basic..!
transition to the route and send the id/filters to it.
from your component, you should send an action, change, with the necessary information. From your controller/route they should have an action that will handle the sent action. They can then do a transitionToRoute/transitionTo and send in the route and the id/filters to use for the route.
http://emberjs.jsbin.com/utuhAKo/1/edit
I need to work with backbone.js, i can't go to "render" part inside my view here is my code:
var Vigne = {
Models:{},
Collections: {},
Views:{},
Templates:{}
}
Vigne.Models.Raisin = Backbone.Model.extend({})
Vigne.Collections.Grape = Backbone.Collection.extend({
model: Vigne.Models.Raisin,
url: "./scripts/data/vin.json",
initialize: function (){
console.log("grape initialised");
}
});
Vigne.Views.Grape= Backbone.View.extend({
initialize: function(){
_.bindAll(this,"render");
this.collection.bind("reset",this.render);
},
render: function(){
console.log("render");
console.log(this.collection.length);
}
})
Vigne.Router = Backbone.Router.extend({
routes:{
"": "defaultRoute"
},
defaultRoute: function(){
console.log("defaultRoute");
Vigne.grape = new Vigne.Collections.Grape()
new Vigne.Views.Grape ({ collection : Vigne.grape });
Vigne.grape.fetch();
console.log(Vigne.grape.length);
}
}
);
var appRouter= new Vigne.Router();
Backbone.history.start();
I am expecting it to display my collection's length in the debugger's console, it seem's like it doesn't reset. Any ideas?
Edit:
i added this within the fetch function:
success: function(){
console.log(arguments);
},
error: function() {
console.log(arguments);
}
});
and the fetch function succeed on getting the json file, but it doesn't trigger the reset function.
i solved this problem by setting the attribute within the fetch function to true:
Vigne.grape.fetch({
reset:true,
error: function() {
console.log(arguments);
}
}
);
This book helped me : http://addyosmani.github.io/backbone-fundamentals/
Backbone calls reset() on fetch success which in turns triggers reset event. But If your fetch fails due to some reason, you won't get any event. So you have to pass an error handler in fetch method and use it to identify the error and handle it.
Vigne.grape.fetch({
error: function() {
console.log(arguments);
}
});
You can also pass success call back and you will be able to know the problem in your fetch.
You can also use Charles proxy/Chrome Debuuger Tool to identify if you are getting proper response from your backend.
Can you please paste your response what you are getting from server. You may vary the data but just keep the format right.
Edit:
One more problem I can see is that you have not defined attributes in your model So after Backbone fetch, it refreshes your collection with the new models fetched from server. Fetch method expects an array of json objects from server and each json object in response array should match with the attributes you have set in defaults in your model Otherwise it won't be able to create new models and hence won't be able to refresh your collection. Can you please rectify this and let me know.
Scenario
I am working on backbone app. What is happening right now is when user clicks edit link on page then it should show a form. I am trying to implement this using backbone routers rather than events. With events object it works perfectly fine. To use routers, I am using global events.
Problem
The problem is that when user clicks on edit link, it shows me following error in console
Uncaught TypeError: Object 10 has no method 'toJSON' views.js:57
This error is because on line 57 in views.js, I am using this.model.toJSON() whereas I am not passing model via router. I don't know how pass model through router
Here is my router. Note: All of the following codes are in separate files
App.Router = Backbone.Router.extend({
routes: {
'contacts/:id/edit': 'editContact'
},
editContact: function (id) {
console.log('yahhhhh');
vent.trigger('contact:edit', id);
}
});
In above router I am triggering an event inside editContact function. Then I am listening to above event in following initialize function.
App.Views.App = Backbone.View.extend({
initialize: function () {
vent.on('contact:edit', this.editContact, this);
},
editContact: function (contact) {
var editContactView = new App.Views.EditContact({ model: contact });
$('#editContact').html(editContactView.render().el);
}
});
Now in above after listening to event in initialize function, I am calling editContact function and I am also passing model using this keyword. Inside editContact function, I am creating an instance of EditContact, view which is following, and then rendering a form which needs to be shown.
App.Views.EditContact = Backbone.View.extend({
template: template('editContactTemplate'),
render: function () {
var html = this.template(this.model.toJSON()); //<--- this is line 57
this.$el.html(html);
return this;
}
});
After doing all of the above, the form is not shown and I am getting above mentioned error.
Question
How do I pass model to render function inside EditContact via router so that it starts working?
UPDATE
Here is my model
App.Models.Contact = Backbone.Model.extend({
urlRoot : '/contacts'
});
In your editContact method the argument contact refers to the id you pass onwards from the router. When you initialize a new view with new App.Views.EditContact({ model: contact }) the model of the view will, expectedly, be the id.
You need to map the id into a model instance. IMHO the correct place to do this is in the router:
editContact: function (id) {
var contact = new App.Models.Contact({id:id});
vent.trigger('contact:edit', contact);
}
Notice that at this point the model will only have the id property set. If you need to populate the model properties for editing, you should fetch the model from the server, and only then trigger the event:
editContact: function (id) {
var contact = new App.Models.Contact({id:id});
contact.fetch().done(function() {
vent.trigger('contact:edit', contact);
});
}
Edit based on comments: Generally speaking you shouldn't pass anything to the router. The router should be a starting point for every new request (url change). If you want to hold some state between page changes, you should store the data on the router level, and pass the models and collections "down" from the view.
In a simplified scenario this would mean initializing and storing a reference to the collection in the router. Something like:
var Router = Backbone.Router.extend({
initialize: function() {
this.contactCollection = new App.Collections.Contacts();
},
editContact: function (id) {
id = parseInt(id, 10);
if(_.isNaN(id)) {
//error...
}
//try to get a model from the collection
var contact = this.contactCollection.get(id);
//if not found, create, add and fetch
if(!contact) {
contact = new App.Models.Contact({id:id});
this.contactCollection.add(contact);
contact.fetch().done(function() {
vent.trigger('contact:edit', contact);
});
} else {
vent.trigger('contact:edit', contact);
}
}
});
Please note that this is just example code, and not necessarily how you should implement it, line by line. You should consider whether it's OK to display a potentially stale model in the view, or whether you should always fetch it from the server. In practice you might also abstract the collection state in a nice class, instead of handling it directly in the router.
Hope this answers your questions.
I have a simple app that lists contact reports,
in it i made a list view that fetches data from Mongolab.
On that i also made an input form that makes a new contact report in the list when submitted
the function i use in the controller is modelled from angular's example on their site :
app.factory('Contact',function($mongolabResource){
return $mongolabResource('contacts');
});
function ContactCreateCtrl($scope,$location,Contact) {
Contact.save(contact,function(){
$location.path('/');
});
};
the $location.path() is the callback that reloads the page.
how do i rewrite this so that when the data has been submitted ( .save() is successful ) the view reloads without the page reloading?
i tried deleting and then redefining the array but doesnt seem to work :
Contact.save(contact,function(){
delete $scope.contacts;
$scope.contacts = Contact.query();
});
i would like to implement this on the delete function as well. Can somebody point me to where i can learn this?
Much thanks for any help
Okay, I updated your fiddle to fetch the value from the database: http://jsfiddle.net/joshdmiller/Y223F/2/.
app.controller( 'MainCtrl', function ( $scope,Contact ) {
$scope.updateContacts = function () {
Contact.query( function( data ) {
$scope.contacts = data;
});
};
$scope.save = function( newContact ) {
Contact.save( newContact, function() {
$scope.updateContacts();
});
};
// The initial data load
$scope.updateContacts();
});
Two things to note:
(1) I moved your Mongo query into a function so that it can be called again when the new record is created;
(2) the $mongolabResource expects a callback to be executed on success; your app flickered because you didn't provide one. In other words, from the time you called the query to the time fetch was complete, your list was empty. Instead, we want it to change only when we get the new data. I changed that too.
In terms of adding the item manually or fetching from the database, best practice is based on the use case and there are trade-offs. But for small data such as this, just fetch from the DB.
got this to work, but still not sure about pushing into the array at the scope, would be better if we could fetch from database
function ContactCreateCtrl($scope,$location,Contact) {
Contact.save(contact,function(){
$scope.contacts.push(contact);
});
also i would need the _id object thats automatically generated by the db, for linking purposes. this method doesnt give me the _id, any insights?
I'm sharing my answer for how I cleared data in the view after sign out from firebase using the firebase auth service. The data was still persisting after calling $scope.currentUser = null; on the signout method. Had to reload to see the data change. Not best UX.
$scope.getCurrentUser = function() {
firebase.auth().onAuthStateChanged(function(user) {
if (user) {
// User is signed in.
$scope.currentUser = user;
} else {
// No user is signed in.
$scope.currentUser = null;
console.log('user not signed in.');
}
});
}
$scope.getCurrentUser();
$scope.signout = function() {
firebase.auth().signOut().then(function() {
// Sign-out successful.
console.log('signed out success');
$scope.getCurrentUser();
}, function(error) {
// An error happened.
console.log(error);
});
}
so calling the getUserData method and making the currentUser = null there updated the view without a reload. this is an example using firebase but with a few adjustments it might apply to your needs for clearing data from view without a full page reload. Firebase does the heavy lifting here of clearing out the user object but my view doesn't care until I check again in my getCurrentUser method to see if there is still a user and if not then clear it from the $scope without reloading the view.