Knockout: динамически добавлять привязки к пользовательским элементам

Вкратце: я ищу компонентный эквивалент привязки предварительной обработки.

Я пытаюсь инкапсулировать сложные привязки, такие как

<button data-bind="openModalOnClick: {template: '...', action: '...'}, css: {...}">
  delete all the things
</button>

в пользовательском элементе, таком как

<confirmation-button>
  delete all the things
</confirmation-button>

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

Я знаю, что мой компонент может вставить кнопку в качестве шаблона, но результирующая разметка

<confirmation-button>
  <button data-bind="openModalOnClick: {template: '...', action: '...'}, css: {...}">
    delete all the things
  </button>
</confirmation-button>

было бы лишним.

В идеале я мог бы использовать регистрацию компонента для динамического добавления необходимых привязок к пользовательскому элементу. Однако (ab) использование createViewModel для этого, похоже, не работает:

  ko.components.register('confirmation-button', {
      viewModel: {
        createViewModel: function createViewModel(params, componentInfo) {
          var Vm;
          
          $(componentInfo.element).attr('data-bind', 'click: function() { confirm("Are you sure"); }');
          
          Vm = function Vm(params) { };
          
          return new Vm(params);
        }
      },
      template: '<!-- ko template: { nodes: $componentTemplateNodes } --><!-- /ko -->'
  });
confirmation-button {
  border: 1px solid black;
  padding: 1rem;
  cursor: pointer;
}
<script src="http://knockoutjs.com/downloads/knockout-3.3.0.js"></script>

<confirmation-button>do stuff</confirmation-button>

Можно ли каким-то образом добавить динамические привязки к самим пользовательским элементам?


person janfoeh    schedule 19.02.2015    source источник
comment
Вы пытались добавить атрибут привязки данных с помощью простого js? А затем, после того, как дом будет загружен с ними, добавьте вашу модель представления.   -  person marko    schedule 19.02.2015
comment
@marko, в какой момент ты бы это сделал?   -  person janfoeh    schedule 19.02.2015
comment
Почему вы хотите динамически вставлять ко-события после некоторого определенного условия, я полагаю?   -  person marko    schedule 19.02.2015
comment
@marko извините, какие события?   -  person janfoeh    schedule 19.02.2015


Ответы (2)


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

Как упоминает RPN в этом комментарии пока нет хуков для событий жизненного цикла на пользовательских элементах (в версии 3.2). По сути, причина, по которой ваше (ab) использование createViewModel не работает, заключается в том, что этот код вызывается до рендеринга любого элемента.

Так что его предложение в этом комментарии относится и к вам. На данный момент самый элегантный способ — создать пользовательскую привязку к элементу верхнего уровня. Если вы хотите сделать его универсальным, вы можете сделать что-то вроде этого:

<custom-element data-bind="render"></custom-element>

А затем в вызове init пользовательской привязки данных render вы можете получить имя пользовательского элемента и найти любую зарегистрированную постобработку для применения. Вот (грубый) пример скрипта: http://jsfiddle.net/8r891g6b/ и вот javascript только что кейс:

ko.components.register('confirm-button', {
    viewModel: function (params) {
        params = params || {};
        this.text = params.text || '(no text passed in)';
    },
    template: '<button data-bind="text: text"></button>'
});

ko.bindingHandlers.render = {
    init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
        ko.bindingHandlers.render[element.tagName.toLowerCase()](element);
    }
};

ko.bindingHandlers.render['confirm-button'] = function (element) {
    ko.utils.registerEventHandler(element, 'click', function (event) {
        if (!confirm('Are you sure?')) {
            event.preventDefault();
        }
    });
};

ko.applyBindings();

Между прочим, этот пример немного шаткий, потому что событие щелчка по кнопке сначала нажмет на кнопку и произойдет независимо от обработчика подтверждения. Я просто придерживался вашего примера, но надеюсь, что основная идея достаточно проста для понимания.

person Milimetric    schedule 21.02.2015
comment
Общая делегирующая привязка render — хорошая идея! Если и когда KO получит события жизненного цикла компонентов, переход должен быть достаточно простым. Спасибо! - person janfoeh; 21.02.2015
comment
Не нужно мне напоминать — я ни разу не оставил вопрос с полезным ответом помеченным как неотвеченный, и не планирую делать этого в будущем. Щедрость еще открыта на пару дней; если ваш обходной путь остается лучшим возможным ответом, я приму его и награжу вас наградой. - person janfoeh; 22.02.2015

Я экспериментировал с различными методами для достижения желаемых результатов и оценивал их плюсы и минусы. Не претендуя на «ответ», это может быть полезно для дальнейшего использования. Я тестировал:

  1. ko.bindingHandlers.component.preprocess: нет доступа к: пользовательскому элементу, шаблону, компоненту и родительской модели представления. доступ к: привязкам.
  2. Пользовательский загрузчик компонентов loadTemplate: нет доступа к: пользовательскому элементу. доступ к: шаблону, родительской модели и модели представления компонентов (через шаблон)
  3. ko.bindingProvider.instance.preprocessNode: нет доступа к: модели представления компонента, шаблону доступа к: пользовательскому элементу, привязкам, родительской модели представления.

#3 выглядит наиболее подходящим из трех. Учитывая следующий код:

ko.bindingProvider.instance.preprocessNode = function(node) {
   // access to current viewmodel
   var data = ko.dataFor(node), 
       // access to all parent viewmodels
       context = ko.contextFor(node),
       // useful to get current binding values
       component = ko.bindingProvider.instance.getBindings(node, context);
   if (node.nodeName === 'CUSTOM-BUTTON') { // only do if 'my-custom-element'
   // kind of 'raw' string extraction but does the job for demo
       var getMsg = node.getAttribute('params').split('msg:')[1], 
       msg = getMsg.slice(0,getMsg.indexOf(','));
       $(node).attr('data-bind','click: function() { confirm('+ msg +'())}');
   } else {
       return null;
   }
}

И следующая скрипта, чтобы проверить это: /a> (в верхней части JS установите параметр 1 для теста № 2 и 2 (по умолчанию) для теста № 3).


(первый ответ) Примечание: хотя эта часть как бы достигает того, что запросил OP, делая элемент контейнера полезным, она прикрепляет события после вместо до загрузки шаблона; сохранить для справки.

Да, это возможно, хотя я пытался спорить с точки нокаута мнение, что это может быть нежелательно. Учитывая, что привязки событий на самом деле являются только операторами, сообщающими Knockout, регистрируют эту функцию для этого события, вы можете установить привязку click непосредственно через JS, например так:

function customButton(params, parent) {
    var self = this;
    this.msg = params.msg;
    this.label = params.label;
    // this is the same as the click binding
    parent.addEventListener('click', function(e) { 
        alert(self.msg()); alert(e.target.nodeName); 
    }, false);
}
var myComponent = {
    viewModel: { createViewModel: function(params, componentInfo) {
        var parent = componentInfo.element;
        return new customButton(params, parent);
    }},
    template: { element: 'custom-button-tmpl' }
}

Для привязок attr и css это немного сложнее, но, учитывая, что наблюдаемые объекты computed — это просто функции, запускаемые каждый раз, когда их наблюдаемые объекты обновляются, вы можете, например, изменить фон кнопки в нашей виртуальной машине выше следующим образом:

//prop used as function, so name doesn't matter
this.background = ko.computed(function() { 
    parent.style.backgroundColor = params.bg();
});

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

person Tyblitz    schedule 19.02.2015
comment
Спасибо за ваш ответ, но я не собираюсь подражать привязкам по частям. Я ищу компонентный эквивалент привязки предварительной обработки - person janfoeh; 19.02.2015
comment
Кроме того, я не пытаюсь заменить пользовательский элемент — как раз наоборот, я пытаюсь сделать его существование полезным, а не просто контейнером, придав ему поведение. - person janfoeh; 19.02.2015
comment
@janfoeh Да, ответ, на который я ссылался, предназначался только для демонстрации предпочтительного / стандартного использования Knockout. Итак, если я вас правильно понял, вы хотите что-то вроде ko.bindingHandlers.component.preprocess, но чтобы все свойства компонента были доступны внутри функции? - person Tyblitz; 20.02.2015
comment
Да; точно так же, как обратный вызов preprocess для обработчика привязки позволяет мне вводить дополнительные привязки, мне нужна такая же возможность для обработчика компонента. - person janfoeh; 20.02.2015
comment
Привет @janfoeh; найдя этот интересный вопрос, я протестировал некоторые другие методы и обновил свой ответ. Посмотрите и посмотрите, может ли он быть как-то полезен :) - person Tyblitz; 23.02.2015
comment
Спасибо, что потратили на это совсем немного времени! preprocessNode кажется действительно жизнеспособной альтернативой. Хотя ваше решение имеет то преимущество, что вам не нужно объявлять привязку делегатора, как я делаю с ответом Milimetrics, его / ее ответ является для меня более практичным обходным путем. Но пока я собираюсь принять ответ Millimetrics в его нынешнем виде, я добавлю за вас вторую награду; это просто займет на день больше, потому что награды должны быть открыты как минимум столько же. Спасибо! - person janfoeh; 23.02.2015
comment
@janfoeh Я полностью согласен с тем, что ответ Millimetrics более практичен, особенно если у вас есть много пользовательских элементов для проверки :) - person Tyblitz; 23.02.2015
comment
По какой-то причине я не могу добавить к этому ответу еще одну награду, не удвоив ее, что было бы несправедливо по отношению к Milimetric. Вместо этого я добавил вознаграждение в этот вопрос и буду наградите его завтра, как только SO позволит мне. - person janfoeh; 24.02.2015
comment
Нет, не волнуйтесь, м8, я нашел этот вопрос интересным еще до того, как за ответ на него была назначена награда, и я, честно говоря, не возражаю. Изучение работы KnockoutJS было приятным и познавательным занятием :) - person Tyblitz; 24.02.2015