/** * @author Ed Spencer * * @aside guide controllers * @aside guide apps_intro * @aside guide history_support * @aside video mvc-part-1 * @aside video mvc-part-2 * * Controllers are responsible for responding to events that occur within your app. If your app contains a Logout * {@link Ext.Button button} that your user can tap on, a Controller would listen to the Button's tap event and take * the appropriate action. It allows the View classes to handle the display of data and the Model classes to handle the * loading and saving of data - the Controller is the glue that binds them together. * * ## Relation to Ext.app.Application * * Controllers exist within the context of an {@link Ext.app.Application Application}. An Application usually consists * of a number of Controllers, each of which handle a specific part of the app. For example, an Application that * handles the orders for an online shopping site might have controllers for Orders, Customers and Products. * * All of the Controllers that an Application uses are specified in the Application's * {@link Ext.app.Application#controllers} config. The Application automatically instantiates each Controller and keeps * references to each, so it is unusual to need to instantiate Controllers directly. By convention each Controller is * named after the thing (usually the Model) that it deals with primarily, usually in the plural - for example if your * app is called 'MyApp' and you have a Controller that manages Products, convention is to create a * MyApp.controller.Products class in the file app/controller/Products.js. * * ## Refs and Control * * The centerpiece of Controllers is the twin configurations {@link #refs} and {@link #cfg-control}. These are used to * easily gain references to Components inside your app and to take action on them based on events that they fire. * Let's look at {@link #refs} first: * * ### Refs * * Refs leverage the powerful {@link Ext.ComponentQuery ComponentQuery} syntax to easily locate Components on your * page. We can define as many refs as we like for each Controller, for example here we define a ref called 'nav' that * finds a Component on the page with the ID 'mainNav'. We then use that ref in the addLogoutButton beneath it: * * Ext.define('MyApp.controller.Main', { * extend: 'Ext.app.Controller', * * config: { * refs: { * nav: '#mainNav' * } * }, * * addLogoutButton: function() { * this.getNav().add({ * text: 'Logout' * }); * } * }); * * Usually, a ref is just a key/value pair - the key ('nav' in this case) is the name of the reference that will be * generated, the value ('#mainNav' in this case) is the {@link Ext.ComponentQuery ComponentQuery} selector that will * be used to find the Component. * * Underneath that, we have created a simple function called addLogoutButton which uses this ref via its generated * 'getNav' function. These getter functions are generated based on the refs you define and always follow the same * format - 'get' followed by the capitalized ref name. In this case we're treating the nav reference as though it's a * {@link Ext.Toolbar Toolbar}, and adding a Logout button to it when our function is called. This ref would recognize * a Toolbar like this: * * Ext.create('Ext.Toolbar', { * id: 'mainNav', * * items: [ * { * text: 'Some Button' * } * ] * }); * * Assuming this Toolbar has already been created by the time we run our 'addLogoutButton' function (we'll see how that * is invoked later), it will get the second button added to it. * * ### Advanced Refs * * Refs can also be passed a couple of additional options, beyond name and selector. These are autoCreate and xtype, * which are almost always used together: * * Ext.define('MyApp.controller.Main', { * extend: 'Ext.app.Controller', * * config: { * refs: { * nav: '#mainNav', * * infoPanel: { * selector: 'tabpanel panel[name=fish] infopanel', * xtype: 'infopanel', * autoCreate: true * } * } * } * }); * * We've added a second ref to our Controller. Again the name is the key, 'infoPanel' in this case, but this time we've * passed an object as the value instead. This time we've used a slightly more complex selector query - in this example * imagine that your app contains a {@link Ext.tab.Panel tab panel} and that one of the items in the tab panel has been * given the name 'fish'. Our selector matches any Component with the xtype 'infopanel' inside that tab panel item. * * The difference here is that if that infopanel does not exist already inside the 'fish' panel, it will be * automatically created when you call this.getInfoPanel inside your Controller. The Controller is able to do this * because we provided the xtype to instantiate with in the event that the selector did not return anything. * * ### Control * * The sister config to {@link #refs} is {@link #cfg-control}. {@link #cfg-control Control} is the means by which your listen * to events fired by Components and have your Controller react in some way. Control accepts both ComponentQuery * selectors and refs as its keys, and listener objects as values - for example: * * Ext.define('MyApp.controller.Main', { * extend: 'Ext.app.Controller', * * config: { * control: { * loginButton: { * tap: 'doLogin' * }, * 'button[action=logout]': { * tap: 'doLogout' * } * }, * * refs: { * loginButton: 'button[action=login]' * } * }, * * doLogin: function() { * //called whenever the Login button is tapped * }, * * doLogout: function() { * //called whenever any Button with action=logout is tapped * } * }); * * Here we have set up two control declarations - one for our loginButton ref and the other for any Button on the page * that has been given the action 'logout'. For each declaration we passed in a single event handler - in each case * listening for the 'tap' event, specifying the action that should be called when that Button fires the tap event. * Note that we specified the 'doLogin' and 'doLogout' methods as strings inside the control block - this is important. * * You can listen to as many events as you like in each control declaration, and mix and match ComponentQuery selectors * and refs as the keys. * * ## Routes * * As of Sencha Touch 2, Controllers can now directly specify which routes they are interested in. This enables us to * provide history support within our app, as well as the ability to deeply link to any part of the application that we * provide a route for. * * For example, let's say we have a Controller responsible for logging in and viewing user profiles, and want to make * those screens accessible via urls. We could achieve that like this: * * Ext.define('MyApp.controller.Users', { * extend: 'Ext.app.Controller', * * config: { * routes: { * 'login': 'showLogin', * 'user/:id': 'showUserById' * }, * * refs: { * main: '#mainTabPanel' * } * }, * * //uses our 'main' ref above to add a loginpanel to our main TabPanel (note that * //'loginpanel' is a custom xtype created for this application) * showLogin: function() { * this.getMain().add({ * xtype: 'loginpanel' * }); * }, * * //Loads the User then adds a 'userprofile' view to the main TabPanel * showUserById: function(id) { * MyApp.model.User.load(id, { * scope: this, * success: function(user) { * this.getMain().add({ * xtype: 'userprofile', * user: user * }); * } * }); * } * }); * * The routes we specified above simply map the contents of the browser address bar to a Controller function to call * when that route is matched. The routes can be simple text like the login route, which matches against * http://myapp.com/#login, or contain wildcards like the 'user/:id' route, which matches urls like * http://myapp.com/#user/123. Whenever the address changes the Controller automatically calls the function specified. * * Note that in the showUserById function we had to first load the User instance. Whenever you use a route, the * function that is called by that route is completely responsible for loading its data and restoring state. This is * because your user could either send that url to another person or simply refresh the page, which we wipe clear any * cached data you had already loaded. There is a more thorough discussion of restoring state with routes in the * application architecture guides. * * ## Advanced Usage * * See [the Controllers guide](#!/guide/controllers) for advanced Controller usage including before filters * and customizing for different devices. */ Ext.define('Ext.app.Controller', { mixins: { observable: "Ext.mixin.Observable" }, config: { /** * @cfg {Object} refs A collection of named {@link Ext.ComponentQuery ComponentQuery} selectors that makes it * easy to get references to key Components on your page. Example usage: * * refs: { * main: '#mainTabPanel', * loginButton: '#loginWindow button[action=login]', * * infoPanel: { * selector: 'infopanel', * xtype: 'infopanel', * autoCreate: true * } * } * * The first two are simple ComponentQuery selectors, the third (infoPanel) also passes in the autoCreate and * xtype options, which will first run the ComponentQuery to see if a Component matching that selector exists * on the page. If not, it will automatically create one using the xtype provided: * * someControllerFunction: function() { * //if the info panel didn't exist before, calling its getter will instantiate * //it automatically and return the new instance * this.getInfoPanel().show(); * } * * @accessor */ refs: {}, /** * @cfg {Object} routes Provides a mapping of urls to Controller actions. Whenever the specified url is matched * in the address bar, the specified Controller action is called. Example usage: * * routes: { * 'login': 'showLogin', * 'users/:id': 'showUserById' * } * * The first route will match against http://myapp.com/#login and call the Controller's showLogin function. The * second route contains a wildcard (':id') and will match all urls like http://myapp.com/#users/123, calling * the showUserById function with the matched ID as the first argument. * * @accessor */ routes: {}, /** * @cfg {Object} control Provides a mapping of Controller functions that should be called whenever certain * Component events are fired. The Components can be specified using {@link Ext.ComponentQuery ComponentQuery} * selectors or {@link #refs}. Example usage: * * control: { * 'button[action=logout]': { * tap: 'doLogout' * }, * main: { * activeitemchange: 'doUpdate' * } * } * * The first item uses a ComponentQuery selector to run the Controller's doLogout function whenever any Button * with action=logout is tapped on. The second calls the Controller's doUpdate function whenever the * activeitemchange event is fired by the Component referenced by our 'main' ref. In this case main is a tab * panel (see {@link #refs} for how to set that reference up). * * @accessor */ control: {}, /** * @cfg {Object} before Provides a mapping of Controller functions to filter functions that are run before them * when dispatched to from a route. These are usually used to run pre-processing functions like authentication * before a certain function is executed. They are only called when dispatching from a route. Example usage: * * Ext.define('MyApp.controller.Products', { * config: { * before: { * editProduct: 'authenticate' * }, * * routes: { * 'product/edit/:id': 'editProduct' * } * }, * * //this is not directly because our before filter is called first * editProduct: function() { * //... performs the product editing logic * }, * * //this is run before editProduct * authenticate: function(action) { * MyApp.authenticate({ * success: function() { * action.resume(); * }, * failure: function() { * Ext.Msg.alert('Not Logged In', "You can't do that, you're not logged in"); * } * }); * } * }); * * @accessor */ before: {}, /** * @cfg {Ext.app.Application} application The Application instance this Controller is attached to. This is * automatically provided when using the MVC architecture so should rarely need to be set directly. * @accessor */ application: {}, /** * @cfg {String[]} stores The set of stores to load for this Application. Each store is expected to * exist inside the *app/store* directory and define a class following the convention * AppName.store.StoreName. For example, in the code below, the *AppName.store.Users* class will be loaded. * Note that we are able to specify either the full class name (as with *AppName.store.Groups*) or just the * final part of the class name and leave Application to automatically prepend *AppName.store.'* to each: * * stores: [ * 'Users', * 'AppName.store.Groups', * 'SomeCustomNamespace.store.Orders' * ] * @accessor */ stores: [], /** * @cfg {String[]} models The set of models to load for this Application. Each model is expected to exist inside the * *app/model* directory and define a class following the convention AppName.model.ModelName. For example, in the * code below, the classes *AppName.model.User*, *AppName.model.Group* and *AppName.model.Product* will be loaded. * Note that we are able to specify either the full class name (as with *AppName.model.Product*) or just the * final part of the class name and leave Application to automatically prepend *AppName.model.* to each: * * models: [ * 'User', * 'Group', * 'AppName.model.Product', * 'SomeCustomNamespace.model.Order' * ] * @accessor */ models: [], /** * @cfg {Array} views The set of views to load for this Application. Each view is expected to exist inside the * *app/view* directory and define a class following the convention AppName.view.ViewName. For example, in the * code below, the classes *AppName.view.Users*, *AppName.view.Groups* and *AppName.view.Products* will be loaded. * Note that we are able to specify either the full class name (as with *AppName.view.Products*) or just the * final part of the class name and leave Application to automatically prepend *AppName.view.* to each: * * views: [ * 'Users', * 'Groups', * 'AppName.view.Products', * 'SomeCustomNamespace.view.Orders' * ] * @accessor */ views: [] }, /** * Constructs a new Controller instance */ constructor: function(config) { this.initConfig(config); this.mixins.observable.constructor.call(this, config); }, /** * @cfg * Called by the Controller's {@link #application} to initialize the Controller. This is always called before the * {@link Ext.app.Application Application} launches, giving the Controller a chance to run any pre-launch logic. * See also {@link #launch}, which is called after the {@link Ext.app.Application#launch Application's launch function} */ init: Ext.emptyFn, /** * @cfg * Called by the Controller's {@link #application} immediately after the Application's own * {@link Ext.app.Application#launch launch function} has been called. This is usually a good place to run any * logic that has to run after the app UI is initialized. See also {@link #init}, which is called before the * {@link Ext.app.Application#launch Application's launch function}. */ launch: Ext.emptyFn, /** * Convenient way to redirect to a new url. See {@link Ext.app.Application#redirectTo} for full usage information. * @return {Object} */ redirectTo: function(place) { return this.getApplication().redirectTo(place); }, /** * @private * Executes an Ext.app.Action by giving it the correct before filters and kicking off execution */ execute: function(action, skipFilters) { action.setBeforeFilters(this.getBefore()[action.getAction()]); action.execute(); }, /** * @private * Massages the before filters into an array of function references for each controller action */ applyBefore: function(before) { var filters, name, length, i; for (name in before) { filters = Ext.Array.from(before[name]); length = filters.length; for (i = 0; i < length; i++) { filters[i] = this[filters[i]]; } before[name] = filters; } return before; }, /** * @private */ applyControl: function(config) { this.control(config, this); return config; }, /** * @private */ applyRefs: function(refs) { //<debug> if (Ext.isArray(refs)) { Ext.Logger.deprecate("In Sencha Touch 2 the refs config accepts an object but you have passed it an array."); } //</debug> this.ref(refs); return refs; }, /** * @private * Adds any routes specified in this Controller to the global Application router */ applyRoutes: function(routes) { var app = this instanceof Ext.app.Application ? this : this.getApplication(), router = app.getRouter(), route, url, config; for (url in routes) { route = routes[url]; config = { controller: this.$className }; if (Ext.isString(route)) { config.action = route; } else { Ext.apply(config, route); } router.connect(url, config); } return routes; }, /** * @private * As a convenience developers can locally qualify store names (e.g. 'MyStore' vs * 'MyApp.store.MyStore'). This just makes sure everything ends up fully qualified */ applyStores: function(stores) { return this.getFullyQualified(stores, 'store'); }, /** * @private * As a convenience developers can locally qualify model names (e.g. 'MyModel' vs * 'MyApp.model.MyModel'). This just makes sure everything ends up fully qualified */ applyModels: function(models) { return this.getFullyQualified(models, 'model'); }, /** * @private * As a convenience developers can locally qualify view names (e.g. 'MyView' vs * 'MyApp.view.MyView'). This just makes sure everything ends up fully qualified */ applyViews: function(views) { return this.getFullyQualified(views, 'view'); }, /** * @private * Returns the fully qualified name for any class name variant. This is used to find the FQ name for the model, * view, controller, store and profiles listed in a Controller or Application. * @param {String[]} items The array of strings to get the FQ name for * @param {String} namespace If the name happens to be an application class, add it to this namespace * @return {String} The fully-qualified name of the class */ getFullyQualified: function(items, namespace) { var length = items.length, appName = this.getApplication().getName(), name, i; for (i = 0; i < length; i++) { name = items[i]; //we check name === appName to allow MyApp.profile.MyApp to exist if (Ext.isString(name) && (Ext.Loader.getPrefix(name) === "" || name === appName)) { items[i] = appName + '.' + namespace + '.' + name; } } return items; }, /** * @private */ control: function(selectors) { this.getApplication().control(selectors, this); }, /** * @private * 1.x-inspired ref implementation */ ref: function(refs) { var me = this, refName, getterName, selector, info; for (refName in refs) { selector = refs[refName]; getterName = "get" + Ext.String.capitalize(refName); if (!this[getterName]) { if (Ext.isString(refs[refName])) { info = { ref: refName, selector: selector }; } else { info = refs[refName]; } this[getterName] = function(refName, info) { var args = [refName, info]; return function() { return me.getRef.apply(me, args.concat.apply(args, arguments)); }; }(refName, info); } this.references = this.references || []; this.references.push(refName.toLowerCase()); } }, /** * @private */ getRef: function(ref, info, config) { this.refCache = this.refCache || {}; info = info || {}; config = config || {}; Ext.apply(info, config); if (info.forceCreate) { return Ext.ComponentManager.create(info, 'component'); } var me = this, cached = me.refCache[ref]; if (!cached) { me.refCache[ref] = cached = Ext.ComponentQuery.query(info.selector)[0]; if (!cached && info.autoCreate) { me.refCache[ref] = cached = Ext.ComponentManager.create(info, 'component'); } if (cached) { cached.on('destroy', function() { me.refCache[ref] = null; }); } } return cached; }, /** * @private */ hasRef: function(ref) { return this.references && this.references.indexOf(ref.toLowerCase()) !== -1; } // <deprecated product=touch since=2.0> ,onClassExtended: function(cls, members) { var prototype = this.prototype, defaultConfig = prototype.config, config = members.config || {}, arrayRefs = members.refs, objectRefs = {}, stores = members.stores, views = members.views, format = Ext.String.format, refItem, key, length, i, functionName; // Convert deprecated properties in application into a config object for (key in defaultConfig) { if (key in members && key != "control") { if (key == "refs") { //we need to convert refs from the 1.x array-style to 2.x object-style for (i = 0; i < arrayRefs.length; i++) { refItem = arrayRefs[i]; objectRefs[refItem.ref] = refItem; } config.refs = objectRefs; } else { config[key] = members[key]; } delete members[key]; // <debug warn> Ext.Logger.deprecate(key + ' is deprecated as a property directly on the ' + this.$className + ' prototype. Please put it inside the config object.'); // </debug> } } if (stores) { length = stores.length; config.stores = stores; for (i = 0; i < length; i++) { functionName = format("get{0}Store", Ext.String.capitalize(stores[i])); prototype[functionName] = function(name) { return function() { return Ext.StoreManager.lookup(name); }; }(stores[i]); } } if (views) { length = views.length; config.views = views; for (i = 0; i < length; i++) { functionName = format("get{0}View", views[i]); prototype[functionName] = function(name) { return function() { return Ext.ClassManager.classes[format("{0}.view.{1}", this.getApplication().getName(), name)]; }; }(views[i]); } } members.config = config; }, /** * Returns a reference to a Model. * @param modelName * @return {Object} * @deprecated 2.0.0 Considered bad practice - please just use the Model name instead * (e.g. `MyApp.model.User` vs `this.getModel('User')`). */ getModel: function(modelName) { //<debug warn> Ext.Logger.deprecate("getModel() is deprecated and considered bad practice - please just use the Model " + "name instead (e.g. MyApp.model.User vs this.getModel('User'))"); //</debug> var appName = this.getApplication().getName(), classes = Ext.ClassManager.classes; return classes[appName + '.model.' + modelName]; }, /** * Returns a reference to another Controller. * @param controllerName * @param profile * @return {Object} * @deprecated 2.0.0 Considered bad practice - if you need to do this * please use this.getApplication().getController() instead */ getController: function(controllerName, profile) { //<debug warn> Ext.Logger.deprecate("Ext.app.Controller#getController is deprecated and considered bad practice - " + "please use this.getApplication().getController('someController') instead"); //</debug> return this.getApplication().getController(controllerName, profile); } // </deprecated> }, function() { // <deprecated product=touch since=2.0> Ext.regController = function(name, config) { Ext.apply(config, { extend: 'Ext.app.Controller' }); Ext.Logger.deprecate( '[Ext.app.Controller] Ext.regController is deprecated, please use Ext.define to define a Controller as ' + 'with any other class. For more information see the Touch 1.x -> 2.x migration guide' ); Ext.define('controller.' + name, config); }; // </deprecated> });