четверг, 26 февраля 2015 г.

Дерево как модель, хранилище и панель

Вложенные структуры наподобие папок с файлами являются излюбленным средством организации данных. Ниже приведу выжимку сведений, которые необходимо знать для отображения иерархий при помощи ExtJs 5. Ограничусь гомогенными деревьями, которые состоят из записей одного типа. Гетерогенные деревья имеют свои особенности.


Модель

Наследуется от TreeModel. Легко по ошибке унаследовать от обычной модели, тогда появляются странные ошибки в консоли.

Прокси для предпочтительнее определять в модели. Хранилища умеют пользоваться ими тоже. Для обычной таблицы достаточно вместо JSON-объекта вернуть массив, чтобы в хранилище было создано несколько записей вместо одной. У деревьев иной принцип. Они загружают дочерние узлы для корневого узла, который определяется в конфигурации хранилища.

Модели типа TreeModel предоставляют интерфейс узлов дерева - NodeInterface. Помимо объявленных в определении полей ExtJs автоматически создаёт дополнительные поля, такие как loaded, expanded и проч. Важно, чтобы не возник конфликт в названии имён. Например, при отображении файлового хранилища из таблицы базы данных может считываться поле loaded, которое содержит дату загрузки файла пользователем. ExtJs поймёт это поле по-своему: поступившая с сервера запись уже присутствует в хранилище и повторно загружать её не следует. Некоторые поля интерфейса NodeInterface придётся дописывать на стороне сервера. По умолчанию узлы дерева отображаются в виде папок. Чтобы отобразить узел в виде документа, нужно на сервере дописать параметр leaf со значением true. Чтобы папка отразилась в развёрнутом виде, нужно добавить параметр expanded = true. Иногда можно определить значение этих полей на стороне браузера. В этом случае задействуется свойство ридера transform.

Хранилище

Сервер должен присылать данные в определённом формате. Бывает не сразу понятно, откуда берутся названия полей. Ниже показаны настройки древовидного хранилища, которые можно было бы и не указывать, но при объяснении они значительно облегчают понимание. Обратите внимание на defaultRootProperty и на параметр rootProperty в определении прокси, данном выше. Параметр nodeParam актуален для AJAX-прокси. при первой загрузке данных в хранилище на сервер уйдёт GET-запрос, в котором node=0, а не id=0, как можно было бы решить на основании нижеследующего определения.

Может случится так, что узлы верхнего уровня в таблице базы данных имеют parentId = NULL. Обязательно нужно преобразовать NULL в 0, иначе эти узлы не отобразятся внутри корневого элемента, имеющего id=0.

Свойство parentIdProperty позволяет отсылать с сервера данные в виде "плоской" структуры, а не вложенной, использующей параметр children. К сожалению, браузер не запросит родительские узлы автоматически, поэтому смысла в нём я вижу мало. Принцип загрузки не изменяется. Всё равно нужно предоставлять в ответе сервера данные всех дочерних и родительских узлов.

Панель

 Про панель нужно знать, что она близка по возможностям к обычной таблице. Наряду с колонкой типа treecolumn, можно добавить и другие типы колонок. В колонке, отображающей вложенную структуру обычно показывается несколько полей модели, поэтому вместо dataIndex внизу используется настройка renderer.

Некоторые вещи достигаются через viewConfig или через getView. Например, прокрутка к найденному узлу(=строке): myTreePanel.getView().focusRow(myNodeRecord).



Результат

Щелчёк по папке загружает новую порцию дочерних узлов.


Первый запрос


Ответ сервера



Фильтр при помощи store.load()



Фильтр при помощи store.setFilters()

Фильтр при помощи store.setRoot()


Ответ сервера при фильтрации



вторник, 3 февраля 2015 г.

ViewController продолжение

Ранее был представлен один из способов работы с контроллерами представлений. Предлагаю вашему вниманию ещё одну технику работы с ними. Она хороша тем, что созданное представление можно красиво встраивать в другие места приложения.

Основная идея заключается в том, что контроллер представления не самостоятельно выбрасывает события, а опосредованно - через привязанное к нему представление.

this.getView().fireEvent('myEventName');

Благодаря этому, когда мы будем встраивать это представление через xtype в другой кусок приложения, обработчики можно будет назначить обычным способом через listeners, не привлекая настройку config другого контроллера.

items: [{
    xtype: 'myComponent'
    listeners: {
         myEventName: 'handlerMethodInideController'
    }
}]



понедельник, 2 февраля 2015 г.

Создаём HTML-компоненты в ExtJs 5.1

Спектр компонентов, которые ExtJs 5 предоставляет нам "из коробки", довольно обширен. Тем не менее иногда необходимо создать собственный компонент.

1. Создаём класс 'MyComponent' для нашего компонента.

Ext.define('MyApp.view.MyComponent', {});


2. Наследуемся от Ext.Component

Ext.define('MyApp.view.MyComponent', {
    extend: 'Ext.Component'
});

Subclassing

3. Обозначаем псевдоним 'my-component' класса, чтобы иметь возможность подключать наш компонент через xtype в состав контейнеров.

Ext.define('MyApp.view.MyComponent', {
    extend: 'Ext.Component',
    alias: 'widget.my-component'

});

XTypes and Lazy Instantiation

4. Определяем HTML-шаблон:

Ext.define('MyApp.view.MyComponent', {
    extend: 'Ext.Component',
    alias: 'widget.my-component',

    tpl: [
        '<div class="my-wrapper">',
        '<div class="my-content">{content}</div>',
        '</div>'
    ]
});



Ext.Component.tpl
Ext.XTemplate


5. Добавим к компоненту свойство 'myWrapper', которое будет предоставлять доступ к элементу внутри компонента.

Ext.define('MyApp.view.MyComponent', {
    extend: 'Ext.Component',
    alias: 'widget.my-component',

    tpl: [
        '<div class="my-wrapper">',
        '<div class="my-content">{content}</div>',
        '</div>'
    ],
    childEls: {
        myWrapper: {
             selectNode: '.my-wrapper'
        }
    }

});

data-ref
Ext.Component.childEls
Ext.util.ElementContainer
Ext.Element

6. Заполняем шаблон исходными данными. Обязательно нужно предоставить набор дефолтных значений, которые позволят заполнить шаблон. Если опустить параметр data, то дочерние элементы (childEls) будут равны null.

Ext.define('MyApp.view.MyComponent', {
    extend: 'Ext.Component',
    alias: 'widget.my-component',

    tpl: [
        '<div class="my-wrapper">',
        '<div class="my-content">{content}</div>',
        '</div>'
    ],
    childEls: {
        myWrapper: {
             selectNode: '.my-wrapper'
        }
    },
    data: {
         content: 'bla bla bla'
    }
});

Ext.Component.data

7. Назначим обработчик события click в элементе myWrapper нашего компонента. Важно использовать метод-шаблон afterRender.

Ext.define('MyApp.view.MyComponent', {
    extend: 'Ext.Component',
    alias: 'widget.my-component',

    tpl: [
        '<div class="my-wrapper">',
        '<div class="my-content">{content}</div>',
        '</div>'
    ],
    childEls: {
        myWrapper: {
             selectNode: '.my-wrapper'
        }
    },
    data: {
         content: 'bla bla bla'
    },
    afterRender: function() {
        this.callParent(arguments);
        this.myWrapper.on('click', function() {
            this.fireEvent('click', this);
        }, this);
    }
});

Template Methods

Работающий пример в Fiddle. Щёлкаем по bla bla bla, и надпись меняется на ups!

Что нужно учитывать? Во-первых, свойство data лучше определить внутри шаблонного метода initComponent. Во вторых, после заполнения шаблона методом update DOM-элементы создаются заново, поэтому придётся заново определить обработчики DOM-событий, в связи с чем есть смысл завести для этой цели отдельную функцию. В-третьих, обновлять шаблон нужно полным набором данных.

Ext.define('MyApp.view.MyComponent', {
    extend: 'Ext.Component',
    alias: 'widget.my-component',

    tpl: [
        '<div class="my-wrapper">',
        '<div class="my-content">{content}</div>',
        '</div>'
    ],
    childEls: {
        myWrapper: {
             selectNode: '.my-wrapper'
        }
    },
    data: {
         content: 'bla bla bla'
    },
    afterRender: function() {
        this.callParent(arguments);
        this.setMyListeners(); // 2 
    },
    initComponent: function() {
        this.data.content = 'lalala'; // 1
        this.update(this.data); // 3
        this.setMyListeners(); // 2
        this.callParent(arguments);
    },
    setMyListeners: function() { // 2
        this.myWrapper.on('click', function() {
            this.fireEvent('click', this);
        }, this);
    } 
});


Чтобы вносить изменения в щедящем режиме, используем this.getEl().

this.getEl().down('form').dom.setAttribute('action', url);
 

this.getEl().down('iframe').addListener(
    'load', 
    function(event, iframe) {
        //...
     }

);

Ещё заглянем в код библиотеки, посмотрим, как сделано там.

FileButton Code


Хорошее объяснение на английском