Upgrade to ExtJS 4.0.7 - Released 10/19/2011
[extjs.git] / docs / guides / mvc_pt2 / README.md
1 # Architecting Your App in Ext JS 4, Part 2
2
3 In the previous Ext JS Architecture article, we explored how to architect a Pandora-style application using Ext JS. We took a look at the Model-View-Controller architecture and how to apply it to a relatively complex UI application that had multiple views and models. In this article, we’re going to move beyond architecting the application visually, and explore how to design and code the controllers and models, starting with Ext.application and the Viewport class.
4
5 Let’s just jump in and start writing the application.
6
7 ## Defining our application
8
9 In Ext JS 3, the Ext.onReady method was the entry point into your application, and the developer had to come up with an application architecture. In Ext JS 4, we have an introduced an MVC-like pattern. This pattern helps you to follow best practices when creating your applications.
10
11 The entry point into an application written with the new MVC package requires that you use the Ext.application method. This method will create an Ext.app.Application instance for you and will fire the launch method as soon as the page is ready. This essentially replaces the need to use Ext.onReady while adding new functionality such as automatically creating a viewport and setting up your namespace.
12
13 ### `app/Application.js`
14
15     Ext.application({
16         name: 'Panda',
17         autoCreateViewport: true,
18         launch: function() {
19             // This is fired as soon as the page is ready
20         }
21     });
22
23 The name configuration causes a new namespace to be created. All our views, models, stores and controllers will live in this namespace. By setting autoCreateViewport to true, the framework will, by convention, include the app/view/Viewport.js file. In this file, a class should be defined with the name Panda.view.Viewport, matching the namespace that was specified by the name configuration of your application.
24
25 ## The Viewport class
26
27 When we looked at which views we needed for our UI, we were very focused on the individual parts. The Viewport of an application acts as the glue for these individual parts. It loads the required views and defines the configuration needed to achieve your app’s overall layout. We have found that progressively defining your views and adding them to the viewport is the fastest way to create the base structure of your UI.
28
29 It is important during this process to focus on scaffolding your views and not on the individual views themselves. It’s almost like sculpting. We start by creating the very rough shapes of our views and add more detail to them later.
30
31 ## Creating the building blocks
32
33 Leveraging the work we already did in the previous article, we are able to define many of the views at once.
34
35 {@img balanced.png}
36
37 ### `app/view/NewStation.js`
38
39     Ext.define('Panda.view.NewStation', {
40         extend: 'Ext.form.field.ComboBox',
41         alias: 'widget.newstation',
42         store: 'SearchResults',
43         ... more configuration ...
44     });
45
46 ### `app/view/SongControls.js`
47
48     Ext.define('Panda.view.SongControls', {
49         extend: 'Ext.Container',
50         alias: 'widget.songcontrols',
51         ... more configuration ...
52     });
53
54 ### `app/view/StationsList`
55
56     Ext.define('Panda.view.StationsList', {
57         extend: 'Ext.grid.Panel',
58         alias: 'widget.stationslist',
59         store: 'Stations',
60         ... more configuration ...
61     });
62
63 ### `app/view/RecentlyPlayedScroller.js`
64
65     Ext.define('Panda.view.RecentlyPlayedScroller', {
66         extend: 'Ext.view.View',
67         alias: 'widget.recentlyplayedscroller',
68         itemTpl: '<div></div>',
69         store: 'RecentSongs',
70         ... more configuration ...
71     });
72
73 ### `app/view/SongInfo.js`
74
75     Ext.define('Panda.view.SongInfo', {
76         extend: 'Ext.panel.Panel',
77         alias: 'widget.songinfo',
78         tpl: '<h1>About </h1><p></p>',
79         ... more configuration ...
80     });
81
82 We have left out some of the configuration here since component configurations are not in the scope of this article.
83
84 In the above configurations, you’ll notice that we have three stores configured. These map to the store names prepared in the previous article. At this point we’ll go ahead and create our stores.
85
86 {@img stores.png}
87
88 ## The models and stores
89
90 Often, it is useful to start with static json files containing mock data to act as our server side. Later, we can use these static files as a reference when we actually implement a dynamic server side.
91
92 For our app, we decided to use two models, Station and Song. We also need three stores using these two models that will be bound to our data components. Each store will load its data from the server side. The mock data files would look something like the following.
93
94 ## Static data
95
96 ### `data/songs.json`
97
98     {
99         'success': true,
100         'results': [
101             {
102                 'name': 'Blues At Sunrise (Live)',
103                 'artist': 'Stevie Ray Vaughan',
104                 'album': 'Blues At Sunrise',
105                 'description': 'Description for Stevie',
106                 'played_date': '1',
107                 'station': 1
108             },
109             ...
110         ]
111     }
112
113 ### `data/stations.json`
114
115     {
116         'success': true,
117         'results': [
118             {'id': 1, 'played_date': 4, 'name': 'Led Zeppelin'},
119             {'id': 2, 'played_date': 3, 'name': 'The Rolling Stones'},
120             {'id': 3, 'played_date': 2, 'name': 'Daft Punk'}
121         ]
122     }
123
124 ### `data/searchresults.json`
125
126     {
127         'success': true,
128         'results': [
129             {'id': 1, 'name': 'Led Zeppelin'},
130             {'id': 2, 'name': 'The Rolling Stones'},
131             {'id': 3, 'name': 'Daft Punk'},
132             {'id': 4, 'name': 'John Mayer'},
133             {'id': 5, 'name': 'Pete Philly &amp; Perquisite'},
134             {'id': 6, 'name': 'Black Star'},
135             {'id': 7, 'name': 'Macy Gray'}
136         ]
137     }
138
139 ## Models
140
141 Models in Ext JS 4 are very similar to Records which we had in Ext JS 3. One key difference is that you can now specify a proxy on your model, as well as validations and associations. The Song model for our application in Ext JS 4 would look like this.
142
143 ### `app/model/Song.js`
144
145     Ext.define('Panda.model.Song', {
146         extend: 'Ext.data.Model',
147         fields: ['id', 'name', 'artist', 'album', 'played_date', 'station'],
148
149         proxy: {
150             type: 'ajax',
151             url: 'data/recentsongs.json',
152             reader: {
153                 type: 'json',
154                 root: 'results'
155             }
156         }
157     });
158
159 As you can see, we have defined the proxy on our model. It is generally good practice to do this as it allows you to load and save instances of this model without needing a store. Also, when multiple stores use this same model, you don’t have to redefine your proxy on each one of them.
160
161 Let’s go ahead and also define our Station model.
162
163 ### `app/model/Station.js`
164
165     Ext.define('Panda.model.Station', {
166         extend: 'Ext.data.Model',
167         fields: ['id', 'name', 'played_date'],
168
169         proxy: {
170             type: 'ajax',
171             url: 'data/stations.json',
172             reader: {
173                 type: 'json',
174                 root: 'results'
175             }
176         }
177     });
178
179 ## Stores
180
181 In Ext JS 4, multiple stores can use the same data model, even if the stores will load their data from different sources. In our example, the Station model will be used by the SearchResults and the Stations store, both loading the data from a different location. One returns search results, the other returns the user’s favorite stations. To achieve this, one of our stores will need to override the proxy defined on the model.
182
183 ### `app/store/SearchResults.js`
184
185     Ext.define('Panda.store.SearchResults', {
186         extend: 'Ext.data.Store',
187         requires: 'Panda.model.Station',
188         model: 'Panda.model.Station',
189
190         // Overriding the model's default proxy
191         proxy: {
192             type: 'ajax',
193             url: 'data/searchresults.json',
194             reader: {
195                 type: 'json',
196                 root: 'results'
197             }
198         }
199     });
200
201 ### `app/store/Stations.js`
202
203     Ext.define('Panda.store.Stations', {
204         extend: 'Ext.data.Store',
205         requires: 'Panda.model.Station',
206         model: 'Panda.model.Station'
207     });
208
209 In the SearchResults store definition, we have overridden the proxy defined on the Station model by providing a different proxy configuration. The store’s proxy is used when calling the store’s load method instead of the proxy defined on the model itself.
210
211 Note that you could implement your server side to have one API for retrieving both search results and the user’s favorite stations in which case both stores could use the default proxy defined on the model, only passing different parameters to the request when loading the stores.
212
213 Lastly, let’s create the RecentSongs store.
214
215 ### `app/store/RecentSongs.js`
216
217     Ext.define('Panda.store.RecentSongs', {
218         extend: 'Ext.data.Store',
219         model: 'Panda.model.Song',
220
221         // Make sure to require your model if you are
222         // not using Ext JS 4.0.5
223         requires: 'Panda.model.Song'
224     });
225
226 Note that in the current version of Ext JS, the 'model' property on a store doesn’t automatically create a dependency, which is why we have to specify requires in order to be able to dynamically load the model.
227
228 Also, for convention, we always try to pluralize the store names, while keeping the model names singular.
229
230 ## Adding the stores and models to our application
231
232 Now that we have defined our models and stores, it’s time to add them to our application. Let’s revisit our Application.js file.
233
234 ### `app/Application.js`
235
236     Ext.application({
237         ...
238         models: ['Station', 'Song'],
239         stores: ['Stations', 'RecentSongs', 'SearchResults']
240         ...
241     });
242
243 Another advantage of using the new Ext JS 4 MVC package is that the Application will automatically load the stores and models defined in the stores and models configurations. Then, it will create an instance for each store loaded, giving it a storeId equal to its name. This allows us to use the name of the store whenever we bind it to a data component like we did in our views, e.g. store: 'SearchResults'.
244
245 ## Applying the glue
246
247 Now that we have our views, models and stores, it’s time to glue them together. You start by adding the views one by one to your viewport. This will make it easier to debug any wrong view configurations. Let’s go through the resulting viewport for the Panda app.
248
249     Ext.define('Panda.view.Viewport', {
250         extend: 'Ext.container.Viewport',
251
252 Your Viewport class will usually want to extend Ext.container.Viewport. This will cause your app to take up all the available space in your browser window.
253
254         requires: [
255             'Panda.view.NewStation',
256             'Panda.view.SongControls',
257             'Panda.view.StationsList',
258             'Panda.view.RecentlyPlayedScroller',
259             'Panda.view.SongInfo'
260         ],
261
262 We set up all the view dependencies in our viewport. This will allow us to use their xtypes, previously configured in our views using the alias property.
263
264         layout: 'fit',
265
266         initComponent: function() {
267             this.items = {
268                 xtype: 'panel',
269                 dockedItems: [{
270                     dock: 'top',
271                     xtype: 'toolbar',
272                     height: 80,
273                     items: [{
274                         xtype: 'newstation',
275                         width: 150
276                     }, {
277                         xtype: 'songcontrols',
278                         height: 70,
279                         flex: 1
280                     }, {
281                         xtype: 'component',
282                         html: 'Panda<br>Internet Radio'
283                     }]
284                 }],
285                 layout: {
286                     type: 'hbox',
287                     align: 'stretch'
288                 },
289                 items: [{
290                     width: 250,
291                     xtype: 'panel',
292                     layout: {
293                         type: 'vbox',
294                         align: 'stretch'
295                     },
296                     items: [{
297                         xtype: 'stationslist',
298                         flex: 1
299                     }, {
300                         html: 'Ad',
301                         height: 250,
302                         xtype: 'panel'
303                     }]
304                 }, {
305                     xtype: 'container',
306                     flex: 1,
307                     layout: {
308                         type: 'vbox',
309                         align: 'stretch'
310                     },
311                     items: [{
312                         xtype: 'recentlyplayedscroller',
313                         height: 250
314                     }, {
315                         xtype: 'songinfo',
316                         flex: 1
317                     }]
318                 }]
319             };
320
321             this.callParent();
322         }
323     });
324
325 Since Viewport extends Container, and Containers can’t have docked items (yet), we have added a Panel as the single item of our viewport. We make this panel the same size as our viewport by defining a layout of fit.
326
327 In terms of architecture, one of the most important things to note here is the fact that we have not defined a layout-specific configuration in the actual views. By not defining properties like flex, width, height in the views, we can easily adjust the application’s overall layout in one single place, adding to the maintainability and flexibility of our architecture.
328
329 ## Application logic
330
331 In Ext JS 3, we often added our application’s logic to the views themselves using handlers on buttons, binding listeners to subcomponents, and overriding methods on the views when extending them. However, just like you shouldn’t inline CSS styles in your HTML markup, it’s preferable to separate the application’s logic from the view definitions. In Ext JS 4, we provide controlleres in the MVC package. They are responsible for listening to events fired by the views and other controllers, and for implementing application logic to act on those events. There are several benefits to this design.
332
333 One benefit is that your application logic is not bound to instances of views which means we can destroy and instantiate our views, as needed, while the application logic continues processing other things, like synchronizing data.
334
335 Additionally in Ext JS 3, you might have had many nested views, each adding layers of application logic. By moving the application logic to controllers, it is centralized, making it easier to maintain and change.
336
337 Finally, the Controller base class provides you with lots of functionality, making it easier to implement your application logic.
338
339 ## Creating our Controllers
340
341 Now that we have the basic architecture for our UI, models and stores set up, it’s time to get in control of our application. We planned to have two controllers, Station and Song, so let’s create the definitions for them.
342
343 ### `app/controller/Station.js`
344
345     Ext.define('Panda.controller.Station', {
346         extend: 'Ext.app.Controller',
347         init: function() {
348             ...
349         },
350         ...
351     });
352
353 ### `app/controller/Song.js`
354
355     Ext.define('Panda.controller.Song', {
356         extend: 'Ext.app.Controller',
357         init: function() {
358             ...
359         },
360         ...
361     });
362
363 When including the controllers in your application, the framework will automatically load the controller and call the init method on it. Inside the init method, you should set up listeners for your view and application events. In larger applications, you might want to load additional controllers at runtime. You can do this by using the getController method.
364
365     someAction: function() {
366         var controller = this.getController('AnotherController');
367
368         // Remember to call the init method manually
369         controller.init();
370     }
371
372 When you load additional controllers at runtime, you have to remember to call the init method on the loaded controller manually.
373
374 For the purposes of our example application, we’ll let the framework load and initialize our controllers by adding them to the controllers array in our application definition.
375
376 ### `app/Application.js`
377
378     Ext.application({
379         ...
380         controllers: ['Station', 'Song']
381     });
382
383 ## Setting up listeners
384
385 Let’s start controlling some parts of our UI by using the control method inside of the controller’s init function.
386
387 ### `app/controller/Station.js`
388
389     ...
390     init: function() {
391         this.control({
392             'stationslist': {
393                 selectionchange: this.onStationSelect
394             },
395             'newstation': {
396                 select: this.onNewStationSelect
397             }
398         });
399     }
400     ...
401
402 The control method is passed an object where the keys are component queries. In our example, the component queries are just using the xtypes of our views. However, using these component queries, you can target very specific parts of your UI. To learn more about advanced component queries, you can refer to the API docs.
403
404 Each query is bound to a listener configuration. Inside each listener configuration, we want to listen for the key which is the event name. The events available are the ones provided by the component that is targeted by your query. In this case, we use the selectionchange event provided by Grid (from which our StationsList view extends) and the select event provided by ComboBox (from which our NewStation view extends). To find out which events are available for a particular component, you can look in the events section available for each component in the API docs.
405
406 {@img apidocs-events.png}
407
408 The value in the listener configuration is the function that gets executed whenever that event fires. The scope of this function is always the controller itself.
409
410 Let’s also set up some listeners in our Song controller.
411
412 ### `app/controller/Song.js`
413
414     ...
415     init: function() {
416         this.control({
417             'recentlyplayedscroller': {
418                 selectionchange: this.onSongSelect
419             }
420         });
421
422         this.application.on({
423             stationstart: this.onStationStart,
424             scope: this
425         });
426     }
427     ...
428
429 In addition to listening for the selectionchange event on our RecentlyPlayedScroller view, we also set up a listener for an application event here. We do this by using the on method on the application instance. Each controller has access to the application instance using the this.application reference.
430
431 Application events are extremely useful for events that have many controllers. Instead of listening for the same view event in each of these controllers, only one controller listens for the view event and fires an application-wide event that the others can listen for. This also allows controllers to communicate with one another without knowing about or depending on each other’s existence.
432
433 Our Song controller is interested in a new station being started because it needs to update the song scroller and song info whenever this happens.
434
435 Let’s take a look at how the Station controller, which will be the one responsible for firing this stationstart application event, actually does this.
436
437 ### `app/controller/Station.js`
438
439     ...
440     onStationSelect: function(selModel, selection) {
441         this.application.fireEvent('stationstart', selection[0]);
442     }
443     ...
444
445 We simply get the single selected item provided by the selectionchange event and pass it as the single argument when firing the stationstart event.
446
447 ## Conclusion
448
449 In this article, we have looked at the basic techniques of architecting your application. Of course, there is a lot to it, and in the next part of this series we will take a look at some more advanced controller techniques and continue wiring up our Panda app by implementing our controller actions and adding some more details to our views.