robert dougan

Software Architect at Sencha

Taking Advantage of ComponentView in Sencha Touch 2

19/12/2011



This blog post is redundant and no longer applies to latest version of Sencha Touch 2. Please read the official guide on the Sencha Touch 2 Documentation.

The GitHub project has been updated to the latest version of Sencha Touch 2. It is available here.

Sencha Touch 2 is still in preview (at the time of writing this), and although the release has been focused on performance improvements, there are a few new components that can be incredibly useful. One of the new components I love is Ext.dataview.ComponentView.

What is it?

Ext.dataview.ComponentView is basically a Ext.DataView. You bind a store to it and it shows it in a list-like fashion. However with ComponentView, each list item can be a component. As a result, the possibilities are incredible.

Please be aware that the following API may change before the Sencha Touch 2.0 final release.

Example

I've put together a simple MVC application to show you how Ext.dataview.ComponentView works. The source can be found here.

Oh, and our example application is going to have some kittens in it. NICE. So let's get started!

Structure

The MVC structure in Sencha Touch 2 is similar to Ext JS 4. You have an few important parts:

I'm not going to explain what this means here. There is a great post on the subject here and I'm sure there will be several guides in the future when Sencha Touch 2 is released.

Your Application

app.js

We need to define an app.js file so Sencha Touch knows we are creating an application. It is very simple to put together.

Ext.Loader.setConfig({enabled: true});
Ext.Loader.setPath('Ext', 'lib/touch2/src');

Ext.application({
    name: 'Example',

    controllers: ['Application'],
    views: ['Main'],

    launch: function() {
        Ext.Viewport.add({
            xclass: 'Example.view.Main'
        });
    }
});

Model

app/model/Kitten.js

Our example is going to have lots of kittens in it which will come from a store, so we obviously need a Kitten model.

Ext.define('Example.model.Kitten', {
    extend: 'Ext.data.Model',

    fields: [
        "name",
        "image",
        { name: "cuteness", type: 'int' }
    ]
});

Store

app/store/Kittens.js

Now we have a Kitten model, we need to setup a store to use that model and give it some data. Simple.

Ext.define('Example.store.Kittens', {
    extend: 'Ext.data.Store',
    model: 'Example.model.Kitten',

    data: [
        {
            name: 'Running kitten',
            image: 'data/images/kitten1.jpg',
            cuteness: 70
        },
        {
            name: 'Spreading legs kitten',
            image: 'data/images/kitten2.jpg',
            cuteness: 90
        },
        {
            name: 'Tongue kitten',
            image: 'data/images/kitten3.jpg',
            cuteness: 70
        },
        {
            name: 'Ginger kitten',
            image: 'data/images/kitten4.jpg',
            cuteness: 80
        },
        {
            name: 'Kitten friends',
            image: 'data/images/kitten5.jpg',
            cuteness: 20
        },
        {
            name: 'Milk kitten',
            image: 'data/images/kitten6.jpg',
            cuteness: 50
        }
    ]
});

Controller

app/controller/Application.js

We only have one controller in our application, and it isn't going to do much. For now, we just need to include our Kittens store and the KittensList view (which we'll create later).

Ext.define('Example.controller.Application', {
    extend: 'Ext.app.Controller',

    stores: ['Kittens'],
    views: ['KittensList']
});

Views

app/view/Main.js

We need to add a global view for application, which basically acts as a Viewport. We shall call this Main.js. There isn't much to it.

Ext.define('Example.view.Main', {
    extend: 'Ext.Container',

    requires: ['Example.view.KittensList'],

    config: {
        layout: 'fit',

        items: [
            { xclass: 'Example.view.KittensList' }
        ]
    }
});

app/view/KittensList.js

The first view we will create is Example.view.KittensList. It is going to extend Ext.dataview.ComponentView and it's not gonna do much.

So here's the code:

Ext.define('Example.view.KittensList', {
    extend: 'Ext.dataview.ComponentView',
    xtype: 'kittenlist',

    requires: ['Example.view.KittensListItem'],

    config: {
        cls: 'kitten-list',
        store: 'Kittens',
        defaultType: 'kittenslistitem'
    }
});

app/view/KittensListItem.js

This view is going to be used as the defaultType of the above Example.view.KittensList. There is a little bit of magic that goes on in this class, so I will go through it a little slower. Sorry about it's length (that's what she said).

So first up, the base of the class:

Ext.define('Example.view.KittensListItem', {
    extend: 'Ext.dataview.DataItem',
    xtype : 'kittenslistitem',

    config: {
        cls: 'kitten-list-item'
    }
});

So we have the basics of the application put together now. If you open it up in a browser, it will show a list with a bunch of items. Unfortunately, because the ListItem has no content, it will be blank.

So what we gotta do to make it display?

New Configurations

First up, we need to add new configurations for each of the different views we are going to have in each list item. To do this, we need to know what it is going to look like.

List Item Template

So you can see we are going to show an image of the kitten, the name of the kitten and a slider which will show the cuteness of the kitten; all inside the Examples.view.KittensListItem.

So let's go ahead and add a configuration for each one of these components:

config: {
    ...

    image: true,

    name: {
        cls: 'x-name',
        flex: 1
    },

    slider: {
        flex: 2
    }
}

So how do these configurations get transformed?

What we need to do is add apply and update methods for each of this configurations. Let me show you the code.

applyImage: function(config) {
    return Ext.factory(config, Ext.Img, this.getImage());
},

updateImage: function(newImage, oldImage) {
    if (newImage) {
        this.add(newImage);
    }

    if (oldImage) {
        this.remove(oldImage);
    }
}

The apply method (which is called when setImage is called) will use Ext.factory to take the passed configuration, and any existing instance (using getImage) and return a new instance of the type passed (in this case, Ext.Img).

Then in our updateImage method, we simply add the newImage into the list item, and remove oldImage item if one exists.

Make sense? If not, take a look at the Class System guide.

We need to now do the same for the other two configurations:

applyName: function(config) {
    return Ext.factory(config, Ext.Component, this.getName());
},

updateName: function(newName, oldName) {
    if (newName) {
        this.add(newName);
    }

    if (oldName) {
        this.remove(oldName);
    }
},

applySlider: function(config) {
    return Ext.factory(config, Ext.slider.Slider, this.getSlider());
},

updateSlider: function(newSlider, oldSlider) {
    if (newSlider) {
        this.add(newSlider);
    }

    if (oldSlider) {
        this.remove(oldSlider);
    }
}

The same idea again, only using Ext.Component and Ext.slider.Slider as the component of choice.

Requires

We are now using the Ext.Img and Ext.slider.Slider components in our list item, so we must require those classes so when this list item class is loaded, the required class are also loaded.

... 

requires: [
    'Ext.Img',
    'Ext.slider.Slider'
]

...

Note: this is not inside the config block.

Layout

Now we have items inside each list item, we must have them displayed properly. For this, we use the layout configuration and set it to hbox.

config: {
    ...

    layout: {
        type: 'hbox',
        align: 'center'
    }
}
dataMap

This is were the magic begins.

The dataMap configuration allows you to call functions with a value of a field, on functions of your list item.

Stumped? Let's look at the code:

config: {
    ...

    dataMap: {
        getImage: {
            setSrc: 'image'
        }
    }
}

So you can see we defined the dataMap configuration. It is just an object with a few keys and values. The key of each item is the method name you want to call. This method name must return something, and then with that returned value, it will call the key of the inner object with the value (which is a field name).

So with the above code, this is what happens when the record of the list item is updated:

Understand now? Good job!

So now we have figured this out, we need to add the correct dataMap for each of the fields so the whole item has the correct look.

config: {
    ...

    dataMap: {
        getImage: {
            setSrc: 'image'
        },

        getName: {
            setHtml: 'name'
        },

        getSlider: {
            setValue: 'cuteness'
        }
    }
}

Custom CSS

For this example I have used SCSS to make it look a little better.

As always, there isn't much too it:

//include sencha touch
@import 'sencha-touch/default/all';

//include layout and form parts of sencha touch
@include sencha-layout;
@include sencha-form-sliders;

//define a variable so we can have consistant padding throughout the app
$list-padding: .7em;

//body background
body {
    background: #eee;
}

//kitten list styling
.kitten-list {
    .x-dataview-inner {
        padding: $list-padding;
    }

    //kitten list item styling
    .kitten-list-item {
        padding: $list-padding/2;
        background: #fff;
        border: 1px solid #ccc;
        border-bottom-width: 0;

        //add rounded corners to the last item
        &:last-child {
            border-bottom-width: 1px;
            @include border-bottom-radius($list-padding/2);
        }
    }

    //add rounded corners to the first item
    .x-mask + .kitten-list-item {
        @include border-top-radius($list-padding/2);
    }

    .kitten-list-item .x-img {
        background: #eee;
        background-size: cover;
        background-position: center center;

        @include border-radius($list-padding/2);

        width: 80px;
        height: 80px;
    }

    .kitten-list-item .x-name {
        padding: 0 $list-padding/2;
    }
}

I've added inline comments to explain everything.

Before

Before

After

After

What if we want to know when the slider changed?

Each list item now has a slider, of which the value can change. So what if you want to know when that happens? Doing this is pretty simple.

Let's go back to our controller.

app/controller/Application.js

What we want to do is override our init method in our controller, so we can call control with any listeners we have on our views.

init: function() {
    this.control({
        'kittenslistitem slider': {
            change: this.onCutenessChange
        }
    });
},

onCutenessChange: function(slider, value) {
    //we should do something here
    console.log('onCutenessChange', value);
}

Now if you reload and move one of the sliders, you will get a billion logs of value being changed. NICE.

Incredible, but be careful

As you can see, the flexibility and power available when using the new Ext.dataview.ComponentView is incredible. This can be so very useful in many, many occasions.

But it's not all good..

So let's say we created this same example with a Ext.DataView (but without the slider). In total, your application would have 2 component; Main.js and KittensList.js. With Ext.dataview.ComponentView though, that number goes up to 20, and that is with only 6 rows in the store. This can drastically increase memory usage and the amount of DOM generated for all your list items. So you must watch out when using this.

Conclusion

If you have any issues, feel free to comment below; but only after you checkout the source over on GitHub (I've included loads of documentation in the actual source code).

That will be all.