Applying click binding to element created after applybindings - javascript

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.

Related

JavaScript element creation differences resulting in jquery.ui.autocomplete element undefined error

I'm trying to dynamically create an input element and attach a jquery.ui autocomplete control to it before adding it to the DOM. The way I normally create dynamic elements seems to be creating an Cannot read property 'element' of undefined error, and I cannot figure out why.
$(document).ready(() => {
const content = document.getElementById('content');
const input = document.createElement('input');
const inputFromTemplate = createFromTemplate(`<input></input>`);
console.log(input);
console.log(inputFromTemplate);
$(input).autocomplete({
source: ['one', 'two', 'three']
});
$(inputFromTemplate).autocomplete({
source: ['one', 'two', 'three']
});
content.append(input);
content.append(inputFromTemplate);
});
function createFromTemplate(template) {
const htmlTemplate = document.createElement('template');
htmlTemplate.innerHTML = template;
return htmlTemplate.content.firstElementChild.cloneNode(true);
};
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
<div id="content"></div>
In the above code sample, when I create an input element using the document.createElement('input') method, the autocomplete control attaches without a problem.
When I create an input element using my own createFromTemplate method, it throws the above error.
Both elements appear the same when logged out to the console, but it appears that the autocomplete control is attaching before the console.log fires (you can see this by running as is, then commenting out the .autocomplete lines of code and seeing the difference in the input code logged out to the console).
I would assume that my understanding of how jquery attaches controls to elements is flawed, as I can't see why there would be a difference between the two.
(Note: Yes, this is a simple reproduction, the reason I want to use a function like createFromTemplate is because the actual input is part of a larger html template, so chaining together document.createElement methods isn't going to happen).
EDIT:
Just to address the issue of it working when the element is attached to the DOM first, unfortunately that isn't an option in the case of the project I'm working on. Something akin to this issue was reported and fixed in a previous version of jquery.ui back in 2012, so it might well be a bug with the library
I suggest a quick read: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template
A DocumentFragment is not a valid target for various events, as such it is often preferable to clone or refer to the elements within it.
HTML
<div id="container"></div>
<template id="template">
<div>Click me</div>
</template>
JavaScript
const container = document.getElementById("container");
const template = document.getElementById("template");
function clickHandler(event) {
alert("Clicked a div");
}
const firstClone = template.content.cloneNode(true);
firstClone.addEventListener("click", clickHandler);
container.appendChild(firstClone);
const secondClone = template.content.firstElementChild.cloneNode(true);
secondClone.addEventListener("click", clickHandler);
container.appendChild(secondClone);
Result
firstClone is a DocumentFragment instance, so while it gets appended inside the container as expected, clicking on it does not trigger the click event. secondClone is an HTMLDivElement instance, clicking on it works as one would expect.
When trying to initialize Autocomplete, event binding will fail. This may be why you are seeing various different errors and why other elements are working and elements in template are not.
So let's test as they advise, the way you tried, and with some other possible solutions.
$(document).ready(() => {
const content = document.getElementById('content');
var inA = document.createElement('input');
var inB = createTempObj("<input>");
var inC = createFromTemplate("<input>");
var inD = document.getElementById("template").content.firstElementChild.cloneNode(true);
var m1 = menuFromTemp();
console.log("Init Autocomplete 1");
console.log("HTML", inA);
initAutoComplete(inA);
console.log("Init Autocomplete 2");
console.log("HTML", inB);
initAutoComplete(inB);
console.log("Init Autocomplete 3");
console.log("HTML", typeof inC, inC);
initAutoComplete(inC);
console.log("Init Autocomplete 4");
console.log("HTML", inD);
//initAutoComplete(inD);
console.log("Init Menu 1");
console.log("HTML", m1);
var menu = $(m1).menu({
role: null
}).hide().menu("instance");
content.append(inA);
content.append(inB);
content.append(inC);
content.append(inD);
content.append(m1);
});
function createFromTemplate(item) {
var base = document.createElement('template');
var real = document.createElement('input');
console.log("item:", typeof item, item);
console.log("real:", typeof real, real);
base.innerHTML = item;
var clone = base.content.children[0].cloneNode(true);
console.log("clone:", typeof clone, clone);
return clone;
};
function createTempObj(el) {
var base = $("<template>");
$(el).appendTo(base);
return base.children(0).clone("true")[0];
}
function initAutoComplete(el) {
$(el).autocomplete({
source: ['one', 'two', 'three']
});
}
function menuFromTemp() {
var base = document.createElement('template');
base.innerHTML = "<ul><li><div>Books</div></li><li><div>Clothing</div></li> <li><div>Electronics</div></li></ul>";
var menu = base.content.children[0].cloneNode("true");
return menu;
}
<link rel="stylesheet" href="//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
<div id="content"></div>
<hr />
<template id="template">
<input>
</template>
Input C and D fail to initialize. I commented them out but you can test this on your own. Based on documentation, they should work as expected. If you want to dig into it further, I bet there is an answer out there. If you want to get work done, move away from using template for this portion.
It's not clear why you are trying to create a template on the fly, why not just create the input element on the fly as needed in JavaScript.
If you create a div element instead of a template element it works.
$(document).ready(() => {
const content = document.getElementById('content');
const input = document.createElement('input');
const inputFromTemplate = createFromTemplate(`<input></input>`);
console.log(input);
console.log(inputFromTemplate);
$(input).autocomplete({
source: ['one', 'two', 'three']
});
$(inputFromTemplate).autocomplete({
source: ['one', 'two', 'three']
});
content.append(input);
content.append(inputFromTemplate);
});
function createFromTemplate(template) {
const htmlTemplate = document.createElement('div');
htmlTemplate.innerHTML = template;
return htmlTemplate.firstElementChild.cloneNode(true);
};
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
<div id="content"></div>

How to stop the multiple binding in KnockOut.js

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.

Subscribe to bindings of an existing DOM element in KnockoutJS

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.

Dynamically Inserting a Div Breaks KnockoutJS DataBinding

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/

Access Html element from ko.computed

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/

Categories