I have created a KnockoutJS application, and I also must use some third-party stuff with it. My third-party stuff uses vanilla Javascript to insert a div into the markup rendered by Knockout. Once this happens, Knockout stops working.
Here's a fiddle that encapsulates the problem: http://jsfiddle.net/p5o8842w/1/
HTML:
<div style="margin-bottom:50px;">
<button onclick='BindVM();' >Bind</button>
<button onclick='ThrowWrench();'>Throw a Wrench Into It</button>
</div>
<div id="ViewModel" data-bind="template: 'Template'">
</div>
<script type="text/html" id='Template'>
<div style="margin-bottom:20px;">
<span data-bind="text: Name"></span>
</div>
<div id="infoDiv">
<input type="text" data-bind="text: Name, value: Name, valueUpdate: 'keyup'" />
</div>
</script>
JavaScript:
function BasicVM () {
var self = this;
self.Name = ko.observable('The Name');
self.Title = ko.observable('The Title');
}
function BindVM() {
var vm = new BasicVM();
var element = document.getElementById('ViewModel');
ko.cleanNode(element);
ko.applyBindings(vm, element);
}
function ThrowWrench() {
var element = document.getElementById('infoDiv');
element.innerHTML = "<div class='container'>" + element.innerHTML + '</div>';
}
First, click 'Bind.' Notice that the textbox is bound to the span; change the box, you change the span.
Then, click 'Throw a Wrench Into It.' Now, the textbox is no longer data-bound to the ViewModel, and typing into it doesn't impact the span.
Things I can't do:
Take the third-party code and refactor/integrate it into my Knockout stuff.
Run the third-party code before I render with Knockout (which I think would help).
Call ko.applyBindings again after running the third-party code. I can do this, but then Knockout destroys what the third-party code did, so I'd have to run it again, which would cause the same problem again.
Is there any way around this?
Because replacing element.innerHTML it's losing is binding. In order to overcome this. Two method are available:
1- Rebind the new element
2- Else
var element = document.getElementById('infoDiv');
var parent = element.parentNode;
var wrapper = document.createElement('div');
parent.replaceChild(wrapper, element);
wrapper.appendChild(element);
This is updated url: http://jsfiddle.net/p5o8842w/5/
Related
I have a view model like this :
function ImageViewModel()
{
var self = this;
self.totalRecordCount = ko.observable();
self.currentRecordCount = ko.observable();
self.images = ko.observableArray();
// fetching all available images
getAvailableImages(self, imageGalleryId, 1);//1 is page number for paging
}
I have html as followes:
<div id="available Images" class="available-images" data-bind="foreach:images">
<div c`enter code here`lass="available-image">
<div class="col-sm-4 thumbnail">
<asp:CheckBox ID="cbxImage" runat="server" CssClass="checkbox" />
<img alt="" data-bind="attr: { 'src': ImagePath, id: 'img_' + ImageId, 'data-id': ImageId }"
style="border: none;" />
</div>
</div>
</div>
i have java script at the page bottom as :
$('.galleryfooter').click(function () {
$(this).attr('data-target', '#imageModal');
$(this).attr('data-toggle', 'modal');
ko.applyBindings(new ImageViewModel(), document.getElementById("imageGallery"));
});
when i first Clicked the images are bind properly but when i clicked the button again then images are get multiplied.Means if i have 5 images in database it displays 25 images.So what should i do?
Stop using jQuery to handle events. Knockout has bindings for that. The agreement you have with Knockout is that it controls the DOM and you only manipulate the viewmodel.
See the click binding and the attr binding. Also, if you have not gone through the Knockout tutorial, I highly recommend it. It will help you let go of the DOM.
I agree with Roy. You need to separate your concerns better. That is the whole point of having a controller. As it stands with every click you are reapplying the binding. The binding should only be applied once. It should be more like this.
function ImageViewModel()
{
var self = this;
self.totalRecordCount = ko.observable();
self.currentRecordCount = ko.observable();
self.images = ko.observableArray();
// fetching all available images
getAvailableImages(self, imageGalleryId, 1);//1 is page number for paging
this.clickThis = function()
{
//doStuff
};
}
ko.applyBindings(new ImageViewModel(), document.getElementById("imageGallery"));
I don't know where your gallery footer is supposed to go. But here is a guess.
<div class="galleryfooter" data-bind="click: function(e){$root.clickThis();}"></div>
Also use knockout for attribute binds instead of jquery.
I have an element that's being created after applyBindings is called.
<span data-bind="html: $root.someObservable() && $root.generateLink()" />
where someObservable is an observable that gets set to true AFTER applybindings has been called, and the function, which is located in the view model:
function generateLink() {
var d = document.createElement("div");
var link = document.createElement("a");
link.href = "someurl.com";
link.target = "_blank";
link.textContent = "link";
d.appendChild(link);
return d.innerHTML;
}
I have confirmed that the function is called after applyBindings is called. I am trying to apply a click binding to this element. None of the techniques I have tried work. I tried calling:
link.setAttribute("data-bind", "click: $root.someFunction.bind($param, 'abc')
followed by a call to:
ko.applyBindings(this, d);
But the click binding never fires. I've also tried:
ko.applyBindingsToNode(link, { click: function() { console.log('aaaaaaaaa'); } }, this);
but again, nothing is triggered. Any ideas?
Thank you!
Could a solution like this work? You could have an observable (observableArray) representing the link(s) you want to show. As you can see below, I delay creating the link model until after binding has already happened (on the button click). I'm not sure if this is feasible but seems like it could be. Cheers!
function generateLink(href) {
var self = this;
self.href = href;
self.target = "_blank";
self.textContext = "link";
return self;
}
function ParentViewModel() {
var self = this;
self.generatedLink = ko.observable(null);
self.onClick = function() {
self.generatedLink(generateLink('http://google.com'));
}
return self;
}
ko.applyBindings(new ParentViewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<button data-bind="click: onClick">Show</button>
<!-- ko with: generatedLink -->
<div>
<a data-bind="text: href, attr: {href: href, target: target, textContext: textContext}"></a>
</div>
<!-- /ko -->
I'd have to look more into how applyBindings processes the nodes to explain exactly why it fails, but it can be done (hackishly).
var vm = {
someObservable: ko.observable()
}
vm.generateLink= function() {
var d = document.createElement("div");
var link = document.createElement("a");
link.href = "someurl.com";
link.target = "_blank";
link.textContent = "link";
d.appendChild(link);
link.setAttribute("data-bind", "click: function(){console.log('clicked')}");
link.setAttribute("id", "myLink");
setTimeout(function(){
ko.applyBindings({}, document.getElementById('myLink'))},
1000)
return d.innerHTML;
}
ko.applyBindings(vm)
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<input type='checkbox' data-bind='checked: someObservable'>Toggle</input>
<!-- ko with: someObservable -->
<span data-bind="html: $root.generateLink()"></span>
<!-- /ko -->
I would highly advise against using said approach. Leveraging the suite of bindings Knockout provides enables you to dispense with the need to make calls against document. If you are manually manipulating the structure of the DOM, it defeats the purpose of using Knockout and makes you work harder to achieve results--case in point the issue at hand. The html binding isn't the approach I would use in this case. There are several ways to achieve the desired result while maintaining more of a M-V-VM separation.
On a side a note, unless things have changed, using self-closing tags can result in unexpected behavior.
I need to subscribe to an existing binding of a DOM element. As an example, I have the following input element:
<div id="MyDiv">
<input id="MyInput" data-bind="enable: isEnabled()" />
</div>
Now, assuming I only have access to the DOM element, I need to do something like this:
var inputElement = $("#MyInput");
var bindings = ko.utils.getBindings(inputElement); // getBindings does not exist
var enableBinding = bindings["enable"];
if (enableBinding != undefined) {
enableBinding.subscribe(function (value) {
if (value == false)
$("#MyDiv").addClass("disabled");
else
$("#MyDiv").removeClass("disabled");
})
}
Is there a way to do this?
Update: I've extended the sample so that you see my use case for this: The div here is automatically generated by a preprocessor and needs the disabled class on it when the input is disabled. It does not work if the attribute is only changed on the input element. The addition/removal must be transparent...
Short answer: Don't do this. There is a reason that getBindings is not a particularly visible function in the Knockout toolkit.
Long answer: You can, through a bit of indirection, get at the original binding.
HTML:
<div id="MyDiv">
<input id="MyInput" data-bind="enable: isEnabled" />
</div>
<input type="checkbox" data-bind="checked: isEnabled" />
JS:
var viewModel = function() {
self.isEnabled = ko.observable(true);
}
ko.applyBindings(new viewModel());
var input = $('#MyInput')[0];
function getBinding(element, name) {
var bindings = ko.bindingProvider.instance.getBindings(input, ko.contextFor(input));
return bindings.hasOwnProperty(name) ? bindings[name] : null;
}
var binding = getBinding(input, 'enable');
binding.subscribe(function(value) {
if (value == false)
$("#MyDiv").addClass("disabled");
else
$("#MyDiv").removeClass("disabled");
});
Working JSFiddle
EDIT: Found a shorter way
Again, if there is any way you can convince your preprocessor to add a CSS observable, do so. Mucking about with bindings in this manner relies on the particular quirks of Knockout 3.3.0's internal implementation, which can change in future releases.
Checkout the answer provided here.
In short, you can use
var viewModel = ko.dataFor(domElement);
to get the viewmodel that is bound to that DOM element. You can then, subscribe to any observables attached to that viewmodel.
Is it possible to access bound element from ko.computed function?
Something like this pseudo code (simplified for clarity):
<h1 id="id1" data-bind="visible: myComputed">
<h1 id="id2" data-bind="visible: myComputed">
<h1 id="id3" data-bind="visible: myComputed">
...
self.myComputed = ko.computed(function(){
return <BOUND_ELEMNT>.id == 'id2';
});
Resulting in only the second element showing.
Note: I'm aware I can have a separate computed for every element, but it is not possible in my case.
EDIT:
Ok - I'll give a more accurate example. The following is similar to what I have:
<section id="id1" data-bind="visible: myComputed1">A lot of code</section>
<section id="id2" data-bind="visible: myComputed2">different lots of code</section>
<section id="id3" data-bind="visible: myComputed3">Other lots of code</section>
...
// This field's value changes over time (between 'id1', 'id2' and 'id3').
// Some complex logic changes this field,
// and as a result only one h1 is showing at a time.
self.myField = ko.observable();
self.myComputed1 = ko.computed(function(){
return self.myField() == 'id1';
});
self.myComputed2 = ko.computed(function(){
return self.myField() == 'id2';
});
self.myComputed3 = ko.computed(function(){
return self.myField() == 'id3';
});
This is an ugly violation of the DRY principle, and I would like to find a way to refactor it. The pseudo code above may solve it, but I'm open for suggestions...
You've created a short, simplified example, which is normally great. However, it feels like you've introduced an XY-problem instead. As such, this answer may or may not be helpful.
You're trying to introduce a dependency on the View in your ViewModel. It should be the other way around! Something like this would make more sense:
<h1 data-bind="visible: myComputed, attr { id: myId }"></h1>
Note the use of the attr binding to set the id. Your ViewModel should be constructed accordingly:
var activeHeaderId = 'id2';
var viewModel = function(id) {
var self = this;
self.myId = ko.observable(id);
self.myComputed = ko.computed(function() {
return self.myId() === activeHeaderId;
});
}
Note: I'm leaving my other answer as an answer to the first bit of the question, maybe it'll help other users stumbling upon this question.
The question in the update is:
This is an ugly violation of the DRY principle, and I would like to find a way to refactor it.
OP indicates in comments that answers focussing on the given example are preferred. The sample code can be easily refactored such that it doesn't violate DRY anymore. (PS. I still think the "why" behind wanting this structure is very important, but that doesn't prevent me from answering the question in the context of the given sample.)
Method 1 - One h1 in the View - One item in the ViewModel
Use the following View:
<h1 data-bind="attr: {id: myField}">
With this ViewModel:
self.myField = ko.observable();
Very DRY, functionally almost equivalent (except that the other 2 h1 elements aren't just invisible, they're not even in the DOM).
Method 2 - Multiple h1s in the View - Multiple items in the ViewModel
Refactor the View to a list structure (see note 4 in the foreach binding):
<!-- ko foreach: items -->
<h1 data-bind="attr: {id: myId},
text: itemName,
visible: $root.currentItem().myId() === myId()">
</h1>
<!-- /ko -->
With the following ViewModel:
var item = function(nr) {
this.itemName = ko.observable("Item number " + nr);
this.myId = ko.observable("id" + nr);
}
var viewModel = function() {
this.items = ko.observableArray([new item(1), new item(2), new item(3)]);
this.currentItem = ko.observable();
}
See this fiddle for a demo.
Method 3 - One h1 in the View - Multiple items in the ViewModel
With this method you use the list like setup from method 2, but only render the currentitem. The View utilizes the with binding:
<!-- ko with: currentItem -->
<h1 data-bind="attr: {id: myId}, text: itemName"></h1>
<!-- /ko -->
The ViewModel is the same as in method 2. See this fiddle for a demo.
Make a custom binding handler that uses your observable to trigger the visibility. Something like this:
ko.bindingHandlers.idVisible = {
update: function(element, valueAccessor) {
var idUnwrapped = ko.utils.unwrapObservable(valueAccessor());
if(idUnwrapped == $(element).attr('id'))
{
$(element).show();
}
else
{
$(element).hide();
}
}
};
Change your HTML:
<h1 id="id1" data-bind="idVisible: headerId">Header 1</h1>
<h1 id="id2" data-bind="idVisible: headerId">Header 2</h1>
<h1 id="id3" data-bind="idVisible: headerId">Header 3</h1>
And a sample view model:
function ViewModel() {
var self = this;
self.headerId = ko.observable('id1');
}
var vm = new ViewModel();
ko.applyBindings(vm);
Here's a jsFiddle with a demo that changes the headerId after two seconds: http://jsfiddle.net/psteele/cq9GU/
I have to pass innerHTML to a div using my JavaScript function.The innerHTML that I am passing also has a div for which I have to attach a click event so that on click of it, the element responds to the click event.
I have a fiddle here to explain the problem:
http://jsfiddle.net/K2aQT/2/
HTML
<body>
<div id="containerFrame">
</div>
</body>
JavaScript
window.onload = function(){usersFunction();};
function usersFunction(){
var someHtml = '<div> <div class ="btnSettings"> </div> <span></span> </div>';
changeSource(someHtml);
}
function changeSource(newSource){
document.getElementById("containerFrame").innerHTML = newSource;
}
While passing the source, how do I tell JavaScript function that this HTML being passed also has some element which has to be bound to a click event?
If you have to do it this way, you could consider adding the click handler inside the HTML string itself:
var someHtml = '<div> <div class ="btnSettings" onclick="return myFunction()"> </div> <span></span> </div>';
Alternatively, after modifying the .innerHTML, find the right <div>:
var frame = document.getElementById('containerFrame');
frame.innerHTML = newSource;
var settings = frame.getElementsByClassName('btnSettings')[0];
// depends on the browser, some IE versions use attachEvent()
settings.addEventListener('click', function(event) {
}, false);
i think you need something like that:
document.getElementById("containerFrame").onclick = function() {
// put your function here
};