Product placement

Here's the code for the list of products:

// app/view/product/List.js
Ext.define('Alcohology.view.product.List', {
    extend: 'Ext.Panel',
    controller: 'product',
    xtype: 'product-list',
    cls: 'product-list',
    viewModel: 'product',
    tbar: [
        {
            xtype: 'combo',
            store: Ext.create('Ext.data.Store', {
                fields: ['text', 'field', 'direction'],
                data : [
                    { text: 'Date Added', property: 'id', direction: 'DESC' },
                    { text: 'Name A-Z', property: 'name', direction: 'ASC' },
                    { text: 'Name Z-A', property: 'name', direction: 'DESC' },
                    { text: 'Price ASC', property: 'price', direction: 'ASC' }
                ]
            }),
            displayField: 'text',
            queryMode: 'local',
            fieldLabel: 'Sort By',
            emptyText: 'None',
            editable: false
        }
    ],
    items: [
        { 
            xtype: 'dataview', itemId: 'productListView', 
            emptyText: '<span class="empty">No Products Found.</span>',
            itemSelector: '.product', bind: '{products}',
            tpl: '<tpl for="."><div class="product"><h2>{name}</h2><img src="/resources/product-images/{imagePath}-thumb.jpg" /><p>&pound;{price}</p></div></tpl>',
        }
    ],

    constructor: function() {
        this.callParent(arguments);
        
        this.add(Ext.create('Alcohology.view.product.Detail', {
            reference: 'productWindow'
        }));
    }
});

The top toolbar for the product list contains a combo box with an inline store containing available sort options. Note that we include the property to sort against and the direction of the sort so that we can pass these straight through to the server later.

There's a case to be made for this combo to be extracted into a separate class or the store to be set up on the view model; it might make this class a bit clearer. On the other hand, the proliferation of files and classes for their own sake will make things less clear too, so we'll keep it inline.

The real work in this class is performed by the dataview bound to a products store on the view model. Note how again that we're creating a window in the constructor of this class too, which will enable it to use the same view controller as the product list dataview.

Here's the code for this window (it's the one that shows details of a product):

// app/view/product/Detail.js
Ext.define('Alcohology.view.product.Detail', {
    extend: 'Ext.Window',
    modal: true,
    header: false,
    resizable: false,
    autoScroll: true,
    height: 600,
    width: 800,
    layout: 'column',
    cls: 'product-detail',
    items: [
        {
            xtype: 'container',
            columnWidth: 0.5,
            defaults: {
                xtype: 'component',
                bind: { data: '{currentProduct}' }
            },
            items: [
                { 
                    xtype: 'container', 
                    tpl: '<img src="/resources/product-images/{imagePath}-thumb.jpg" />' 
                },
                { tpl: '<ul><li>{features}</li></ul>' }
            ]
        },
        {
            xtype: 'container',
            columnWidth: 0.5,
            defaults: {
                xtype: 'component',
                bind: { data: '{currentProduct}' }
            },
            items: [
                { tpl: new Ext.Template('<h1>{name}</h1>',
                    '<h2 class="brewery">{brewery}</h2>',
                    '<h2><p class="price">&pound;{price}</p>',
                    '<p class="previousPrice">Was: &pound;{previousPrice}</p>',
                    '</h2>') },
                { tpl: '<div class="description">{description}</div>' }
            ]
        }
    ],

    bbar: [
        { text: 'Back', itemId: 'close', glyph: 0xf190 },
        '->',
        { text: 'Add to Cart', itemId: 'addToCart', glyph: 0xf07a }
    ]
});

There's a neat little trick used in this class. The window is split into two using a column layout and filled with a number of components that have their data config bound to the currentProduct on the view model. By using the tpl config on these components to set up an HTML template, each pane in the window can pull properties from the currentProduct and they'll be incorporated in the template. This gives us a hybrid approach that leverages the Ext JS column layout and standard HTML/CSS for customization.

Note

In bbar for this window, we use the glyph property to set FontAwesome icons on the buttons using the unicode character code of the icon in question.

The view controller that works with the product list and detail has a couple of interesting features as follows:

// app/view/product/ProductController.js
Ext.define('Alcohology.view.product.ProductController', {
    extend: 'Ext.app.ViewController',
    alias: 'controller.product',
    listen: {
        component: {
            '#productListView': { 'itemclick': 'onProductClick' },
            '#close': { 'click': 'onProductClose' },
            '#addToCart': { 'click': 'onAddToCart' },
            'combo': { 'select': 'onSortSelect' }
        }
    },

    routes : {
        'product/:id': 'onProductRoute',
        'category/:id': 'onCategoryRoute'
    },

    onSortSelect: function(combo, records) {
        if(records.length > 0) {
            var prop = records[0].get('property'),
                dir = records[0].get('direction'),

            this.getViewModel().set('sortProperty', prop);
            this.getViewModel().set('sortDirection', dir);
        }
    },

    onCategoryRoute: function(id) {
        var cfg = { reference: 'Alcohology.model.Category', id: id };
        this.getViewModel().linkTo('currentCategory', cfg);
        this.lookupReference('productWindow').hide();
    },

    onProductRoute: function(id) {
        var cfg = { reference: 'Alcohology.model.Product', id: id };
        this.getViewModel().linkTo('currentProduct',  cfg);
        this.lookupReference('productWindow').show();
    },

    onProductClick: function(view, record, el) {    
        this.redirectTo('product/' + record.getId());
    },

    onProductClose: function() {
        var id = this.getViewModel().get('currentCategory').getId();
        this.redirectTo('category/' + id);
    },

    onAddToCart: function() {
        var product = this.getViewModel().get('currentProduct'),

        this.getViewModel().get('cart').addProduct(product);

        Ext.toast('Product Added'),
    }
});

After wiring up event listeners and routes, we have the onSortSelect method that handles the user's selection of a sort option. We pick out the values we need and send them to the view model.

The routing handles on this view controller: onCategoryRoute and onProductRoute deal with the selection of a category (which shows a list of products) and the selection of a product (which shows a single product), and do so using a technique (which is new to us).

By using the linkTo method, we tell Ext JS to load the record with the specified ID if it's not already loaded it before. By doing this, we save the manual labor of loading the record ourselves. It's a neat shortcut that lets us set currentProduct and currentCategory on the view model with minimal code.

The onProductClick and onProductClose methods use redirectTo and hand off the real behavior to the relevant routes. The onAddToCart method grabs the cart store from the view model and uses the addProduct method that we created back in our data layer to push the current product into the cart.

Finally, we have the product view model:

// app/view/product/ProductModel.js
Ext.define('Alcohology.view.product.ProductModel', {
    extend: 'Ext.app.ViewModel',
    alias: 'viewmodel.product',
    links: {
        currentCategory: {
            type: 'Alcohology.model.Category',
            id: 1
        }
    },
    data: {
        sortProperty: 'id',
        sortDirection: 'ASC'
    },
    stores: {
        products: {
            type: 'products',
            autoLoad: true,
            remoteFilter: true,
            remoteSort: true,
            sorters: [{
                property: '{sortProperty}',
                direction: '{sortDirection}'
            }],
            filters: [{
                property: 'categoryId',
                value: '{currentCategory.id}'
            }]
        }
    }
});

The links configuration sets up the initial category to be loaded; Ext JS will do this automatically and anything that's been bound to it will be able to make use of it as soon as the load is complete. No manual intervention here is required; just wire up the configuration and go.

The data object contains the default values for product sorting and you can see that these are used by the products store and sent off to the server thanks to remoteSort. The product store is used to power the list of products in a category, and to this end it has a filter that's bound to the ID of currentCategory. This gets sent along with the sort options as JSON.

Categories and products are taken care of. It's time to move on to the shopping cart UI.

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

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