I'm trying to create a custom knockout bindingHandler to add a custom jQuery UI widget but have run into trouble trying to access the elements created during binding. I'm sure there's something fundamental about this that I'm missing. I have the following html:
<table data-bind="myGrid: {}">
<thead>
<tr data-bind="foreach: { data: columns, as: 'column' }">
<th data-bind="text: column"></th>
</tr>
</thead>
<tbody data-bind="foreach: { data: rows, as: 'row' }">
<tr data-bind="foreach: { data: $parent.columns, as: 'column' }">
<td data-bind="text: row[column]"></td>
</tr>
</tbody>
</table>
And the following javascript:
var vm = {
columns: [
'A', 'B'
],
rows: []
};
$.widget("my.grid", {
_create: function() {
var columns = this.element.find('th');
}
});
ko.bindingHandlers.myGrid = {
init: function (element) {
//$(element).grid();
},
update: function(element) {
$(element).grid();
}
};
ko.applyBindings(vm);
When the widget is created, it needs to find each th element created from the binding. However, the elements don't appear to be created at that point in time. I have tried both the init and update methods of the bindinghandler.
This works if I manually add the widget to the element, just not within the bindinghandler.
When and how do I access the elements created from data-binding so that my jQuery widget can modify them?
You need to take control of the bindings to your descendant elements within your custom binding handler.
See http://knockoutjs.com/documentation/custom-bindings-controlling-descendant-bindings.html
But basically, do something like:
ko.bindingHandlers.myGrid = {
init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
// bind our child elements (which will create the virtual foreach elements)
ko.applyBindingsToDescendants(bindingContext, element);
// make your grid widget
$(element).grid();
// tell KO we have already bound the children
return { controlsDescendantBindings: true };
},
update: function() { ... }
};
Related
In an ASP.Net MVC application I have a table that's based on a model passed to the view. For this reason, the table is not based on a Json and the fields have no column names that I can reference in the 'columns'[] section of the script (or at least I don't know how!...). Still, the table works fine except that I can't manage to generate a calculated field. In all the various experiments I keep getting a NaN or undefined result, depending on the various attempts, and I understand this is because evidently I'm not referencing the proper data value, but how can I do that if I don't have column names?
The table is properly structured, thead, tbody and tfoot have the same number of columns. This is the html for the table:
<table id="mainTable" class="table table-hover">
<thead>
<tr>
<th>#Html.DisplayNameFor(model => model.Value1)</th>
<th>#Html.DisplayNameFor(model => model.Value2)</th>
<th>test</th>
</tr>
</thead>
<tbody>
#foreach (var item in Model)
{
<tr>
<td>#Html.DisplayFor(modelItem => item.Value1)</td>
<td>#Html.DisplayFor(modelItem => item.Value2)</td>
<td></td>
</tr>
}
</tbody>
<tfoot><tr><th></th><th></th><th></th></tr></tfoot>
</table>
The render function on first and second columns works just fine. The third column is where I am trying to calculate and display the difference between first and second column, and that's where I get NaN or undefined. This is the script:
<script>
$(document).ready(function () {
// DataTable
var table = $('#mainTable').DataTable({
responsive: true,
columns: [
{ render: function (data, type, row) { return data + '$' } },
{ render: function (data, type, row) { return data + '$' } },
{ render: function (data, type, row) { return (row.Value1 - row.Value2) + '$' }}]
});
});
</script>
This is the result I'm getting on this particular attempt. I've tried other versions, unfortunately with similar disappointing results:
Value 1 Value 2 test
200.00$ 100.00$ NaN$
etc...
I have also tried this:
columns: [ { data: 'Value1', render: function (data, type, row) { return data + '$' } } ]
and this
columns: [ { name: 'Value1', render: function (data, type, row) { return data + '$' } } ]
and this
columns: [ { data: 'Value1', name: 'Value1', render: function (data, type, row) { return data + '$' } } ]
but in all cases the entire script fails.
I also tried to assign names with columnDefs [...] but no luck.
Would someone be so kind to tell me what I'm doing wrong, or what I should do to make this work?
Thank you!
probably going about this the wrong way but I have an html table that is populated using a knockout observable array using foreach. in each row I have a drop down. I like the jquery ui select menu so I am using that. unfortunately when you run the fiddle you will notice that when updating the drop down the corresponding knockout selected value is not being updated.
here is the fiddle https://jsfiddle.net/8rw4ruze/3/
here is the html.
<table class="table table-condensed table-responsive">
<thead>
<th>id</th>
<th>animal</th>
<th>Selected Value</th>
</thead>
<tbody data-bind="foreach: tableRows">
<tr>
<td data-bind="text: id"></td>
<td>
<select class="form-control" data-bind="options: recordList,
optionsText: 'desc',
optionsValue: 'val',
value: selectedVal,
attr: {id: selectId}">
</select>
</td>
<td data-bind="text: selectedVal"></td>
</tr>
</tbody>
</table>
and here is the javascript
function record(val, desc) {
var self = this;
this.val = ko.observable(val);
this.desc = ko.observable(desc);
}
function tableRow(id, recordList) {
var self = this;
this.id = ko.observable(id);
this.recordList = ko.observable(recordList)
this.selectedVal = ko.observable('A');
this.selectId = ko.computed(function() {
return 'mySelect' + self.id()
}, this);
}
function Model() {
var self = this;
this.records = ko.observableArray("");
this.tableRows = ko.observableArray("");
}
var mymodel = new Model();
$(function() {
mymodel.records.push(new record('A', 'ant'));
mymodel.records.push(new record('B', 'bird'));
mymodel.records.push(new record('C', 'cat'));
mymodel.records.push(new record('D', 'dog'));
mymodel.tableRows.push(new tableRow(1, mymodel.records()));
mymodel.tableRows.push(new tableRow(2, mymodel.records()));
mymodel.tableRows.push(new tableRow(3, mymodel.records()));
mymodel.tableRows.push(new tableRow(4, mymodel.records()));
ko.applyBindings(mymodel);
for (var i = 0; i < 4; i++) {
var id = '#mySelect' + (i + 1)
$(id).selectmenu({
width: 125,
change: function(event, ui) {
var newVal = $(this).val();
mymodel.tableRows()[i].selectedVal(newVal);
}
});
}
});
thanks all I went with a data attribute. I'd prefer to use the custom binding but I'm not smart enough to figure that out so I went with this.
for (var i = 0; i < 4; i++) {
var id = '#mySelect' + (i + 1)
$(id).selectmenu({
width: 125,
change: function(event, ui) {
var newVal = $(this).val();
var index = $(this).data( "index" );
mymodel.tableRows()[index].selectedVal(newVal);
}
}).data( "index", i );
}
here is the fiddle https://jsfiddle.net/8rw4ruze/7/
I think I got it working with the custom binding here it is
https://jsfiddle.net/8rw4ruze/8/
Matt.k is correct that your value of i is not what you want it to be when it is used. One way around that is to use this loop instead of a simple for:
$('select').each(function (i, e) {
e.selectmenu({
width: 125,
change: function(event, ui) {
var newVal = $(this).val();
mymodel.tableRows()[i].selectedVal(newVal);
}
});
});
This is a little fragile, as it relies on the i and the e to correspond correctly. A more robust (and typical in Knockout) approach is to use a binding handler, which would allow you to specify the binding and have Knockout go through the elements and make the necessary calls. Surprisingly, a little web-searching didn't turn one up for me.
I see there is a knockout-jqueryui project on github that might also give you a clean way of using the widgets you want.
I need to pass props to a child component Product but i don't know what i'm missing here.
Parent Component:
var Products = React.createClass({
getInitialState: function(){..},
deleteProduct: function(){..},
updateProduct: function(){..},
render: function(){
var products = _.map(this.state.products, function(product){
return(
<Product key={product.id} product={product} handleDeleteProduct={this.deleteProduct} handleEditProduct={this.editProduct} formData={this.props.formData}/>
)
});
return (
<table>
<thead>
<tr>
<th>Name</th>
<th>Quantity</th>
<th>Price</th>
<th colSpan="4">Actions</th>
</tr>
</thead>
<tbody>
{products}
</tbody>
</table>
)
}
});
Child Component:
var Product = React.createClass({
console.log(this); //props: handleDeleteProduct: undefined, handleEditProduct: undefined
handleEdit: function() {
e.preventDefault();
data = {..};
$.ajax({
..,
success: (function(_this) {
console.log(_this); //props: handleDeleteProduct: undefined, handleEditProduct: undefined
return function(data) {
_this.setState({
edit: false
});
return _this.props.handleEditProduct(_this.props.product, data);
};
})(this)
});
}
});
I'm able to use key and product as a props inside the component but not this.props.handleDeleteProduct and this.props.handleEditProduct.
I think may be i'm using the props inside the success callback of the $.ajax and then may be some thing related to async request. Not sure.
The error i'm getting is
Uncaught TypeError: _this.props.handleEditProduct is not a function
I'm not sure what i'm doing wrong here. I tried to loop directly in between <tbody> but still no luck.
Also here i'm calling the functions like this.deleteProduct as a reference but not by function call. And if i do by calling by function then it is reporting execjs error.
I took this as a reference for looping inside JSX: loop inside React JSX
But no luck. Please help.
You are passing handleEditProduct={this.editProduct}, when the function is called updateProduct in your parent component. Change it to handleEditProduct={this.updateProduct} and I'll bet it works
EDIT:
Since that didn't work, I looked a little harder and I think I see what the problem is. I'm fairly sure that _ doesn't autobind this like React.createClass does. So when you map over your products here:
var products = _.map(this.state.products, function(product){
return(
<Product key={product.id} product={product} handleDeleteProduct={this.deleteProduct} handleEditProduct={this.editProduct} formData={this.props.formData}/>
)
});
this is not set to your react element. Try keeping a reference to this before you map, explicitly bind this to your map function, or use ES6 arrow functions: https://babeljs.io/docs/learn-es2015/#arrows-and-lexical-this. The simplest way to achieve what you want would be to save this in a variable:
var self = this;
var products = _.map(this.state.products, function(product){
return(
<Product key={product.id} product={product} handleDeleteProduct={self.deleteProduct} handleEditProduct={self.editProduct} formData={self.props.formData}/>
)
});
You can also use bind to achieve the same effect: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
I have a web API which successfully returns an array of blog posts in JSON format:
[{"ID":1,"Title":"First Blog Post","Body":"Some Content"},{"ID":2,"Title":"Second BlogPost","Body":"Some other content"}]
For exercise purposes, I want to display all posts in a list using Knockout.js.
Here is my code:
<script>
$(document).ready(function () {
function AppViewModel() {
var self = this;
self.posts = ko.observableArray([
{ Title: 'Default Title', Body: 'Default Body' },
]);
$.getJSON('api/posts', function (data) {
ko.mapping.fromJSON(data, {}, self.posts);
});
}
ko.applyBindings(new AppViewModel());
});
My bindings:
<tbody data-bind="foreach: posts">
<tr>
<td data-bind="text: Title"></td>
<td data-bind="text: Body"></td>
</tr>
</tbody>
My table shows up empty, it's not showing the JSON data for some reason...
ANSWER: I had to change fromJSON to fromJS and it works! Thanks so much for your help everyone
In your case you need to specify the update target for the mapping.
From the Knockout Mapping plugin documentation:
If, like in the example above, you are performing the mapping inside
of a class, you would like to have this as the target of your mapping
operation. The third parameter to ko.mapping.fromJS indicates the
target. For example,
ko.mapping.fromJS(data, {}, someObject);
So in your case:
$.getJSON('api/posts', function (data) {
ko.mapping.fromJSON(data, {}, self.posts);
});
A working JSFiddle
Also note that that the property names are case sensitive and they should match in the JSON and in your viewmodel and bindings. e.g you retrive "Title" but you are using title
The json being returned has title case property names, but the original in self.posts has lowercase property names. I assume you are binding to the lowercase version in your template. Consider changing the json returned to lowercase too.
This sorts in one direction, but not the other. Is there something wrong with the table specifications. It wold be great if someone could post an HTML sample that has sorting working in both directions on a column with dollar values that include commas in them.
// create sorter
<script type="text/javascript" id="js">
$(document).ready(function() {
// call the tablesorter plugin
$("table.tablesorter").tablesorter({
// enable debug mode
debug: false
});
});
</script>
Not sure if a prefix is needed here (table.tablesorter):
// add parser through the tablesorter addParser method
<script type="text/javascript" id="js">
$.tablesorter.addParser({
// set a unique id
id: 'money',
is: function(s) {
// return false so this parser is not auto detected
return false;
},
format: function(s) {
return s.toLowerCase().replace("\$","").replace(",","");
},
// set type, either numeric or text
type: 'numeric'
});
Not sure if table.tablesorter is needed here:
// specify column
$(function() {
$("table.tablesorter").tablesorter({
headers: {
// column to be handled specially
7: {
sorter:'money'
}
}
});
});
</script>
The following is the top of the table:
<table cellspacing="1" class="tablesorter">
<thead>
<tr>
<th>Name</th>
<th>Major</th>
<th>Gender</th>
<th>English</th>
<th>Japanese</th>
<th>Calculus</th>
<th>Overall grades</th>
<th>Money</th>
</tr>
</thead>
<tbody>
<tr>
<td>Student01</td>
<td>Languages</td>
<td>male</td>
<td>80</td>
<td>70</td>
<td>75</td>
<td>bad</td>
<td>$1.00</td>
</tr>
Since you are working with numbers, you'll need to parse the string into a real number. Change this line in your parser:
format: function(s) {
return parseFloat( s.toLowerCase().replace("\$","").replace(",","") );
}