I'm building an Ember app which uses quite a few components. I'm also using Bootstrap. I've got a layout with tabs, and inside the second tab (which is hidden by default), the component (which contains a list of models which have a hasMany relationship with the main model) won't render.
I think I tracked this down to Ember Data resolving after the view is rendered, because if I click on another model of the list, these relations will show up.
Some info and details:
I have two main models:
Image
Crop
An image can have many crops.
I have an Images/Index controller which has this function:
loadCrops: function() {
var self = this;
this.get('selectedImage').get('crops').then(function(crops) {
self.set('selectedImageCrops', crops);
});
}.on('model.isFulfilled')
I added this method because I tried to manually resolve the relationship and get the crops for the image loaded in a variable but I had no luck with this. I'm passing the variables like this:
{{image-crops image=selectedImage crops=selectedImageCrops}}
This is my Index route:
export default Ember.Route.extend({
model: function () {
return this.store.find('image');
},
setupController: function(controller, model, request) {
controller.set('model', model);
}
});
If anyone needs more details please, ask for them. Thank you all!
When you use function() {}.on() you are telling Ember to execute that function when an event occurs. model.isFulfilled isn't an event though but a property so you need to observe it instead and do a quick check within the method that it really has been fullfilled (so it won't trigger if the promise is restarted for example)
loadCrops: function() {
if(!this.get('model.isFulfilled')) {
return;
}
var self = this;
this.get('selectedImage').get('crops').then(function(crops) {
self.set('selectedImageCrops', crops);
});
}.observes('model.isFulfilled')
Also as a side note I would suggest that you use an ES6 arrow function (which retains the outer this) instead of using var self = this, it's make the code a bit nicer.
loadCrops: function() {
if(!this.get('model.isFulfilled')) {
return;
}
this.get('selectedImage').get('crops').then((crops) => {
this.set('selectedImageCrops', crops);
});
}.observes('model.isFulfilled')
Try changing to a computed property:
selectedImageCrops: function() {
return this.get('selectedImage.crops');
}.property('selectedImage')
What I did in the end was moving the code to load the crops to the route's model, and returning an Ember.RSVP.hash with both the images and the crops, and then assigning it to the controller:
export default Ember.Route.extend({
/**
* Returns an Image instances array
*
* #method model
* #return {Ember.RSVP.hash} Images & Crops
*/
model: function () {
return Ember.RSVP.hash({
images: this.store.find('image'),
crops: this.store.find('crop')
});
},
/**
* Takes care of setting the needed variables in the controller
*
* #method setupController
*/
setupController: function(controller, model, request) {
controller.set('model', model.images);
controller.set('crops', model.crops);
}
});
Then I added a helper function in the controller to get the current image's crops:
selectedImageCrops: function() {
return this.get('crops').filter((obj) => {
return obj.image === this.get('selectedImage');
})[0];
}.property("selectedImage")
Thanks to #Karl-Johan Sjögren for the tip on the arrow function!
Related
So I've created a scroller control that I want to use in two different places within the same viewmodel for example:-
define(['common/viewmodels/controls/scroller-nav', 'common/viewmodels/controls/scroller-nav'],
function(mainScrollNav, modalScrollNav))
vm = {
activate: activate,
mainScrollControl: ko.observable(null),
modalScrollControl : ko.observable(null)
}
return vm;
function activate() {
vm.mainScrollControl({ model: mainScrollNav, view: 'common/views/controls/mainScroll' });
vm.modalScrollControl({ model: modalScrollNav, view: 'common/views/controls/modalScroll' });
// load up the data that is to be used for each (should be independent)
mainScrollNav.init();
modalScrollNav.init();
}
}
}
The control loads fine on both instances where mainScrollControl and modalScrollControl is populated however, the data is being shared (modify the scroller position in the modal and its also modified on the main page) even though the controls are defined separately. It seems like mainScrollNav and modalScrollNav link to a single service viewmodel as opposed to being independent viewmodels. Am I going about this right way or should I be using something else?
The solution was not to create a viewmodel, but a function of the view model so...
var control = function(){
vm = {
// Vars and functions
}
return vm;
}
return control;
Then the viewmodel can be resused as many times as needed just by calling the passed reference in the define paramaters. They both work independently as well.
define(['common/viewmodels/controls/scroller-nav'],function(scrollNav)){
vm = {
mainScroller: new scrollNav(),
subPageScroller: new scrollNav()
}
return vm;
I'm currently developing my first Backbone single page app project and I'm facing an issue.
Basically I have a menu (html select input element) implemented as a View. Its value is used to control pretty much every other data requests since it specifies which kind of data to show in the other Views.
Right now I handle the DOM event and trigger a global event so that every model can catch it and keep track internally of the new value. That's because that value is then needed when requesting new data. But this doesn't look like a good solution because A) I end up writing the same function (event handler) in every model and B) I get several models with the same variable.
var Metrics = Backbone.Collection.extend({
url: "dummy-metrics.json",
model: MetricsItem,
initialize: function () {
this.metric = undefined;
},
setMetric: function (metric) {
this.metric = metric;
globalEvents.trigger("metric:change", this.get(metric));
}
});
var GlobalComplexity = Backbone.Collection.extend({
url: function () {
var url = "http://asd/global.json?metric=" + this.metric;
return url;
}, //"dummy-global.json",
model: GlobalComplexyItem,
initialize: function () {
this.metric = undefined;
this.listenTo(globalEvents, "metric:change", this.updateMetric);
},
updateMetric: function (metric) {
this.metric = metric.get("id");
this.fetch({ reset: true });
}
});
All my other Collections are structured like GlobalComplexity.
What's the cleanest way to solve this problem?
Thank you very much.
Define a global parametersManager. Export an instance (singleton) then require it when you need it.
On "globalupdate" you update the parametersManager then trigger "update" for all your model/collections so they'll look what are the current parameters in the parametersManager.
I am struggling with when to destroy backbone views. I know I need to destroy the view somewhere, but I am not sure where.
I have the following code in router.js
routes: {
"names/search": "nameSearch",
"companies/search": "companySearch"
},
initialize: function(){
Backbone.history.start();
this.navigate("#/", true);
}
nameSearch: function () {
require(["app/views/RecordSearch"], function (RecordSearchView) {
var obj = {};
obj.Status = [utils.xlate("On Assignment"), utils.xlate("Candidate")];
var view = new RecordSearchView({ model: obj, el: $(".content") }, { "modelName": "Candidate" });
view.delegateEvents();
});
},
companySearch: function () {
require(["app/views/RecordSearch"], function (RecordSearchView) {
var view = new RecordSearchView({ model: {}, el: $(".content") }, { "modelName": "Company" });
view.delegateEvents();
});
}
And then in RecordSearchView.js I have the following function that is called when a user clicks the search button
doSearch: function () {
require(["app/utils/SearchHelper", "app/models/" + modelName, "app/views/SearchResults"], function (SearchHelper, Model, SearchResultsView) {
var obj = $("#searchForm").serializeArray();
var params = SearchHelper.getQuery(obj);
params["page"] = 1;
params["resultsPerPage"] = 25;
var collection = new Model[modelName + "Collection"]({}, { searchParams: params });
params["Fields"] = collection.getSearchFields();
collection.getPage(params["page"], function (data) {
require(["app/views/SearchResults"], function (SearchResultsView) {
App.Router.navigate(modelName + "/search/results");
var view = new SearchResultsView({ collection: data, el: $(".content") });
view.delegateEvents();
});
});
return false;
});
And SearchResults.js
return BaseView.extend({
init: function () {
this.render();
},
render: function () {
var data = this.collection.convertToSearchResults();
this.$el.html(template(data));
return this;
}
});
The problem is the second time I perform any search (calling the doSearch function from RecordSearch.js). As soon as I perform the second search, the data shown is that belonging to the previous search I performed. (For example I do a name search and it works, then do a company search but the screen shows company search results but then is quickly replaced with name search results).
My questions are
I suspect I need to call some cleanup code on the view before it is re-used. Where is the proper place within a backbone application to run this.
Is there anything wrong with the way I load SearchResults view from within RecordSearch view? SearchResults does not have a path on my router, but it is basically a form post, so I assume it shouldn't?
Any help is appreciated.
This problem is quite common and is known as Zombie Views. Derick Bailey explains this issue very well here: http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/
However unfortunately you can't simply solve it without changing the way you are loading your views.
Because you are loading them inside RequireJS modules that will keep it in the local var scope, you are losing the reference to the views once the route has been fully processed.
In order to solve this problem, you would need to keep the reference of the current view somewhere, and then properly dispose it before calling another view, something like this:
showView: function(view) {
this.currentView && this.currentView.remove();
this.currentView = view;
this.currentView.render();
$('#content').html(this.currentView.el);
}
More about this solution here: http://tiagorg.com/talk-backbone-tricks-or-treats-html5devconf/#/6
I personally suggest you adopting a solution that will take care of this for you, like Marionette.js
It will handle this and quite many other issues, by providing the missing gaps of every Backbone-based architecture.
Main goal: Using .find() to access a model other than the one available in the current controller -in order to compare data from the current controller's model with a piece of data from a 'foreign' controller's model.
What triggers the comparison:
I have a button inside a template with {{ action "isResponse"}}. This template's controller has an isResponse : function() {...}
The problem I have: The action is fired every time I click the button, but App.Answer.find() only returns content after the 2nd click. I'm wondering if this is because the Answer model hasn't loaded, but am unsure how to properly set up an observer for isLoaded in my example (if that is even the issue)
So how come App.Answer.find() returns empty the first time it's called??
App.ChoiceController = Ember.ObjectController.extend({
chosen: false,
isResponse: function() {
// successfully returns what I want from this controller's model
var questionId = this.get('question.id')
// gets DS.RecordArray of the model i'd like to compare with
var answers = App.Answer.find()
// filter to get a result that matches this.get('question.id')
var answer = answers.filter(function(ans) {
// returns all entries that match
if(ans.get('question.id') == questionId) { return true }
}, 'answers.isLoaded'); // this observer doesn't seem to hurt or help
// get the final value I need
var choice = answer.mapProperty('choice.id')
// if choice array is not empty, (should only have 1 element anyways)
if(!choice) {
this.set('chosen', choice[0]);
} else {
this.set('chosen', false);
}
}
})
Here are the models involved. Both include DS.belongsTo attributes
App.Choice = DS.Model.extend({
"question" : DS.belongsTo('App.Question')
})
App.Answer = DS.Model.extend({
"question" : DS.belongsTo('App.Question')
"choice" : DS.belongsTo('App.Choice')
})
App.Question = DS.Model.extend({
})
EDIT
Here is jsfiddle showing the behavior. Make sure to open your browser console to notice that each button requires 2 clicks for action isResponse to function properly. http://jsfiddle.net/iceking1624/QMBwe/
After reading your comment I've retought a solution to your problem and one possible way might be that you can define a AnswerController of type ArrayController (since it's for a collection of answers) and then setup this controller in your ApplicationRoute's setupController hook.
Main goal: Using .find() to access a model other than the one available in the current controller -in order to compare data from the current controller's model with a piece of data from a 'foreign' controller's model.
Later on you can then require access to the AnswerController's data using the needs API with needs:['answers'] from inside whatever controller that needs access to the answers collection, and finally have access to the data with this.get('controllers.answer'). You can find here more info on the needs API.
See here a possible solution that works correctly, displaying the right choice already on the 1st click:
App.AnswerController = Ember.ArrayController.extend({});
App.ApplicationRoute = Ember.Route.extend({
setupController: function(controller, model) {
this.controllerFor('answer').set('content', App.Answer.find());
}
});
App.ChoiceController = Ember.ObjectController.extend({
needs: ['answer'],
chosen: false,
isResponse: function() {
var questionId = this.get('question.id');
var answers = this.get('controllers.answer');
var answer = answers.content.filter(function(ans) {
if(ans.get('question.id') == questionId) { return true }
}
var choice = answer.mapProperty('choice.id');
if(!choice) {
this.set('chosen', choice[0]);
} else {
this.set('chosen', false);
}
}
});
And here a working fiddle.
Hope it helps.
I have the following controller in ExtJs:
Ext.define('FileBrowser.controller.BrowserController', {
extend: 'Ext.app.Controller',
views: ['browser.tree_dir', 'browser.grid_file'],
stores: ['store_dir', 'store_file'],
init: function () {
this.control({
'window > tree_dir': {
itemclick: {
fn: function (view, record, item, index, event) {
if (record.isLeaf() == false) {
Ext.getStore('store_file').load({
params: {
dir: record.data.id
}
});
var parentOfCurrentFiles = record.data.id
nodeId = record.data.id;
htmlId = item.id;
var grid_view = this.getView('browser.grid_file');
var grid_view_v = grid_view.getView();
grid_view_v.refresh();
}
}
}
}
});
},
onPanelRendered: function () {
console.log('The panel was rendered');
}
});
If you notice under 'itemclick' I am trying to refresh one of my views, my approach is not working. Can anyone explain to me how I can refresh the view? Thank you.
Replace var grid_view= this.getView('browser.grid_file'); with var grid_view= this.getView('browser.grid_file').create(); to get a real instance (as I already told you, getView() only return the view config, not a instance!) or if you have already created that grid and only one instance exist use the xtype along with a component query to receive it var grid_view=Ext.ComponentQuery('grid_file')[0]
Now to the refresh()
Basically you never need to call this method cause your grid is bound to a store and any change made on this store is directly reflected to your grid.
I would also recommend you to store view instances when creating them instead of using queries or directly use the ref property and let ExtJS do the work for you. The last one will the best solution you I guess... Take a look at ref's within the API examples and give it a try.
So what you are trying to do is, load the store and have the data reflect once you refresh the grid_view...?
In that case, you haven't done a setStore() to the grid, or if you have done that elsewhere, you are't doing a setData() to the store. Also you should call the refresh on the grid.