How does my Knockout / RequireJS widget communicate with the correct context when used on a site that also uses Knockout?

Scenario

I am creating a widget using RequireJS and KnockoutJS. The widget calls ko.applyBindings(widgetViewModel, thisWidget) when it is created. The widget must be available on the site, regardless of whether the site uses Knockout itself.

Problem

When I remove my widget to a site that uses Knockout, a view model is used instead of the widget if the site calls ko.applyBindings(siteViewModel) after installing the widget. The widget receives the siteViewModel instead of the desired widgetViewModel .

What I tried so far

  • The ko.applyBindings(siteViewModel) call will occur before the widget is installed. This works, but not perfect, because it sets limits on the way my widget is.

  • Add a custom binding for my widget that applies the correct binding context (i.e. widgetViewModel to my widget and returns {controlsDescendantBindings: true}; ). For example, the knockout instance used by the site is not the same as my widget (due to Require), so I will need to access the site instance, possibly by viewing the global namespace.

+4
source share
3 answers

I think you were close to your second decision. What you can do is create a custom binding that simply stops the site bindings, like the example presented on this page http://knockoutjs.com/documentation/custom-bindings-controlling-descendant-bindings.html

But, as you say, if they use knockout on the site already, you need this binding to be on this version of the knockout, so you should wrap it in if (ko), like this ...

 if(ko){ ko.bindingHandlers.allowBindings = { init: function(elem, valueAccessor) { // Let bindings proceed as normal *only if* my value is false var shouldAllowBindings = ko.unwrap(valueAccessor()); return { controlsDescendantBindings: !shouldAllowBindings }; } }; } 

Then, since you are using requirejs, you can map your knockout version to something other than ko, like ko2 or something else.

 define(["knockout", "jquery"], function (ko2, $) { var myViewModel = { SomeComponentLevelBinding: ko2.observable() }; ko2.applyBindings(myViewModel, document.getElementById('someElementId')); } 

Then your html might look something like this ...

 <body> <div> <span data-bind="text: SomeSiteLevelBinding"></span> </div> <div data-bind="allowBindings: false"> <div id="someElementId"> <span data-bind="text:SomeComponentLevelBinding"></span> </div> </div> </body> 

Since the site is already using knockout, and we have added allowBindings to this instance, we stop the knockout version of the site to control anything inside this div. Then, because we only apply the bindings to the div inside it from our component, using the component version of the knockout, we must have 2 versions of the knockout on the same page as the slide together.

This should also be good if they do not have a knockout on the page, because if they are not used, we do not add allowBindings, and since we apply bindings to the div inside it, the allowBindings:false data binding attribute is allowBindings:false in this script.

+1
source

I recommend writing your widget so that it exports the applicator method or can be included as a ViewModel property.

demo

 exports.widget = function(title){ var self = this; self.title = ko.observable(title); self.apply = function(element){ ko.applyBindings(self, element); }; return self; }; 

The HTML for your widget is as follows:

 <h1 data-bind="text: title">Default</h1> 

Now we need to give both users the opportunity to include it in their document, which does not interfere with their workflow.

Users who are not members of KO can simply bind it to the container of the h1 element (let it pretend to be your module called MyWidgets).

 MyWidgets.widget("Not In KO").apply(document.getElementById('parentOfWidget')); 

Knockout users can include it in their ViewModel and use the with binding to give it context.

 var ViewModel = function(){ var self = this; self.widgetInstance = new MyWidgets.widget("In KO"); self.thing = "Other Thing"; } ko.applyBindings(new ViewModel); 

Their HTML will look like this:

 <div id="ko" data-bind="with: widgetInstance"> <h1 data-bind="text: title">Default</h1> </div> <h2 data-bind="text: thing"></h2> 

Please note that they can create other properties that do not conflict with any properties of your widget.

(someone, think of a better name than apply and edit my post)

0
source

How about writing a custom binding for your widgets, for example:

 define(["knockout", "jquery"], function (ko, $) { var widgetViewModel = { title: "My Widget Titel" }; var widgetWrapperHtml = "<div data-bind=\"mywidget: true\"></div>"; var widgetHtml = $("<div data-bind=\"text: $data.title\"</div>"); ko.virtualElements.allowedBindings.mywidget = true; ko.bindingHandlers.mywidget = { init: function(element, valueAccessor, allBindings, viewModel, bindingContext) { // Create the widget context: var widgetBindingContext = widgetViewModel; // Or uncomment this line to created a child context: // var widgetBindingContext = bindingContext.createChildContext(widgetBindingContext); ko.virtualElements.emptyNode(element); ko.virtualElements.prepend(element, widgetHtml[0]); ko.applyBindingsToDescendants(innerContext, element); } }; var applyWidgetToElement = function (element) { $(element).html(widgetWrapperHtml); ko.applyBindings({}, element); }; return applyWidgetToElement; }) 

Then you can use your widget as follows:

 require(["mywidget", "jquery"], function (widget, $) { widget($('.widgetStyle')[0]); }); 

but also like this:

 require(["mywidget", "jquery", "knockout"], function (widget, $, ko) { var viewModel = { title: "My App" }; widget($('.widgetStyle')[0]); ko.applyBindings(viewModel); }); 
0
source

Source: https://habr.com/ru/post/1484889/


All Articles