Knockout.JS — хорошая библиотека для создания сложных веб-приложений. Долгое время мне в ней не хватало асинхронного механизма шаблонов. Реализовать его не получалось, пока я не узнал что window.setTimeout вызывает свой callback не раньше окончания работы текущего контекста. Т.е. в коде
Статья для разбирающихся в механизме биндинга knockout.js и умеющих писать customBindings.
Итак, зная поведение setTimeout(callback, 0) реализация очень простая.
Код custom-binding:
Применение:
Без нулевого timeout возникли бы проблемы: если получение шаблона займет небольшое время, то вставка шаблона в дерево DOM может произойти раньше, чем механизм биндинга knockout доберется до соответствующего элемента. В этом случае биндинг произойдет дважды, что ничем хорошим не заканчивается.
Эту проблему можно было бы решить другим способом: создать observable поле template, в которое сначала ставится затычка вроде «loading...», а уже при получении ответа с сервера загружается сам шаблон. Но это провоцирует мигания в интефейсе: на полсекунды появляется индикация загрузки.
Если кого-то заинтересует, могу добавить кэширование шаблонов и поддержку биндинга asynctemplate одновременно с with и foreach.
UPD: как пояснил в комментариях xdenser все намного проще. В документации Knockout.js изложен более простой путь: нужно добавить init в custom-binding:
+исправлена ошибка при биндинге шаблона: в изначальном варианте биндинг происходил только на первый элемент.
вывод на консоль произойдет только после завершения долгой функции случайного поиска числа очень близкого к единице.setTimeout("console.log(window.Value)",0),(function (){while (Math.random() < 0.9999999);window.Value = 1;})()
Статья для разбирающихся в механизме биндинга knockout.js и умеющих писать customBindings.
Итак, зная поведение setTimeout(callback, 0) реализация очень простая.
Код custom-binding:
ko.bindingHandlers['asynctemplate'] = { update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { $(element).empty(); var template = ko.utils.unwrapObservable(valueAccessor()); if (!template) return; setTimeout(function() { $.ajax({ url: template }).done(function(result){ var view = $(result).appendTo(element)[0]; ko.applyBindings(bindingContext.$data, view); }); }, 0); } }
Применение:
// хардкод путь к темлэйту <div data-bind="asynctemplate: '/Templates/Controls/Components/Modal.html'"></div> // или биндинг на переменную <div data-bind="asynctemplate: templatePath"></div> // нельзя одновременно с with или foreach <div data-bind="asynctemplate: templatePath, with: templateViewModel">!ОШИБКА!</div>
Без нулевого timeout возникли бы проблемы: если получение шаблона займет небольшое время, то вставка шаблона в дерево DOM может произойти раньше, чем механизм биндинга knockout доберется до соответствующего элемента. В этом случае биндинг произойдет дважды, что ничем хорошим не заканчивается.
Эту проблему можно было бы решить другим способом: создать observable поле template, в которое сначала ставится затычка вроде «loading...», а уже при получении ответа с сервера загружается сам шаблон. Но это провоцирует мигания в интефейсе: на полсекунды появляется индикация загрузки.
Если кого-то заинтересует, могу добавить кэширование шаблонов и поддержку биндинга asynctemplate одновременно с with и foreach.
UPD: как пояснил в комментариях xdenser все намного проще. В документации Knockout.js изложен более простой путь: нужно добавить init в custom-binding:
ko.bindingHandlers['asynctemplate'] = { init: function () { return { controlsDescendantBindings: true }; }, update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { $(element).empty(); var template = ko.utils.unwrapObservable(valueAccessor()); if (!template) return; $.ajax({ url: template }).done(function (result) { $(result).appendTo(element); ko.applyBindingsToDescendants(bindingContext, element); }); } }
+исправлена ошибка при биндинге шаблона: в изначальном варианте биндинг происходил только на первый элемент.
