A room with a view

We mentioned that it was a good idea to design your application (starting with the data layer and then moving to the views) so that it's easier to understand the interactions that your controllers will have to deal with. When moving from the design to the code, the same applies, so we will write the user interface for this application and then later wire it up to the data via controllers.

First up, we need a viewport. We only have one page in the CMS, so the viewport is the container for all of the individual subviews (such as the tree and the detail panel). This application is fairly focused, so we're going to put all our views and associated classes under the ArchitectureCms.view.main.* namespace. Here's the code for our ArchictureCms.view.main.Main viewport:

// app/view/main/Main.js
Ext.define('ArchitectureCms.view.main.Main', {

    extend: 'Ext.panel.Panel',
 requires: [
        'ArchitectureCms.view.main.Detail',
        'ArchitectureCms.view.main.Tree'
    ],
    
    session: true,

    controller: 'main',
    viewModel: 'page',
    
    title: 'Architect CMS',
    bind: { title: 'Architect CMS - Currently Editing "{currentPage.text}"' },

    layout: 'border',

    items: [
        { xtype: 'page-detail', region: 'center', reference: 'detail' },
        { xtype: 'page-tree', region: 'west', width: 300, reference: 'tree', split: true }
    ]
});

This is mostly straightforward (we extend Ext.Panel rather than Ext.Container to give us support for a title bar). Next up, we require the view classes we're going to use in the viewport.

The session option is set to true. We'll discuss this in more detail shortly.

The view controller and view model are specified by their aliases; we'll create these classes later. Sencha Cmd knows that these are "auto-dependencies", so we will automatically require them without having to include them in the requires array.

We create a default title, namely, Architect CMS, but in the next line, we have our first use of the bind option. Let's break down what's happening here. We've already specified a view model for this class and always have to bind to a value in a view model. Not only this, the bind option is only triggered when the view model value changes, which is why we need to specify a default value via the title configuration. For the bind configuration, we specify the values we want to bind against (in this case title) and then provide a binding expression. Here, it's just a string. The segment in curly brackets determines the value on the view model to bind to. Later, we'll look at currentPage.text and see how this gets set, but it will suffice for now to realize that when this value changes; it gets incorporated into the value for title. We'll see something like this:

A room with a view

Note that this will happen without having to wire up any event handlers. It's a little sprinkle of magic that reduces the boilerplate code we have to write.

Next up, we specify a border layout and then fill the items array with the tree and detail panel, referencing them by their xtype. Thanks to our configuration of the requires option, Ext JS is already aware of these classes, so we can use the aliases as shorthand.

Other than the binding configuration and a bit of auto-requiring magic, there's nothing special happening here. The key, in terms of application design, is the introduction of the binding concept in association with view models and view controllers. Hopefully, we've shown how these ideas can be introduced with barely any additional code.

The tree panel and searching

Now that we've got our viewport container, we can introduce the views themselves. First, we'll look at the code for the tree that shows the page hierarchy:

// app/view/main/Tree.js
Ext.define('ArchitectureCms.view.main.Tree', {
    extend: 'Ext.tree.Panel',
    xtype: 'page-tree',
    rootVisible: false,
     tbar: [
        { xtype: 'textfield', emptyText: 'Search...', width: '100%', bind: { value: '{searchTerm}'}}
    ],

    bind: { store: '{pages}', searchFor: '{searchTerm}' },

    config: {
        searchFor: null
    },

    applySearchFor: Ext.emptyFn
});

More binding expressions! One important thing to realize is that a view model declared on a high-level component, in this case our ArchitectureCms.view.main.Main viewport, will cascade down and become available to child components. This means that our binding expressions in the tree will refer to the Page view model we assigned to the main viewport. What customer requirement are we trying to fulfill by using binding in the tree?

We want to be able to search for a page and have it highlighted in the tree. To do so, when we type in textfield, the value has to be passed to the tree. A traditional way of doing this would be to listen for a change event or keypress on textfield, then trigger a search method on the tree. Rather than doing this manually, we can use data binding via a view model to achieve the same effect:

The tree panel and searching

Data flows between UI components through the view model

The searchTerm value on the view model can flow back and forth between the searchFor config on the tree and the value on textfield. However, in this case, it's only one direction (from textfield down to the tree).

In addition, we tell the tree to bind to the pages value on the view model; we know we're going to need a list of pages from somewhere.

The missing piece in this puzzle is the part that actually does the searching on the tree. Thanks to the Ext JS configuration system, any config option that is specified also creates an applyConfigName method on the class instance and this is called every time the config option changes. This means that by creating applySearchFor on the tree, every time searchFor updates via its binding, we can run a piece of code to do something with the new value.

Note that we put a function placeholder in the last code snippet (the Ext.emptyFn part). Here's the actual code we're going to use here:

applySearchFor: function(text) {
    var root = this.getRootNode();
    var match = root.findChildBy(function(child) {
        var txt = child.get('text'),

        if(txt.match(new RegExp(text, 'i'))) {
            this.expandNode(child, true, function() {
                var node = this.getView().getNode(child);
                Ext.get(node).highlight();
            }, this);
        }
    }, this, true);
}

In brief, use a regular expression to do a case-insensitive match on the search term against the text of each tree node. If a match is found, expand the tree to this point and call its highlight method to produce a visual cue.

Pages in detail

The tree is used to browse the hierarchy of trees in the CMS, so we now need a way to look at the detail of each page. The detail pane is a panel containing a number of form fields:

Ext.define('ArchitectureCms.view.main.Detail', {
    extend: 'Ext.form.Panel',
    xtype: 'page-detail',
    defaultType: 'textfield',
    bodyPadding: 10,
    hidden: true,
    bind: {
        hidden: '{!currentPage}'
    },
    items: [
        { xtype: 'container', cls: 'ct-alert', html: 'This record is unsaved!', bind: { hidden: '{!isUnsavedPage}' } },
        { fieldLabel: 'Id', bind: '{currentPage.id}', xtype: 'displayfield'},
        { fieldLabel: 'Published', bind: '{currentPage.published}', xtype: 'checkboxfield' },
        { fieldLabel: 'Label', bind: '{currentPage.text}' },
        { fieldLabel: 'URL Stub', bind: '{currentPage.stub}' },
        { fieldLabel: 'Body', bind: { value: '{currentPage.body}' }, xtype: 'htmleditor' }
    ],
    bbar: [
        { text: 'Save', itemId: 'save' },
        { text: 'Add Child Page', itemId: 'addChild' },
        { text: 'Delete', itemId: 'delete' }
    ]
});

Each of the form fields has a binding expression, which ties the field value to a value on the currentPage object of the view model. When the user changes the field, the view model will automatically get updated. Note that we don't have to specifically state the property to bind to because form fields have their defaultBindProperty set to value.

The whole form panel has its hidden value bound to currentPage, so if this value is not set, the panel will be hidden. This allows you to hide the form when no page is selected. We've also got a warning message as the first item in the panel, which will be hidden when the view model's isUnsavedPage value changes to false.

We've only written a little bit of code outside the UI configuration, yet with the addition of the view model, we'll already have a populated tree panel with search tied to a detail panel. Next, we'll look at the view model code itself.

The magical page view model

This view model uses a simple formula to provide a calculated value to the view:

// app/view/main/PageModel.js
Ext.define('ArchitectureCms.view.main.PageModel', {
    extend: 'Ext.app.ViewModel',
    alias: 'viewmodel.page',

    requires: ['Architecture.store.Pages'],

    stores: {
        pages: {
            type: 'pages',
            session: true
        }
    },

    formulas: {
        isUnsavedPage: function(get) {
            return get('page.id').toString().indexOf('Unsaved-') > -1;
        }
    }
});

Considering the functionality that this class enables, that's very little code. The store definition is fairly self-explanatory, just using the ArchitectureCms.store.Pages alias to specify that the view model has a page value powered by this store.

The formulas definition is a little more interesting. It's a way of declaring that a value will be returned based on other values in the view model. In this case, as we specified on our model that newly created records would use a prefix of Unsaved-, we can look for this to determine whether the record's been saved to the server or not. So, isUnsavedPage returns true or false depending on whether the record's ID contains this prefix or not.

The only missing thing here is the currentPage value. We can set arbitrary values on the view model. So, this gets set elsewhere in the controller. Before we talk about this, let's jump back to discuss a new concept in Ext JS 5: Ext.data.Session.

This data is now in session

An Ext.data.Session is a way of centralizing data in an application, ensuring that stores are working with the same set of data without having redundant reloading. It also allows much easier batched updates and deletions.

In our application, we set session, set to true on our top-level viewport, which tells Ext JS to automatically create a session and make it available to any other code that requests it. This is the simplest way of constructing a session, although there's a lot more customization that we can do if need be.

The reason we use a session in this application is to allow us to link the data that the tree and the detail panel uses. This helps with data binding too; we can use exactly the same model instance in the tree and the detail panel, which means that updates made in the detail panel will flow through the view model and into the correct page instance in the tree. In a moment, when we look at our view controller, we'll use the session a little more and get a glimpse of how it can help manage your data.

The glue controlling all

In previous chapters, we've looked at how the controller can use event domains to hook into anything interesting happening elsewhere in the application. Here, we use the same approach, which we discussed previously, and get the controller to hook up a bunch of event handlers to deal with user actions in the user interface:

// app/view/main/MainController.js
Ext.define('ArchitectureCms.view.main.MainController', {
    extend: 'Ext.app.ViewController',
    alias: 'controller.main',

    requires: ['ArchitectureCms.model.Page'],

    init: function() {
        this.listen({
            component: {
                'treepanel': {
                   'select': 'onPageSelect'
                },
                'page-detail #save': {
                    click: 'onSaveClick'
                },
                'page-detail #addChild': {
                    click: 'onAddClick'
                },
                'page-detail #delete': {
                    click: 'onDeleteClick'
                }
            }
        });
    },


 onPageSelect: function(tree, model) {
        this.getViewModel().setLinks({
            currentPage: {
                type: 'Page',
                id: model.getId()
            }
        });
    },


    onAddClick: function() {
        var me = this;

        Ext.Msg.prompt('Add Page', 'Page Label', function (action, value) {
            if (action === 'ok') {
                var session = me.getSession(),
                    selectedPage = viewModel.get('currentPage'),
                    tree = me.lookupReference('tree'),

                var newPage = session.createRecord('Page', {
                    label: value,
                    text: value,
                    leaf: true
                });

                selectedPage.insertChild(0, newPage);
                tree.setSelection(newPage);
                tree.expandNode(selectedPage);
            }
        });
    },


    onDeleteClick: function() {
        var me = this;
        
        Ext.Msg.confirm('Warning', 'Are you sure you'd like to delete this record?', function(btn) {
            if(btn === 'yes') {
                me.getViewModel().get('currentPage').erase();
                me.getViewModel().set('currentPage', null);
                Ext.toast('Page deleted'),
            }
        }, this)
        
    },

    onSaveClick: function() {
        this.getViewModel().get('currentPage').save();
        Ext.toast('Page saved'),
    }
});

This view controller handles the following four events:

  • The select event on the tree handled by the onPageSelect method
  • The click event on the detail panel's save button handled by onSaveClick
  • The click event on the detail panel's add child button handled by onAddChildClick
  • The click event on the detail panel's delete button handled by onDeleteClick

Some of this will be self-explanatory, some relates to data binding, and some relates to the session. Let's break down the important parts.

Selecting a page

When the tree fires a select event, the view controller's onPageSelect method gets passed the model for the selected tree node. We mentioned earlier that we can set arbitrary values on the view model, specifically the currentPage value, and so this is what we do here, but with a twist.

Rather than just setting the data, we give Ext JS a hint that we want to set a model instance by using the links configuration. By supplying the name of the model class and its ID, Ext JS will use the matching instance if it's already available in the current Ext.data.Session or it'll automatically load it from the server. It's a handy shortcut for reducing the number of requests to the backend API and another example of how to use a session.

Adding a page

The view controller listens for events on a button with an item ID of #addChild. When it fires, we ask the user for the name of the new page, and the next step is to actually create a page record. Rather than using Ext.create, we call createRecord on the current Ext.data.Session, which allows you to continue to make Ext JS aware of the records we're managing. It also allows you to maintain a global understanding of the saved and unsaved records. This would be even more useful in an application where we need to do batch updates of records.

After creating a model instance, we follow the pseudocode we wrote earlier in the chapter, but tie it to actual Ext JS methods and add the page to the tree data structure before selecting it in the tree UI itself.

Deleting a page

This is fairly straightforward (handle the click event from the #delete button and then grab the currentPage from the view model). We also remove the leftover page from the view model so that the detail panel automatically clears itself, rather than leaving a dead record available to edit. We display a notification to the user with Ext.toast.

Saving a page

This is even simpler (handling a click on the #save button, grabbing the currentPage from the view model, and then calling its save method). There's nothing special happening here. The only thing to note is that if this is a new record, the server will respond with a new ID and replace the one that Ext JS automatically allocated. Thanks to the binding to isUnsavedPage on the view model, this will cause the "unsaved" message to disappear from the detail panel.

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

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