An Ext JS application created with Sencha Cmd will set up the main view as a viewport filling the entire browser window. We'll use this view and adapt it to our needs, as shown in the following code:
// app/view/main/Main.js Ext.define('Alcohology.view.main.Main', { extend: 'Ext.Panel', xtype: 'app-main', controller: 'main', viewModel: 'main', layout: 'border', header: { xtype: 'app-header' }, items: [ { xtype: 'categories', width: 200, region: 'west' }, { xtype: 'product-list', region: 'center' } ], initComponent: function() { this.callParent(arguments); this.add(Ext.create('Alcohology.view.cart.Cart', { reference: 'cartWindow' })); this.add(Ext.create('Alcohology.view.account.Account', { reference: 'accountWindow' })); } });
Here we are! Our first view component, the panel that will contain everything else in the application. The header
config is set to a custom xtype
that we'll build later. The items in the panel are configured to use a border layout and consist of the category list and the product list.
There is one oddity here: adding windows to a panel within the initComponent
method. This provides two benefits:
lookupReference
This is a simple approach that solves an obvious sounding issue, that is, where do I create my windows? It doesn't feel "right" to put them in the items
config with the product and category list, although we certainly can without any ill effect. Another common solution is to instantiate the window in the view controller itself, but as the window then isn't a child of the main view, this leads to issue with the view model inheritance. Creating the windows in the initComponent
method feels like a natural way to bypass this problem.
We decided earlier that the main view controller will also handle events from the header, so let's look at the header view next:
// app/view/header/Header.js Ext.define('Alcohology.view.header.Header', { extend: 'Ext.panel.Header', xtype: 'app-header', cls: 'app-header', layout: 'hbox', title: 'alcohology.', items: [ { xtype: 'account-indicator', width: 80, bind: '{currentUser}' }, bind: { data: { count: '{cartCount}' } }} ] });
Our custom header component inherits from Ext.panel.Header
and implements an hbox
layout. The two items contained within are also custom classes, one for the cart icon and one for the account icon. These are configured to bind to currentUser
and cartCount
respectively, which are values in the main view model that we'll look at later.
The cart icon is called MiniCart
and it looks like this:
// app/view/header/MiniCart.js Ext.define('Alcohology.view.header.MiniCart', { extend: 'Alcohology.ux.ClickContainer', xtype: 'minicart', cls: 'mini-cart', tpl: new Ext.Template('<span style="font-family:FontAwesome;">', '</span> {count} items') });
In the header
component, we specified that the data
config for MiniCart
should be an object with a count
value. This count
value will be bound to a cartCount
value in the view model. In turn, we now use this count
value in the template, which allows you to have an icon that updates with the count of items in the cart.
There are a couple of other things to note here. We're using the FontAwesome
icon set to add a bit of graphical flair to the cart; you can see it being used in a span
tag in the tpl
configuration.
FontAwesome
can be found at http://fortawesome.github.io/Font-Awesome/.
The second point to note is that this class inherits from Alcohology.ux.ClickContainer
. What's this? Take a look at the following code:
// app/ux/ClickContainer.js Ext.define('Alcohology.ux.ClickContainer', { extend: 'Ext.Container', xtype: 'clickcontainer', listeners: { 'afterrender': function(me) { me.getEl().on('click', function() { me.fireEvent('click'), }); } } });
A normal container doesn't have a click
event, so this ClickContainer
hooks into the underlying element that allows you to handle user interaction with the container. This is handy if you don't need button styling and would like a bare-bones clickable component.
The account indicator also extends ClickContainer
as follows:
// app/view/header/AccountIndicator.js Ext.define('Alcohology.view.header.AccountIndicator', { extend: 'Alcohology.ux.ClickContainer', xtype: 'account-indicator', cls: 'account-indicator', config: { user: null }, defaultBindProperty: 'user', data: { label: 'Login' }, tpl: '<span style="font-family:FontAwesome;"></span> {label}', applyUser: function(user) { if(user) { this.setData({ label: user.email }); } } });
Our favorite trick of binding to a custom configuration option is again used here with a little twist. If the value of a user being bound is null
, that is, if the user has yet to log in, we use the default value of the data
config to set a label on this component. If they have logged in, we set the label to their e-mail address.
You can see in the tpl
configuration that we're using FontAwesome
again. It's also the place we use the label that has a default value of login
.
Let's get back to the code that handles the user's interactions with these components.
The main controller is not only the place that handles user clicks and taps, but the place that defines some relevant routes. It even handles a custom event. Let's take a look:
// app/view/main/MainController.js Ext.define('Alcohology.view.main.MainController', { extend: 'Ext.app.ViewController', alias: 'controller.main', listen: { component: { 'component[cls="mini-cart"]': { click: 'onCartClick' }, 'component[cls="account-indicator"]': { click: 'onAccountClick' }, }, controller: { '*': { loginrequired: 'onLoginRequired' } } }, routes: { 'account': 'onAccountRoute', 'cart': 'onCartRoute' }, onLoginRequired: function() { Ext.toast('Please login or register.'), this.redirectTo('account', true); }, onCartClick: function() { this.redirectTo('cart', true); }, onAccountClick: function() { this.redirectTo('account', true); }, onAccountRoute: function() { this.lookupReference('accountWindow').show(); }, onCartRoute: function() { this.lookupReference('cartWindow').show(); } });
There's a very handy technique demonstrated here: the click
handlers for account-indicator
and minicart
both simply redirect to their relevant routes. This means that we can put the logic to show the account and cart windows in onAccountRoute
and onCartRoute
route handlers.
The other piece of functionality implemented in this view controller is the listener on the controller domain. It listens for any controller firing the loginrequired
event and handles it with the onLoginRequired
method. Within onLoginRequired
, we pop up a brief note to the user via the Ext.toast
feature and simply redirect them to the login/registration
page.
This enables any controller or view controller to request the user to log in without having to be explicitly aware of the implementation of the account system. Let's take a look at the view model for the main viewport:
// app/view/main/MainModel.js Ext.define('Alcohology.view.main.MainModel', { extend: 'Ext.app.ViewModel', alias: 'viewmodel.main', stores: { cart: { type: 'cart' }, orders: { type: 'pastorders'} }, data: { cartCount: 0 }, constructor: function() { var me = this; me.callParent(arguments); me.get('cart').on('datachanged', function(store) { me.set('cartCount', store.count()); }); } });
This top-level view model provides the stores for past orders and the shopping cart as well as a property giving us the number of items in the shopping cart.
Due to the default in Ext JS, we have to manually listen to the datachanged
event on the cart store in order to get a "live" count of items because a change in the size of the store won't trigger a databind.
We've covered the "main" view and associated classes, so let's move on to the view that will list product categories.