I've faced with this problem many times when optimize my Sencha Touch 2 apps.
It's obvious that I should keep the DOM light-weighted, my application contains an Ext.TabBar and a main Ext.Container above. Everytime when switch from a view to another, I simply remove current view and add new view to that Container.
But the problem is, there are some views which have customized data. I mean they have inner data such as html content, filtered store records, etc. When I remove them from my main Container, I want to somehow save their "states" to a global variable, for example: I'm doing an e-Commerce app with products with details. When remove details panel from main container, I want to do something like this:
var saved_detail_panel = mainContainer.getActiveItem();
if I could do that, later when I want to add that detail panel back to main container, I can simply use:
mainContainer.add(saved_detail_panel);
I've tried many times but could not find one that works yet.
Highly appreciate for any helps. Thank you.
Updated:
When I put this code in my event handler in a controller:
var temp = Ext.create('taxi.view.location.LocationPanel', {showAnimation: null});
taxi.main_container = temp;
It works well but is not performant. The thing I want to do is to create it once only in my app.js, like this:
launch: function(){
var temp = Ext.create('taxi.view.location.LocationPanel', {showAnimation: null});
};
and only use this in Controller:
taxi.main_container = temp;
It works for the first time. But in the second time, it shows this error:
Uncaught TypeError: Cannot call method 'replaceCls' of null
Can you use the app's "global namespace"? Then you can reference MyApp.savedValue anywhere in your app?
Ext.application({
name: 'MyApp',
// views: [],
// models: [],
// stores: [],
controllers: ['Main'],
launch: function() {
MyApp.savedValue = "Hello word";
Ext.Viewport.add(Ext.create('Sencha.view.tablet.MainView'));
}
});
One other idea in the Sencha examples is in the KitchenSink demo. In app/controllers/Main.js, they use a view cache, which is setup in the config:{} and accessed via a getter/setter. I think the main controller always persists so your cache is always available. In fact, aren't all of your controllers persist if they're loaded in the app.js?
controllers: ['Main','FooController', 'BarController'],
Snippets from: app/controllers/Main.js
config: {
/**
* #private
*/
viewCache: [], // Accessed via getViewCache(), setViewCache()
...
},
createView: function(name) {
var cache = this.getViewCache(), // Implied getter
ln = cache.length,
limit = 20, // max views to cache
view, i, oldView;
// See if view is already in the cache and return it
Ext.each(cache, function(item) {
if (item.viewName === name) {
view = item;
return;
}
}, this);
if (view) {
return view;
}
// If we've reached our cache limit then remove something
if (ln >= limit) {
for (i = 0; i < ln; i++) {
oldView = cache[i];
if (!oldView.isPainted()) {
oldView.destroy();
cache.splice(i, 1);
break;
}
}
}
// Create the view and add it to the cache
view = Ext.create(name);
view.viewName = name;
cache.push(view);
this.setViewCache(cache); // Implied setter
return view;
},
try this,
var appNS = new Ext.application({
name: 'app',
//others
launch: function () {
appNS.SavedValue = 'getting start..........';
},
//others
});
and then you can use it inside of controller
console.log(appNs.SavedValue);
i hope it might be helpful.
another soln is caching ur views. it is not actually caching.
first add an array to your application like .
Ext.application({
name: 'app',
viewCache: [],
viewCacheCount: 0,
launch: function () {
this.viewCache.push("app init......."); // push your view
}
});
and this can be retrive inside of any controller something like
ths.getApplication().viewCache.pop(); //pop your view
I dont know it might create some problem in case of some views/components that are auto destroyable.
Try to add autoDestroy: false in your view.
Ext.define('JiaoJiao.view.personal.Card', {
extend: 'Ext.NavigationView',
xtype: 'personalContainer',
config: {
tab: {
title: '个人',
iconCls: 'user',
action: 'personalTab'
},
autoDestroy: false,
items: [
{
xtype:'personal',
store: 'Personals'
}
]
}
});
Related
I have 2 components - addProjectForm and listProjects. They are both nested components inside the root module. Whenever I add a project using the form, I want it to appear in the list straight away.
To achieve this, I had to pass down the controller instance to each component like this:
var RootComponent = {};
rootComponent.controller = function() {
this.example = 'test variable';
}
rootComponent.view = function(ctrl) {
return [
m.component(addProjectForm, ctrl),
m.component(listProjects, ctrl)
];
}
and then the listProjectscomponent for example, looks like this:
var listProjects = {
controller: function(root) {
this.root = root;
},
view: function(ctrl) {
console.log(ctrl.root.example);
}
};
So this way I keep calling methods on the top level, but I don't quite like passing down the controller instance like this. Is there any other way I should be doing it?
I think this is what you're looking for:
Mithril.js: Should two child components talk to each other through their parent's controller?
A newer way of solving this common problem is to use a Flux like architecture developed by Facebook:
https://facebook.github.io/flux/
Writing your own dispatcher is semi-trivial. Here's an example that someone else built alongside Mithril:
https://gist.github.com/MattMcFarland/25fb4f0241530d2f421a
The downside with this approach is it would be somewhat anti-Flux to use m.withAttr, as views aren't supposed to write directly to models in the dispatcher paradigm.
The problem you have is the difference between passing by reference or by value. In JS all primitive types are passed by value. Thats why you can't pass the string directly since it's cloned during pass. You have multiple options here:
You can use m.prop and just pass the variable down to the components, m.props stores the value in function that is always passed by reference.
var RootComponent = {};
rootComponent.controller = function() {
this.example = m.prop('test variable');
}
rootComponent.view = function(ctrl) {
return [
m.component(addProjectForm, ctrl.example),
m.component(listProjects, ctrl.example)
];
}
If the variable is an array, it will be passed by reference anyways.
Second option is to keep the list in the root context and add a callback to the second component.
var RootComponent = {};
rootComponent.controller = function() {
var projects = this.projects = [];
this.addProject = function(project) {
projects.push(project);
}
}
rootComponent.view = function(ctrl) {
return [
m.component(addProjectForm, {
onsubmit: ctrl.addProject
}),
m.component(listProjects, ctrl.projects)
];
}
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.
I'm using Require.js and Backbone, and have a Backbone router module like:
define([
"views/global",
"views/project/edit",
"views/project/list",
], function(GlobalView, edit, list){
return Backbone.Router.extend({
routes: {
"projects/:action/" : "projectsAction",
},
projectsAction : function(action) {
/* .... lots of code cut out here .... */
/* Create and render the action specified */
this.subView = new eval(action+"()").render();
}
});
});
This is an example, I've cut a lot of setup code out of projectAction.
I would like the URL: /projects/list to run projectAction, with the action param = list, and then the list module from the Require.js function to be called. I'm currently doing it with eval(), but I'm wondering if there is a better way?
Basically, in Javascript, can you refer to a variable, with another variable name, without using eval()?
I guess a shorter version would be, how do you do:
var name = "Math.random";
name(); // = 0.34343....
Without eval()?
You cannot access a variable having the name in a string. But you can create a mapping:
var actions = {
edit: edit,
list: list
};
And then you can access the function by the key:
projectsAction : function(action) {
this.subView = new actions[action]().render();
}
The best way imo, is to use the require function of requirejs:
projectsAction : function(action) {
/* .... lots of code cut out here .... */
/* Create and render the action specified */
var self = this;
require('views/project/' + action, function(view) {
(self.subView = new view).render();
}
}
As it would also cut the boilerplate from having lots of actions.
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.