I have been working on KnockoutJS since two weeks and I am trying to add inline editing in a grid using KnockOutJS and jQuery. My html:
<table>
<thead>
<tr>
<th>ID</th>
<th>Title</th>
<th>Excerpts</th>
<th>Content</th>
</tr>
</thead>
<tbody data-bind="foreach: Articles">
<tr>
<td data-bind="text: id"></td>
<td data-bind="text: Excerpts, event: { dblclick: $root.editField }"></td>
<td data-bind="text: Excerpts, event: { dblclick: $root.editField }"></td>
<td data-bind="text: Content, event: { dblclick: $root.editField }"></td>
</tr>
</tbody>
My JS:
function Articles(Articles) {
this.id = ko.observable(Articles.id);
this.Title = ko.observable(Articles.Title);
this.Excerpts = ko.observable(Articles.Excerpts);
this.Content = ko.observable(Articles.Content);
}
var ViewModel = {
Articles: ko.observableArray
([new Articles(id = 1, Title = "Title1", Excerpts = "Excerpts1", Content = "Content1")]),
loadArticles: function () {
var self = this;
self.Articles(Articles);
},
editField: function (d, e) {
var currentEle = $(e.target);
var value = $(e.target).html();
$(currentEle).html('<input class="thVal" type="text" value="' + value + '" />');
$(currentEle).find('input').focus();
$(currentEle).find('input').keyup(function (event) {
if (event.keyCode == 13) {
$(currentEle).html($(currentEle).find('input').val().trim());
//CallAjaxWithData('/MTB_Articles/EditArticle', 'POST', ko.toJSON(d), null, null); // To update data in server
}
});
$(document).click(function () {
if ($(currentEle).find('input').val() != null) {
$(currentEle).html($(currentEle).find('input').val().trim());
//CallAjaxWithData('/MTB_Articles/EditArticle', 'POST', ko.toJSON(d), null, null); // To update data in server
}
});
}
}
ko.applyBindings(ViewModel);
ViewModel.loadArticles();
Whenever the user double clicks on any td in the grid, I am adding an input field dynamically using the editField function and binding the updated value to the td again when user presses enter key or clicks somewhere else on the page. The parameter d in the editField function gives the current viewmodel object. I have to update the corresponding value in the parameter d when user edits the value in a particular column, convert d to json format and send it to server via ajax call to be updated in the database. The changes made by the user should be reflected in the view model( the parameter d). So how can we update the view model using dynamically added controls?
JSFiddle for this
You can do it in a more 'ko-ish' way that will make it easier for you.
KO is mostly declarative, and you're mixing declarative and procedural (jQuery) code.
To make it declarative, and much easier to implement, do the following:
add an editing observable property to your Articles. Initialize it to false
inside the <td>'s show either the text, or a data-bound input, depending on the value of the editing observable property
use the double click event, to set editing to true
use the enter key press to do what you need (ajax) with the values in your model, and set the editing to false again
You can do it like this:
<td>
<!-- ko ifnot: editing, text: Excerpts --><!-- /ko -->
<!-- ko if: editing -->
<input class="thVal" type="text" data-bind="value: Excerpts" />
<!--- /ko -->
</td>
Or even shorter:
<td>
<!-- ko ifnot: editing, text: Excerpts --><!-- /ko -->
<input class="thVal" type="text" data-bind="value: Excerpts, if: editing" />
</td>
Related
I have a multi dropdown list and I need to do the following:
1. Make sure that when selecting value in one dropdown list it won't appear in the others (couldn't find a proper solution here).
2. When selecting the value "Text" a text field (<input>) will apear instead of the Yes/no dropdown.
3. "Choose option" will appear only for the first row (still working on it).
4. Make sure that if "Text" is selected, it always will be on the top (still working on it).
JSFiddle
HTML:
<div class='liveExample'>
<table width='100%'>
<tbody data-bind='foreach: lines'>
<tr>
<td>
Choose option:
</td>
<td>
<select data-bind='options: filters, optionsText: "name", value: filterValue'> </select>
</td>
<td data-bind="with: filterValue">
<select data-bind='options: filterValues, optionsText: "name", value: "name"'> </select>
</td>
<td>
<button href='#' data-bind='click: $parent.removeFilter'>Remove</button>
</td>
</tr>
</tbody>
</table>
<button data-bind='click: addFilter'>Add Choice</button>
JAVASCRIPT:
var CartLine = function() {
var self = this;
self.filter = ko.observable();
self.filterValue = ko.observable();
// Whenever the filter changes, reset the value selection
self.filter.subscribe(function() {
self.filterValue(undefined);
});
};
var Cart = function() {
// Stores an array of filters
var self = this;
self.lines = ko.observableArray([new CartLine()]); // Put one line in by default
// Operations
self.addFilter = function() { self.lines.push(new CartLine()) };
self.removeFilter = function(line) { self.lines.remove(line) };
};
ko.applyBindings(new Cart());
I will appeaciate your assist here! Mainly for the first problem.
Thanks!
Mike
If you want to limit the options based on the options that are already selected in the UI, you'll need to make sure every cartLine gets its own array of filters. Let's pass it in the constructor like so:
var CartLine = function(availableFilters) {
var self = this;
self.availableFilters = availableFilters;
// Other code
// ...
};
You'll have to use this new viewmodel property instead of your global filters array:
<td>
<select data-bind='options: availableFilters,
optionsText: "name",
value: filterValue'> </select>
</td>
Now, we'll have to find out which filters are still available when creating a new cartLine instance. Cart manages all the lines, and has an addFilter function.
self.addFilter = function() {
var availableFilters = filters.filter(function(filter) {
return !self.lines().some(function(cartLine) {
var currentFilterValue = cartLine.filterValue();
return currentFilterValue &&
currentFilterValue.name === filter.name;
});
});
self.lines.push(new CartLine(availableFilters))
};
The new CartLine instance gets only the filter that aren't yet used in any other line. (Note: if you want to use Array.prototype.some in older browsers, you might need a polyfill)
The only thing that remains is more of an UX decision than a "coding decision": do you want users to be able to change previous "Choices" after having added a new one? If this is the case, you'll need to create computed availableFilters arrays rather than ordinary ones.
Here's a forked fiddle that contains the code I posted above: http://jsfiddle.net/ztwcqL69/ Note that you can create doubled choices, because choices remain editable after adding new ones. If you comment what the desired behavior would be, I can help you figure out how to do so. This might require some more drastic changes... The solution I provided is more of a pointer in the right direction.
Edit: I felt bad for not offering a final solution, so here's another approach:
If you want to update the availableFilters retrospectively, you can do so like this:
CartLines get a reference to their siblings (the other cart lines) and create a subscription to any changes via a ko.computed that uses siblings and their filterValue:
var CartLine = function(siblings) {
var self = this;
self.availableFilters = ko.computed(function() {
return filters.filter(function(filter) {
return !siblings()
.filter(function(cartLine) { return cartLine !== self })
.some(function(cartLine) {
var currentFilterValue = cartLine.filterValue();
return currentFilterValue &&
currentFilterValue.name === filter.name;
});
});
});
// Other code...
};
Create new cart lines like so: self.lines.push(new CartLine(self.lines)). Initiate with an empty array and push the first CartLine afterwards by using addFilter.
Concerning point 2: You can create a computed observable that sorts based on filterValue:
self.sortedLines = ko.computed(function() {
return self.lines().sort(function(lineA, lineB) {
if (lineA.filterValue() && lineA.filterValue().name === "Text") return -1;
if (lineB.filterValue() && lineB.filterValue().name === "Text") return 1;
return 0;
});
});
Point 3: Move it outside the foreach.
Point 4: Use an if binding:
<td data-bind="with: filterValue">
<!-- ko if: name === "Text" -->
<input type="text">
<!-- /ko -->
<!-- ko ifnot: name === "Text" -->
<select data-bind='options: filterValues, optionsText: "name", value: "name"'> </select>
<!-- /ko -->
<td>
Updated fiddle that contains this code: http://jsfiddle.net/z22m1798/
I'm trying to reimplement a student attendance example from the Udacity JavaScript Design Patterns course. So far I've managed to recreate the table and correctly populate it with some student data, however it appears when I change a checkbox value this isn't updated in the model.
For example, when I display
debugVM.studentList()[0].days();
in the console the output displays the initial attendance data instead of the current state of the checkboxes. A JSFiddle for this can be found here.
index.html
<table>
<thead>
<tr>
<th>Student</th>
<!-- ko foreach: attendance -->
<th data-bind="html: $data"></th>
<!-- /ko -->
</tr>
</thead>
<tbody data-bind="foreach: studentList">
<tr>
<td data-bind="html: name, click: $parent.debugStudent"></td>
<!-- ko foreach: days -->
<td>
<input type="checkbox" data-bind="value: $data, checkedValue: $data, checked: $data" />
</td>
<!-- /ko -->
</tr>
</tbody>
</table>
app.js
var Student = function(student){
this.name = ko.observable(student.name);
this.days = ko.observableArray(student.days);
};
var ViewModel = function() {
var self = this;
this.attendance = ko.observableArray([1,2,3,4,5,6,7,8,9,10,11,12]);
this.studentList = ko.observableArray([]);
students.forEach(function(student) {
self.studentList.push(new Student(student));
});
self.debugStudent = function() {
console.log(self.studentList()[0].days());
};
};
var debugVM = new ViewModel();
ko.applyBindings(debugVM);
From the knockout documentation
Key point: An observableArray tracks which objects are in the array,
not the state of those objects
In your case this means that days should be not observable array, but array of observables.
For example you can add new viewModel Day:
var Day = function(isChecked) {
this.isChecked = ko.observable(isChecked);
}
And set the days property like this
this.days = [];
for (var i = 0; i< student.days.length; i++) {
this.days.push(new Day(student.days[i] == 1));
}
See working fiddle
I have this HTML and am trying to use it to generate a table with the option to add more rows:
<thead>
<tr>
<th>Item</th>
<th>Cost</th>
<th>Amount</th>
<th>
<button class="btn btn-default" data-bind="click: addItem">Add Item</button>
</th>
</tr>
</thead>
<tbody data-bind="foreach: items">
<tr>
<td data-bind="template: {name: $parent.dynTemplate, data: item }">
<td data-bind="template: {name: $parent.dynTemplate, data: cost() }"></td>
<td data-bind="template: {name: $parent.dynTemplate, data: amount() }">
<td></td>
</tr>
</tbody>
I'm using two different templates:
<script id="inpTmp" type="text/html">
<input data-bind="value: $data" />
</script>
<script id="dispTmp" type="text/html">
<p data-bind="text: $data"></p>
</script>
and am choosing which one to call based on the results of the dynTemplate function. The knockout that I have powering this is very simple:
function ItemAdd(name, icost, iamount) {
var self = this;
self.item = name;
self.cost = ko.observable(icost);
self.amount = ko.observable(iamount);
}
function TestModel() {
var self= this;
self.items= ko.observableArray([
new ItemAdd("a", 5, 10),
new ItemAdd("b", 6, 4)
]);
self.addItem= function() {
self.items.push(new ItemAdd("", 0, 0));
};
self.dynTemplate= function(init, s) {
if(init=== 0 || init=== '') {
return 'inpTmp';
}
return 'dispTmp';
};
}
ko.applyBindings(new TestModel());
The problem that I am running into is that when I enter values into newly created rows, the values in items do not change. They initialize properly, but when I run a function to log the values in items they stay as their defaults. If I use knockout if statements, then everything updates properly. However, using 6 sets of if statements didn't seem very effective so I wanted to see if I could pull it out into a function and then send back the proper template. I'm trying to have inputs there when the value is "" or 0, and then change them to <p> when something is entered.
I've tried changing how the data is passed into the template, and I've tried to assign context using with, but to no avail. Calling dynTemplate does not work unless prefixed by $root or $parent. If that is changing the context, is there a way to reset it?
Is this a problem of context, and if so, is there a way to assign context with the dynTemplate function? Or are the newly created elements from the template not properly binding? I've searched quite a bit, and have found templates within foreach loops, but have not seen functions being used to apply them. If there is a better way to do this, please let me know.
Thank you for the help
Your current sample doesn't work because ko dependency tacker doesn't see that your model field is changed. It happens because 'init' is unwrapped value (not an observable).
This fiddle shows how to make it work with single 'item' field.
http://jsfiddle.net/tabalinas/VXXqr/
In this changed version of dynTemplate we get the value of observable, and thus dependency tracker can see that value changed. Of course, we need to change the template.
self.dynTemplate= function(item, s) {
var val = item.item();
if(val=== 0 || val=== '') {
return 'inpTmp';
}
return 'dispTmp';
};
<script id="inpTmp" type="text/html">
<input data-bind="value: $data.item" />
</script>
<script id="dispTmp" type="text/html">
<p data-bind="text: $data.item"></p>
</script>
For your case, where you need universal template for all fields you can do the following: pass as data the name of the field. The template will pick up data from $parent. dynTemplate func is changed accordingly.
<tbody data-bind="foreach: items">
<tr>
<td data-bind="template: {name: $parent.dynTemplate, data: 'item' }">
</td>
<td data-bind="template: {name: $parent.dynTemplate, data: 'cost' }">
</td>
<td data-bind="template: {name: $parent.dynTemplate, data: 'amount' }">
</td>
</tr>
</tbody>
<script id="inpTmp" type="text/html">
<input data-bind="value: $parent[$data]" />
</script>
<script id="dispTmp" type="text/html">
<p data-bind="text: $parent[$data]"></p>
</script>
self.dynTemplate= function(field, context) {
var value = context.$parent[field]();
if(value=== 0 || value=== '') {
return 'inpTmp';
}
return 'dispTmp';
};
See fiddle http://jsfiddle.net/tabalinas/VXXqr/5/
When I update an observable element of Knockout the UI is not getting update
HTML
<tbody data-bind="foreach: students, visible: !students().isDeleted">
<tr>
<td data-bind="text: RollNo"></td>
<td data-bind="text: Name"></td>
<td data-bind="text: Phone"></td>
<td data-bind="text: Email"></td>
<td>
Edit
Delete
</td>
</tr>
</tbody>
Javascript
function StudentModel(student){
this.RollNo = ko.observable(student.RollNo);
this.Name = ko.observable(student.Name);
this.Phone = ko.observable(student.Phone);
this.Email = ko.observable(student.Email);
this.isDeleted = ko.observable(student.isDeleted);
this.isEdited = ko.observable(student.isEdited);
}
function StudentViewModel() {
//Array of students
this.students = ko.observableArray();
//Data retrived from the server
var listStudent= JSON.parse(#Html.Raw(ViewBag.StudentsList));;
var mappedStudents = $.map(listStudent, function(student) { return new StudentModel(student) });
//Map it to show the data
this.students(mappedStudents);
//Delete student
this.deleteStudent= function(student){
var stu = this.students()[this.students.indexOf(student)];
stu.isDeleted(true);
}.bind(this);
When I click on Delete the UI is not updated... When I try stu.isDeleted=true; still it does not works... Any help would be appreciated...
Fiddle
The problem is in the databinding.
visible: !students().isDeleted
This looks up the isDeleted property in the observable array. Which doesn't exist, so it is false and will always show all the elements.
If you want to hide the students the visible binding should be on the <tr>.
If you want to remove the student from the observable array you can just remove it.
http://jsfiddle.net/8fALs/2/
I have a table, that is filled through data-binds with data from observable array of objects (persons). When i click a certain cell of table, index of a line, and index of a cell is written into variables "self.currentLine" and "self.currentCell", while input appears above with 100% width and 100% height, covering that data with itself.
Is there a possibility to get access to certain field of certain object in observable array, using only indexes of fields instead of using field names? (ex. not self.persons[0]'name', but self.persons[0][0])
Here is a code(JS):
function person(fullname, age, sex, married)
{
this.name = ko.observable(fullname); //string, only observable field, while i'm trying to get this working properly.
this.age = age; //Data
this.sex = sex; //string
this.married = married; //bool
};
function personViewModel()
{
var self = this;
self.currentLine = ko.observable();
self.currentCell = ko.observable();
self.columnNames = ko.observableArray([
'Name',
'Age',
'Sex',
'Married'
]);
self.persons = ko.observableArray([...]);
};
self.setLine = function(index)
{
self.currentLine(index);
};
self.setCell= function(cellIndex)
{
self.currentCell(cellIndex);
};
};
ko.applyBindings(new personViewModel());
And HTML code i use:
<table>
<thead data-bind="template: { name: 'tableHeader', data: columnNames }" />
<tbody data-bind="template: { name: 'tableContent', foreach: persons }" />
</table>
<script id="tableHeader" type="text/html">
<tr data-bind="foreach: $data">
<td data-bind="text: $data,
css: { 'active': $root.currentItem() == $data }">
</td>
</tr>
</script>
<script id="tableContent" type="text/html">
<tr data-bind="click: $root.setLine.bind($data, $index())">
<td data-bind="click: $root.setCell.bind($data, $element.cellIndex)">
<span data-bind="text: name"></span>
<input type="text" data-bind="visible: $root.currentCell() == 0 && $index() == $root.currentLine(),
value: name"/> <!--fixed-->
</td>
</tr>
</script>
In html i set input visible according to cell clicked in the table. So now i need to pass a value of a cell to an input, so i could edit this data.
UPDATE: as usual, i've forgot to put round brackets '()' after value: name() in input. But here comes second question. As i know value must be automaticly changed while input loses his focus. But mine doesn't change...
Use the input value binding to to pass a value of a cell:
AFAIK, there is no way to access a field with its supposed index, to read a field from an object in observableArray you may use this syntax :
persons()[index].fieldName(), given that the field is observable also.
hope it help.