Содержание

Доступность (disable/enable)

В интерфейсах нередко возникает необходимость блокировать часть интерфейса, чтобы пользователь не мог совершить определенные действия. Для этого используется механизм доступности.

Если узел недоступен (disabled), он не должен реагировать на пользовательские действия. Недоступность не распространяется на логику приложения, то есть не влияет на действия, которые иницируются программно. Поэтому нужно проверять доступность узлов и игнорировать пользовательский ввод в зависимости от этого, если это необходимо.

Принцип работы

Для задания состояния "недоступен" используется свойство disabled. Для изменения значения этого свойства используются методы:

  • disable() – меняет значение на true;
  • enable() - меняет значение на false;
  • setDisabled(state) – где state приводится к boolean и это значение присваивается.

Все методы возвращают true, если значение было изменено, и false в противном случае.

var Node = require('basis.ui').Node;
var view = new Node();

console.log(view.disabled);
// > false

view.disable();
console.log(view.disabled);
// > true

view.enable();
console.log(view.disabled);
// > false

view.setDisabled(123);
console.log(view.disabled);
// > true

Значение свойства disabled может быть выставлено при создании экземпляра. По умолчанию оно равно false.

var Node = require('basis.ui').Node;
var foo = new Node();
var bar = new Node({ disabled: true });
var baz = new Node({ disabled: false });

console.log(foo.disabled);
// > false
console.log(bar.disabled);
// > true
console.log(baz.disabled);
// > false

Когда узел становится недоступным, то выбрасывается событие disable, а когда доступным – событие enable. События не выбрасываются при создании экземпляра.

var Node = require('basis.ui').Node;
var view = new Node({
  handler: {
    disable: function(){
      console.log('view is disabled');
    },
    enable: function(){
      console.log('view is enabled');
    }
  }
});

view.disable();
// > view is disabled

view.disable();
// события не будет, т.к. view уже недоступно

view.enable();
// > view is enabled

Контекст

На доступность узла влияет не только значение собственного свойство disabled, но и значения disabled вышестоящих узлов (ancestors). Другими словами зависит от контекста.

Узел со значением свойства disabled равным true влияет на свое поддерево, то есть на свои дочерние узлы, дочерние узлы дочерних узлов и т.д., это распространяется и на все сателлиты. Для распространения влияния используется свойство contextDisabled.

Введение дополнительного свойства обусловлено тем, что каждый узел имеет свое собственное значение свойства disabled, которое зависит от определенной логики. Его нельзя менять из-за изменений в контексте, иначе при разблокировке вышестоящего узла (установке disable в значение false) будет потеряно настоящее состояние узла, который, возможно, должен остаться заблокированным.

Значение свойства contextDisabled обновляется автоматически. Оно хранит true, если среди вышестоящих узлов есть хотя бы один с disabled равным true. Иначе в нем хранится false.

var Node = require('basis.ui').Node;
var list = new Node({
  childNodes: [
    new Node()
  ]
});

console.log(list.disabled);
// > false
console.log(list.contextDisabled);
// > false
console.log(list.firstChild.disabled);
// > false
console.log(list.firstChild.contextDisabled);
// > false

list.setDisabled(true);

console.log(list.disabled);
// > true
console.log(list.contextDisabled);
// > false
console.log(list.firstChild.disabled);
// > false
console.log(list.firstChild.contextDisabled);
// > true

Принцип обновления contextDisabled: при изменении свойства disabled узла, на то же значение меняется contextDisabled для всех нижестоящих узлов. При этом поддеревья узлов с disabled равным true игнорируются, так как такие узлы создают собственный контекст.

При отвязывании узла от его родителя или владельца contextDisabled не изменяется (чтобы избежать лишних событий). Но при привязывании узла к новому родителю или владельцу contextDisabled меняется согласно текущему состоянию контекста.

var Node = require('basis.ui').Node;
var child = new Node();
var list = new Node({
  disabled: true,  // список заблокирован по умолчанию
  childNodes: [
    child
  ]
});

console.log(child.contextDisabled);
// > true

list.removeChild(child);
console.log(child.contextDisabled);
// > true

list.enable();
list.appendChild(child);
console.log(child.contextDisabled);
// > false

Узел считается недоступным если его свойство disabled или свойство contextDisabled равно true. Пример функции проверяющей доступность:

function isViewDisabled(view){
  return view.disabled || view.contextDisabled;
}

Но в такой функции нет необходимости, так как у узлов есть метод isDisabled().

var Node = require('basis.ui').Node;
var list = new Node({
  childNodes: [
    new Node()
  ]
});

console.log(list.isDisabled());
// > false
console.log(list.firstChild.isDisabled());
// > false

list.disable();

console.log(list.isDisabled());
// > true
console.log(list.firstChild.isDisabled());
// > true

При изменении contextDisabled так же выбрасывается событие disable или enable. Фактически эти события выбрасываются, когда меняется возвращаемый результат метода isDisabled().

var Node = require('basis.ui').Node;
var LogNode = Node.subclass({
  emit_disable: function(){
    Node.prototype.emit_disable.call(this);
    console.log(this.name, 'disabled');
  },
  emit_enable: function(){
    Node.prototype.emit_enable.call(this);
    console.log(this.name, 'enabled');
  }
});

var list = new LogNode({
  name: 'list',
  childNodes: [
    new LogNode({ name: 'item1' }),
    new LogNode({ name: 'item2' })
  ],
  satellite: {
    foo: new LogNode({ name: 'foo' })
  }
});

list.disable();
// > item1 disabled
// > item2 disabled
// > foo disabled
// > list disabled

list.firstChild.disable();
// событий не будет, первый ребенок и так уже недоступен

list.enable();
// > item2 enabled
// > foo enabled
// > list enabled
// item1 остался недоступным

Использование контекста избавляет от необходимости делать недоступными каждое отдельное представление или элемент управления. Для этого достаточно сделать недоступным лишь одно "главное" представление отвечающее за контекст. Например, форма с полями: не нужно делать недоступным каждое поле, достаточно сделать недоступной только форму, ее поля так же станут недоступными.

Реактивные значения

Начиная с версии 1.4 в качестве значения disabled можно задать объект с интерфейсом binding bridge (bb-value). В этом случае disabled будет автоматически синхрозироваться с таким объектом (его значение приводится к boolean). При этом disabled по прежнему хранит true или false, а при изменениях срабатывают события disable и enable. Связь с bb-value "прячется" в приватном свойстве disabledRA_, в котором хранится специальный адаптер-наблюдатель.

var Node = require('basis.ui').Node;

var someValue = new basis.Token(false);
var view = new Node({
  disabled: someValue
});

console.log(view.disabled);
// > false

someValue.set(true);
console.log(view.disabled);
// > true

Можно использовать эту возможность для более сложных сценариев. Например, блокировать кнопку, если у владельца нет выбранных узлов.

var Node = require('basis.ui').Node;

var list = new Node({
  selection: true,
  childNodes: [
    { name: 'foo' },
    { name: 'bar' }
  ],
  satellite: {
    deleteButton: new Node({
      disabled: Value
        .factory('ownerChanged', 'owner.selection')
        .pipe('itemsChanged', function(selection){
          return !selection.itemCount;
        })
    })
  }
});

console.log(list.selection.itemCount);
// > 0
console.log(list.satellite.deleteButton.disabled);
// > true

list.firstChild.select();
console.log(list.selection.itemCount);
// > 1
console.log(list.satellite.deleteButton.disabled);
// > false

list.selection.clear();
console.log(list.selection.itemCount);
// > 0
console.log(list.satellite.deleteButton.disabled);
// > true

Стоит принимать во внимание следующие особенности использования bb-value в качестве значения disabled:

  • вызов методов disable() и enable() не меняет состояние disabled (в режиме разработки выводится предупреждение), когда для него установлено bb-value;
  • для привязки и сбрасывания bb-value необходимо использовать метод setDisabled(newValue).

Примеры

Недоступность при загрузке данных

var Node = require('basis.ui').Node;
var STATE = require('basis.data').STATE;

var view = new Node({
  handler: {
    stateChanged: function(){
      this.setDisabled(this.state == STATE.PROCESSING);
    }
  }
});

// альтернативное решение
// basis.js 1.4
var Node = require('basis.ui').Node;
var Value = require('basis.data').Value;
var STATE = require('basis.data').STATE;

var view = new Node({
  disabled: Value.factory('stateChanged', function(node){
    return node.state == STATE.PROCESSING;
  }
});

Биндинги

В модуле basis.ui уже определены биндинги disabled и enabled, в их описании нет необходимости. Здесь приведено их описание в качестве примера, как можно использовать доступность в биндингах.

var Node = require('basis.ui').Node;

var view = new Node({
  binding: {
    disabled: {
      events: 'disable enable',
      getter: function(node){
        return node.isDisabled();
      }
    },
    enabled: {
      events: 'disable enable',
      getter: function(node){
        return !node.isDisabled();
      }
    }
  }
});