I am trying to wireup knockout.js inside some existing application.
Binding works in some mysterious ways. If I use p tag it completelly messes up the binding.
If I use the label tag it also messes up the binding.
For example:
<p data-bind="foreach: currentFields">
<p data-bind="foreach: props">
<span data-bind="text: type"></span>
</p>
</p>
This is not gonna work. If I change to span it works.
But then I have the problem with a label tag. If I delete label the binding works.
It seems autobinding is completely fucked up if I use anything except plain span.
Is there a proper javascript binding library? Is Angular.js any better in this regard?
Because rewriting all the existing HTML including CSS just to please knockout.js is really no fun at all :)
Sample code:
<p>
<span data-bind="foreach: currentFields">
<span data-bind="text: value"></span>
<span data-bind="text: type"></span>
<span data-bind="text: selected"></span>
<span data-bind="text: props"></span>
<span data-bind="foreach: props, visible: selected">
<li>
<label>Labela:</label>
<span>
<span data-bind="text: type"></span>
<input type="text" data-bind="value: type, valueUpdate: 'afterkeydown'" required="required" maxlength="140" ></input>
</span>
</li>
</span>
</span>
</p>
var Property = function(type) {
this.type = ko.observable(type);
}
var Field = function(value) {
this.props = ko.observableArray([new Property("a"), new Property("b")]);
this.type = ko.observable('text');
this.value = ko.observable(value);
this.selected = ko.observable(false);
};
var myViewModel = function () {
var self = this;
this.currentFields = ko.observableArray([new Field("a"), new Field("b")]);
this.currentField = ko.observable();
this.addField = function() {
self.currentFields.push(new Field("xyz"));
};
this.selectField = function(field) {
ko.utils.arrayForEach(self.currentFields(), function(item) {
item.selected(false);
});
field.selected(true);
self.currentField(field);
};
};
$(document).ready(function() {
ko.applyBindings(new myViewModel());
});
Could you havea look at this fiddle and tell what is not working as expected ?
<span data-bind="foreach: currentFields">
<span data-bind="text: value"></span>
<span data-bind="text: type"></span>
<span data-bind="text: selected"></span>
<span data-bind="text: ko.toJSON(props)"></span>
<span data-bind="foreach: props, visible: selected">
<li>
<label>Labela:</label>
<span>
<span data-bind="text: type"></span>
<input type="text" data-bind="value: type, valueUpdate: 'afterkeydown'" required="required" maxlength="140"></input>
</span>
</li>
</span>
</span>
I changed this binding :
<span data-bind="text: props"></span>
into :
<span data-bind="text: ko.toJSON(props)"></span>
If you have invalid HTML, the browser reformats it before Knockout has a chance to parse it. What were nested elements in your original HTML become de-nested in the browser.
The <p> element has specific limitations--it cannot contain other paragraphs or lists or basically any type of block element.
See http://www.w3.org/TR/html5/grouping-content.html#the-p-element
Related
I have a form, I would like to dynamically build, such that each product that has a help text gets a tooltip.
This is a Product. It extends BaseProduct:
class BaseProduct {
constructor(name, helpText = null) {
this.name = name;
this.helpText = helpText;
}
label() {
if (this.helpText) {
return `
<label class="tooltip">
${this.name}
<div class="right">${this.helpText}</div>
</label>`;
}
return `<label for="not-important">${this.name}</label>`;
}
}
class Product extends BaseProduct {
constructor(name, price, helpText = null) {
super(name, helpText);
this.price = price;
}
toHtml() {
let inner = `
<input type="checkbox" name="not-important"
id="not-important" value="not-important" />
${super.label()}
<span class="price">${this.price}</span>`;
return `
<p class="item-row">
${inner}
</p>`;
}
}
You can check it out here: https://jsfiddle.net/whoru74c/10/
There is one example that is not dynamic, showing what it is supposed to look like. The other Product gets dynamically added.
The strange part about the Product is that I can print the result of calling Product.toHtml() or super.label() and it looks like expected:
<label class="tooltip">
Product Name
<div class="right">[Product help text]</div>
</label>
But when I insert it into the DOM tree, using insert(), it gets split up, so that the <div class="right"> comes after </label>:
<p class="item-row">
<input type="checkbox" name="not-important" id="not-important" value="not-important">
<label class="tooltip">
Product Name
</label>
<!-- 1 -->
</p>
<!-- The following should be in 1 -->
<div class="right">[Product help text]</div>
<span class="price">123</span>
<p></p>
Furthermore, a random empty <p>-tag gets added, and I cannot figure out why.
Here is a quick recap of what Lain and traktor pointed out, for anyone that might experience a similar problem.
According to the HTML spec, a <div> is not allowed inside of a <label>. Therefore, it gets pushed out of the element.
Changing the <div> inside <label> to a <span> seems to work fine. It might affect the layout though, so now you know where to look if that happens.
Rendered HTML before changing <div> to <span>:
<p class="item-row">
<input type="checkbox" name="not-important" id="not-important" value="not-important">
<label class="tooltip">
Product Name
</label>
</p>
<div class="right">[Product help text]</div>
<span class="price">123</span>
<p></p>
After:
<p class="item-row">
<input type="checkbox" name="not-important" id="not-important" value="not-important">
<label class="tooltip">
Product Name
<span class="right">[Product help text]</span>
</label>
<span class="price">123</span>
</p>
At times I have elements from a view model that I am accessing multiple times in the view and I would like to be able to assign an alias from the view context to shorten things up.
I'm looking for something that would be similar to the foreach as alias or with, but that I am able to use on a single arbitrary access.
As an example, in the following I might want to alias $root.form().budget.budgetEndDate to endDate
<input id="foo" type="text" data-bind="dateTimePicker: $root.form().budget.budgetEndDate" />
Then I could rewrite the binding as
<input id="foo" type="text" data-bind="dateTimePicker: endDate" />
Two options.
First, for demo purposes only but not quite recommended, you ould pollute the global namespace:
function Root() {
this.form = ko.observable(new Form());
}
function Form() {
this.budget = {
budgetEndDate: ko.observable("dummy budget end date")
};
window["endDate"] = this.budget.budgetEndDate;
}
ko.applyBindings(new Root());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.0/knockout-min.js"></script>
<div>
endDate outside form: <b data-bind="text: endDate"></b>
</div>
<div data-bind="with: form">
<div>inside form: <b data-bind="text: endDate"></b></div>
<div data-bind="with: budget">
<div>inside form.budget: <b data-bind="text: endDate"></b></div>
</div>
</div>
Second, you could place it on $root:
function Root() {
this.form = ko.observable(new Form());
this.endDate = this.form().budget.budgetEndDate;
}
function Form() {
this.budget = {
budgetEndDate: ko.observable("dummy budget end date")
};
}
ko.applyBindings(new Root());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.0/knockout-min.js"></script>
<div>
endDate outside form: <b data-bind="text: $root.endDate"></b>
</div>
<div data-bind="with: form">
<div>inside form: <b data-bind="text: $root.endDate"></b></div>
<div data-bind="with: budget">
<div>inside form.budget: <b data-bind="text: $root.endDate"></b></div>
</div>
</div>
However, on a subjective note, I'd consider this a design smell and/or an XY-problem. The place you bind to budgetEndDate should probably be the right context / view to access it directly. You should consider moving that observable to another part of your view model.
I have a knockout foreach section and I am trying to get the values of the generated controls inside of a function bound to a click event for each section. For each section that is generated, when I click on the button I want to know what it's comment is.
//HTML
<div data-bind="foreach: areas">
<button data-bind="click: $parent.saveComment()" />
<input type="text" data-bind="text: comment" />
</div>
//KnockoutJS function
saveComment: function(){
console.log([value of the comment textbox]);
}
Typically for an <input> you'll want to use the value binding. You can pass $data but it is not a requirement as that value will be passed automatically for you.
<div data-bind="foreach: areas">
<button data-bind="click: $parent.saveComment">Save</button>
<input type="text" data-bind="value: comment"></input>
<br/>
</div>
Do not include any () on the saveComment binding and declare the function with a parameter in your model. The function will receive the value that represents the equivalent of $data.
saveComment: function (data) {
console.log(data.comment());
}
Or if you prefer, you can simply use this inside of saveComment to refer to the "current" context.
saveComment: function () {
console.log(this.comment());
}
Check out my example fiddle that contains both usages.
HTML:
<div data-bind="foreach: areas">
<button data-bind="click: $parent.saveComment($data)" />
<input type="text" data-bind="text: comment" />
</div>
Javascript:
saveComment: function(area){
console.log(rea);
}
See:
What is the origin and purpose of the variable $data in KnockoutJS?
So I have this SPA developed using this sample.
The sample shows the list of Todo in a table something like this
<section id="lists" data-bind="foreach: todoLists, visible: todoLists().length > 0">
<table width="100%" style="margin-top: 20px;" class="table-main">
<thead>
<tr class="b-table-line">
<th>Select</th>
<th>Title</th>
<th>Artist</th>
</tr>
</thead>
<tbody data-bind="foreach: todos">
<tr>
<td>
<input type="checkbox" data-bind="checked: isDone" /></td>
<td>
<input class="todoItemInput" type="text"
data-bind="value: title,
disable: isDone,
blurOnEnter: true,
updateOnTitle:true,
click: $root.clearErrorMessage" />
</td>
<td>
<input class="todoItemInput" type="text"
data-bind="value: artist,
click: $root.clearErrorMessage" />
</td>
</tr>
</tbody>
</table>
Now what I am trying to do here is as soon as I change the Title text, I try to change Artist text as well, for that I have created a custom binding updateOnTitle and associated it with the textbox as shown in the table. Its definition looks something like this:
ko.bindingHandlers.updateOnTitle = {
init:function(element,valueAccessor,allBindings,viewModel,bindingContext)
{
$(element).blur(function (evt) {
//Here I am trying to update the artist property based on title
bindingContext.$data.title("Title goes here");
bindingContext.$data.artist("New Artist Name Here");
}
}
The changes are not reflected in the table above. Both these properties are observable.
I would like to know what exactly am I missing here?
I may as well turn my comment into an answer. I think binding handlers are better suited as a general approach to solve problems, and it pays off to avoid having a binding rely on a specific ViewModel (your binding refers to "artist" and "title"). A writeable computed observable may be more suited for the task.
Suppose the following view:
<h3>Inputs</h3>
Title: <input type="text" data-bind="value: title, valueUpdate: 'afterkeydown'" /><br />
Artist: <input type="text" data-bind="value: artist, valueUpdate: 'afterkeydown'" />
<hr />
<h3>Read only version</h3>
Title: <span data-bind="text: title"></span><br />
Artist: <span data-bind="text: artist"></span>
Notice the first input is bound to titleEditing, the computed observable. The ViewModel could be defined with these properties:
function ViewModel() {
var self = this;
var _title = ko.observable('my title');
self.title = ko.computed({
read: _title,
write: function(newval) {
_title(newval);
self.artist('New Artist Name Here');
}
});
self.artist = ko.observable('john doe');
};
Now, if you update the first input, title will change and artist will reset.
See this fiddle for a demo.
I know how to bind to a property, but how do i bind to a property like:
Parent.Child
Using the hello world example on Knockout JS.com:
Html:
<p>First name: <input data-bind="value: firstName" /></p>
<p>Last name: <input data-bind="value: lastName" /></p>
<h2>Hello, <span data-bind="text: fullName"> </span>!</h2>
<h2>ChildProperty: <span data-bind="text: parentProperty.childProperty"> </span>!</h2>
Javascript:
var ViewModel = function(first, last) {
this.firstName = ko.observable(first);
this.lastName = ko.observable(last);
this.parentProperty = ko.observable(
{
childProperty: "I am a child Property"
});
this.fullName = ko.computed(function() {
// Knockout tracks dependencies automatically. It knows that fullName depends on firstName and lastName, because these get called when evaluating fullName.
return this.firstName() + " " + this.lastName();
}, this);
};
ko.applyBindings(new ViewModel("Planet", "Earth"));
I would like to create a binding to the childProperty.
I created a jsfiddle here
Thanks
So so very close!
You want
<span data-bind="text: parentProperty().childProperty"> </span>
Your updated fiddle http://jsfiddle.net/szk2e/2/
Adding an answer here as this is the best fit to my particular situation...
There is a situation where Tim's answer won't work. This is when the parent property can be undefined.
For example, if you're using the common pattern of itemsSource and selectedItem (where the user selects a single item from a list) selectedItem will be undefined on first evaluation, and whenever the user has undone their selection. Using the binding text:selectedItem().SomeProperty will "break" knockout, preventing bindings from being evaluated. Note, trying to short circuit this using the visible binding (e.g., text:selectedItem().SomeProperty, visible:selectedItem) will NOT work.
In this case, you have to use the with binding to switch the binding context to the value of the property. So, using OP's example...
<p>First name: <input data-bind="value: firstName" /></p>
<p>Last name: <input data-bind="value: lastName" /></p>
<h2>Hello, <span data-bind="text: fullName"> </span>!</h2>
<h2 data-bind="with:parentProperty">ChildProperty:
<span data-bind="text: childProperty"></span>!
</h2>
Note that the behavior for this binding is (from the docs)
The with binding will dynamically add or remove descendant elements depending on whether the associated value is null/undefined or not
If you also need to hide the container depending on whether the property is undefined or not, then you should use the <!-- ko --> virtual element to surround the container. More information can be found here.