Interface in your face

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:

  • The main view controller can refer to the windows using lookupReference
  • The windows will have access to the main view model via the view model inheritance

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.

One step ahead

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;">',
        '&#xf07a;</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.

Tip

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.

Tip

This feature can also be implemented as a mixin rather than a base class.

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.

Under the main control

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset