I have an array that looks something like the following
var initData = [
new Order({
orderId: "183175",
name: "Columbus Africentric",
production: [{
pType: "Art Time",
by: "MJ"
}, {
pType: "Front Pocket",
by: "WB"
}]
}),
new Order({
orderId: "198675",
name: "Stanford High",
production: [{
pType: "Art Time",
by: "MJ"
}, {
pType: "Full Back",
by: "WB"
}]
})
]
I'm trying to do a with binding to show only extra information for on order when the item is clicked on. So I have a foreach for the orders that shows the orderId and the name in a table, and a button to click that then should show all production items for the chosen order. Something like the following
<tbody data-bind="foreach:orders">
<tr>
<td>
<label class="read" data-bind="text:orderId, visible:true" />
</td>
<td>
<label class="read" data-bind="text:name, visible:!$root.isItemEditing($data)" />
</td>
<td>
<td class="tools">
<div data-bind="if: production"><button data-bind="click: $root.toggleProductionMode">Production</button>
</div>
</td>
</tr>
<tr data-bind="visible: showProductionOrder, with: production">
<td colspan="5">
<h3>Production Summary</h3>
<table class="ko-grid">
<thead>
<tr>
<th>Type</th>
<th>By</th>
</tr>
</thead>
<tbody
<tr>
<td>
<label class="read" data-bind="text:pType, visible:!$root.isItemEditing($data)" />
</td>
<td>
<label class="read" data-bind="text:by, visible:!$root.isItemEditing($data)" />
</td>
</tr>
</tbody>
</table>
</tr>
I think I need to use a foreach to get to the production information. Can a foreach binding be used inside a with binding? Or do I even need one? If I have it bound using the "with" binding, is there a certain way to get to the multiple production items? I know this is super easy and it's probably staring me right in the face.
also, when creating the Item Model I'm doing the following, which I think might be incorrect.
function Order(data) {
self.orderId = ko.observable();
self.name = ko.observable();
self.production = ko.observableArray([
[
self.pType = ko.observable(),
self.by = ko.observable()
]
]);
}
You don't have to create a new table inside the main table. For a child collection you have to use "ko foreach: production" as a html comment and then add your tr tags afterwards to display the production items. Have a look into this JSFiddle example.
// HTML
<table>
<tr>
<th>Student ID</th>
<th>Student Name</th>
</tr>
<tbody data-bind="foreach: Students">
<tr>
<td data-bind="text: StudentID"></td>
<td data-bind="text: StudentName"></td>
</tr>
<!-- ko foreach: Courses -->
<tr>
<td style='padding-left:20px;' data-bind="text: CourseID"></td>
<td style='padding-left:20px;' data-bind="text: CourseName"></td>
</tr>
<!-- /ko -->
</tbody>
</table>
// KNOCKOUT CODE
function StudentViewModel() {
var self = this;
self.Students = [
{ StudentID: "1", StudentName: "Ali",
Courses: [ { CourseID: "100", CourseName: "Math" }, { CourseID: "102", CourseName: "Physics" } ]
},
{ StudentID: "2", StudentName: "Isa" ,
Courses: [ { CourseID: "103", CourseName: "Chemistry" }, { CourseID: "104", CourseName: "Social Studies" } ] },
{ StudentID: "3", StudentName: "Zoya" ,
Courses: [ { CourseID: "100", CourseName: "Math" }, { CourseID: "106", CourseName: "Stats" } ] },
];
}
ko.applyBindings(new StudentViewModel());
Related
I am currently working with a relatively large Vue (Vue 2) project that uses a lot of tables, and I want to create a reusable table component where each column is a child component / slot. Something like this:
<Table :data="data">
<TableColumn field="id" label="ID" />
<TableColumn field="name" label="Name" />
<TableColumn field="date_created" label="Created" />
</Table>
const data = [
{ id: 1, name: 'Foo', date_created: '01.01.2021' },
{ id: 2, name: 'Bar', date_created: '01.01.2021' }
];
Which in turn should output this:
<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Created</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>Foo</td>
<td>01.01.2021</td>
</tr>
<tr>
<td>2</td>
<td>Bar</td>
<td>01.01.2021</td>
</tr>
</tbody>
</table>
We've previously used Buefy, but the vendor size becomes unnecessarily large, as we only use a fraction of the components' functionality - so I want to create a lightweight alternative.
With this data you only need 2 Props, labels and data.
<!-- Component -->
<table>
<thead>
<tr>
<td v-for="(label, labelIndex) in labels" :key="labelIndex">
{{ label.text }}
</td>
</tr>
</thead>
<tbody>
<tr v-for="(item, itemIndex) in data" :key="itemIndex">
<td v-for="(label, labelIndex) in labels" :key="labelIndex">
{{ item[label.field] }}
</td>
</tr>
</tbody>
</table>
// Data and labels
const labels = [
{ text: ID, field: id },
{ text: Name, field: name },
{ text: Created, field: date_created },
]
const data = [
{ id: 1, name: 'Foo', date_created: '01.01.2021' },
{ id: 2, name: 'Bar', date_created: '01.01.2021' }
];
<table-component
:labels="labels"
:data="data"
>
</table-component>
If you need something more complex you can use nested components combined with a named slots for the header or footer of the table (or other options like search).
For simplicity's sake, I have a simple array. What I'm looking to do is to loop through the array's questions for the first category, and then loop through the array's questions for the second category, and so on.
The "Answer" will be text input fields or radio buttons or whatever.
How would I "nest" the two loops in knockout.js? I can't separate it into two arrays, because then I lose the relationship between the two loops.
//knockout.js viewModel
function SurveyViewModel() {
var self = this;
self.EvaluationElement = ko.observableArray([
{category: "Category1", question: "Question #1"},
{category: "Category1", question: "Question #2"},
{category: "Category2", question: "Question #3"},
{category: "Category2", question: "Question #4"},
{category: "Category3", question: "Question #5"},
{category: "Category3", question: "Question #6"},
{category: "Category4", question: "Question #7"},
And I want to build a view (HTML) that is essentially a table nested as follows:
<table>
<tr>
<th colspan="3">Category 1</th>
</tr>
<tr>
<td>Question #1</td>
<td>Answer</td>
<td></td>
</tr>
<tr>
<td>Question #2</td>
<td>Answer</td>
<td></td>
</tr>
<tr>
<td colspan="3">Category 2</td>
</tr>
<tr>
<td>Question #3</td>
<td>Answer</td>
<td></td>
</tr>
<tr>
<td>Question #4</td>
<td>Answer</td>
<td></td>
</tr>
<tr>
<td>Question #5</td>
<td>Answer</td>
<td></td>
</tr>
</table>
I'm trying to do something like this... there are obvious errors, but hopefully it conveys my thinking.
<br>
<table class="table table-hover">
<tbody data-bind="foreach: EvaluationElement/Category">
<tr>
<td colspan="2" data-bind="text: category></td>
</tr>
<data-bind="foreach: EvaluationElement">
<tr>
<td data-bind="text: question"></td>
<td>Answer</td>
<td></td>
</tr>
<// close loop>
</tbody>
</table>
<br>
<button data-bind="click: submitSurvey">Submit</button>
You should restructure your data in your view model to keep your view nice and simple. If your view requires table bodies per category, that's what the viewmodel data should reflect.
To restructure the list of questions in to a list of categories, you can use Knockout's pureComputed.
We create a function that does the data transformation from [ { category, question } ] to [ { category, questions: [ { category, question ] } ].
This very much resembles a groupBy. There are many ways to write a grouping function; search for "group by property in javascript" if you want to learn more about the different approaches.
With the new structure, the view very much resembles your desired format:
function SurveyViewModel() {
var self = this;
self.EvaluationElement = ko.observableArray([{
category: "Category1",
question: "Question #1"
},
{
category: "Category1",
question: "Question #2"
},
{
category: "Category2",
question: "Question #3"
},
{
category: "Category2",
question: "Question #4"
},
{
category: "Category3",
question: "Question #5"
},
{
category: "Category3",
question: "Question #6"
},
{
category: "Category4",
question: "Question #7"
}
]);
self.categories = ko.pureComputed(function() {
// Assumes questions sorted by category
return self.EvaluationElement().reduce(
function(cats, q, i, qs) {
const prev = qs[i - 1];
if (!prev || prev.category !== q.category) {
cats.push({
category: q.category,
questions: [ q ]
});
} else {
cats[cats.length - 1].questions.push(q);
};
return cats;
}, []);
});
};
ko.applyBindings(new SurveyViewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<table>
<tbody data-bind="foreach: categories">
<tr>
<td colspan="2" data-bind="text: category"></td>
</tr>
<!-- ko foreach: questions -->
<tr>
<td data-bind="text: question"></td>
<td>Answer</td>
<td></td>
</tr>
<!-- /ko -->
</tbody>
</table>
I am trying to learn a little bit of knockout.js so I am running through the examples on the website in particular the contact example. (Knockout Contacts Editor)
I am trying to extend this example by adding a select list with a property of 'ContactType'
ContactType is a basic list containing 2 objects.
I have created a fork of the example and extended it slightly My Extended Example fiddle
var initialData = {
"names": [{
firstName: "Danny",
lastName: "LaRusso",
contactTypeId: 1,
phones: [{
type: "Mobile",
number: "(555) 121-2121"
}, {
type: "Home",
number: "(555) 123-4567"
}]
}, {
firstName: "Sensei",
lastName: "Miyagi",
contactTypeId: 2,
phones: [{
type: "Mobile",
number: "(555) 444-2222"
}, {
type: "Home",
number: "(555) 999-1212"
}]
}],
"contactTypes": [{
"id": 1,
"type": "Family"
}, {
"id": 2,
"type": "Friend"
}]
}
var ContactsModel = function (contacts) {
var self = this;
self.contactTypes = ko.observableArray(ko.utils.arrayMap(contacts.contactTypes,
function (contactType) {
return {
id: contactType.id,
type: contactType.type
};
}));
self.contacts = ko.observableArray(ko.utils.arrayMap(contacts.names, function (contact) {
return {
firstName: contact.firstName,
lastName: contact.lastName,
phones: ko.observableArray(contact.phones),
contactTypeId: ko.observable(contact.contactTypeId)
};
}));
self.addContact = function () {
self.contacts.push({
firstName: "",
lastName: "",
phones: ko.observableArray(),
contactTypeId: ko.observable()
});
};
self.removeContact = function (contact) {
self.contacts.remove(contact);
};
self.addPhone = function (contact) {
contact.phones.push({
type: "",
number: ""
});
};
self.removePhone = function (phone) {
$.each(self.contacts(), function () {
this.phones.remove(phone)
})
};
self.save = function () {
self.lastSavedJson(JSON.stringify(ko.toJS(self.contacts), null, 2));
};
self.lastSavedJson = ko.observable("")
};
ko.applyBindings(new ContactsModel(initialData));
HTML
<div class='liveExample'>
<h2>Contacts</h2>
<div id='contactsList'>
<table class='contactsEditor'>
<tr>
<th>First name</th>
<th>Last name</th>
<th>Contact Type</th>
<th>Phone numbers</th>
</tr>
<tbody data-bind="foreach: contacts">
<tr>
<td>
<input data-bind='value: firstName' />
<div><a href='#' data-bind='click: $root.removeContact'>Delete</a>
</div>
</td>
<td>
<input data-bind='value: lastName' />
</td>
<td>
<select class="form-control" data-bind="options: $root.contactTypes,
optionsText: 'type',
optionsValue:'id',
value:'contactTypeId',
optionsCaption: 'Please Select...'"></select>
</td>
<td>
<table>
<tbody data-bind="foreach: phones">
<tr>
<td>
<input data-bind='value: type' />
</td>
<td>
<input data-bind='value: number' />
</td>
<td><a href='#' data-bind='click: $root.removePhone'>Delete</a>
</td>
</tr>
</tbody>
</table> <a href='#' data-bind='click: $root.addPhone'>Add number</a>
</td>
</tr>
</tbody>
</table>
</div>
<p>
<button data-bind='click: addContact'>Add a contact</button>
<button data-bind='click: save, enable: contacts().length > 0'>Save to JSON</button>
</p>
<textarea data-bind='value: lastSavedJson' rows='5' cols='60' disabled='disabled'></textarea>
</div>
I have 2 problems which I am not sure what I am doing wrong with.
When the page is loaded the select list is not displaying the correct value. Instead its just displaying the 'Please Select' value
When I try save the record the selected value from the select list is not saved.
Am I doing something obviously wrong here?
The issue was you were setting the value binding incorrectly. Instead of value: 'contactTypeId' when it should just be value: contactTypeId without the quotations.
<select class="form-control" data-bind="options: $root.contactTypes,
optionsText: 'type',
optionsValue: 'id',
value: contactTypeId,
optionsCaption: 'Please Select...'">
</select>
Also for the save function it's better to use use knockouts ko.toJSON instead of JSON.stringify(ko.toJS
self.save = function () {
self.lastSavedJson(ko.toJSON(self.contacts), null, 2);
};
I am trying to implement that functionality on this demo:
http://opensoul.org/2011/06/23/live-search-with-knockoutjs/
I managed to get everything working, but the demo uses a very old version of knockout.
When I upgrade the knockout version the functionality breaks.
This is my updated code:
$(function() {
var assets = [
{id: "1", poster: "Pic010.jpg", name: "Mike", category: "category1", type: "Movie", popup: "1" },
{id: "2", poster: "Pic06.jpg", name: "James", category: "category2", type: "Movie", popup: "2" },
{id: "3", poster: "Pic04.jpg", name: "John", category: "category1", type: "Pop-up", popup: "3" },
{id: "4", poster: "Pic07.jpg", name: "Bob", category: "category2", type: "Pop-up", popup: "4" },
{id: "5", poster: "Pic011.jpg", name: "Mary", category: "category3", type: "Promo", popup: "5" }
];
var viewModel = {
assets: ko.observableArray(assets),
query: ko.observable(''),
search: function(value) {
viewModel.assets.removeAll();
for(var x in assets) {
if(assets[x].name.toLowerCase().indexOf(value.toLowerCase()) >= 0) {
viewModel.assets.push(assets[x]);
}
}
}
};
viewModel.query.subscribe(viewModel.search);
ko.applyBindings(viewModel);
});
The display:
<form action="#">
<input class="form-control" placeholder="Search…" type="search" name="q" data-bind="value: query, valueUpdate: 'keyup'" autocomplete="off">
</form>
<div class="content">
<table>
<tbody data-bind="foreach:assets">
<tr>
<td style="vertical-align: middle" data-bind="text: id"> </td>
<td><img data-bind="attr:{ src: '/manager/files/' + poster }" height="100px" /></td>
<td style="vertical-align: middle" data-bind="text: name"></td>
<td style="vertical-align: middle" data-bind="text: category"></td>
<td style="vertical-align: middle" data-bind="text: type"></td>
<td style="vertical-align: middle" data-bind="text: popup"></td>
<td class="actions" style="vertical-align: middle">
<i class="fa fa-pencil-square-o"></i>
<form data-bind="attr:{ action: '/Assets/delete/' + id, name: 'delete_' + id, id: 'delete_'+ id }" style="display:none;" method="post"><input type="hidden" name="_method" value="POST"></form>
</i>
</td>
</tr>
</tbody>
</table>
</div>
here are examples, working with Knockout version 1.21: http://jsfiddle.net/7ZLdk/1/ example not working with version 3.0: http://jsfiddle.net/7ZLdk/2/
This behavior has been changed back in 2011: Updated remove and removeAll to modify their underlying arrays rather han creating new arrays
So now when you call removeAll on an ko.observableArray it removes the items form the underlying arrays, so in your case it empties your original assets array.
A quick fix would be to clone the array when assigning to your ko.observableArray:
assets: ko.observableArray(assets.slice(0)),
Demo JSFiddle.
A better and more modern solution would be to use a ko.computed property and the arrayFilter helper method to do the filtering:
assets: ko.computed(function () {
if (!viewModel.query())
return assets;
return ko.utils.arrayFilter(assets, function(item) {
return item.name.toLowerCase().indexOf(viewModel.query().toLowerCase()) >= 0;
});
}, null, {deferEvaluation: true})
Demo JSFiddle.
There are two tables using the same source. These tables are using source binding of kendo templates. At present the source for both these tables are employees. Both these tables are displaying the same data.
Now, we need to modify it to show only check-box selected records in the result table. Also, when user clicks on the delete button on the result table, the check-box should be un-selected in the section table.
What modification do we need to do to make it work in MVVM?
Head
<head>
<title>MVVM Test</title>
<script type="text/javascript" src="lib/kendo/js/jquery.min.js"></script>
<script type="text/javascript" src="lib/kendo/js/kendo.web.min.js"></script>
<!----Kendo Templates-->
<script id="row-template" type="text/x-kendo-template">
<tr>
<td data-bind="text: name"></td>
<td data-bind="text: age"></td>
<td><button type="button" data-bind="click: deleteEmployee">Delete</button></td>
</tr>
</script>
<script id="selection-table-template" type="text/x-kendo-template">
<tr>
<td data-bind="text: name"></td>
<td data-bind="text: age"></td>
<td>
<input type="checkbox" name="selection" value="a">
</td>
</tr>
</script>
<!--MVVM Wiring using Kendo Binding-->
<script type="text/javascript">
$(document).ready(function () {
kendo.bind($("body"), viewModel);
});
</script>
</head>
MVVM
<script type="text/javascript">
var viewModel = kendo.observable({
// model definition
employees: [
{ name: "Lijo", age: "28" },
{ name: "Binu", age: "33" },
{ name: "Kiran", age: "29" }
],
personName: "",
personAge: "",
//Note: Business functions does not access any DOM elements using jquery.
//They are referring only the objects in the view model.
//business functions (uses "this" keyword - e.g. this.get("employees"))
addEmployee: function () {
this.get("employees").push({
name: this.get("personName"),
age: this.get("personAge")
});
this.set("personName", "");
this.set("personAge", "");
},
deleteEmployee: function (e) {
//person object is created using "e"
var person = e.data;
var employees = this.get("employees");
var index = employees.indexOf(person);
employees.splice(index, 1);
}
});
</script>
Body
<body>
<table id="selectionTable">
<thead>
<tr>
<th>
Name
</th>
<th>
Age
</th>
</tr>
</thead>
<tbody data-template="selection-table-template" data-bind="source: employees">
</tbody>
</table>
<br />
<hr />
<table id="resultTable">
<thead>
<tr>
<th>
Name
</th>
<th>
Age
</th>
</tr>
</thead>
<!--The data-template attribute tells Kendo UI that the employees objects should be formatted using a Kendo UI template. -->
<tbody data-template="row-template" data-bind="source: employees">
</tbody>
</table>
</body>
REFERENCES
set method - ObservableObject - Kedo API Reference
set method - kendo Model - Kedo API Reference
Filtering source in a Kendo Template
Kendo-UI grid Set Value in grid with Javascript
First things first.
If you remove the object from the viewModel when you delete it, it will be removed from your source table as well. You would need two arrays to handle this if you wanted it to be the way you describe. But based on the first part of your question, I thought I would post a solution.
HTML
<script id="row-template" type="text/x-kendo-template">
<tr data-bind="visible: isChecked">
<td data-bind="text: name"></td>
<td data-bind="text: age"></td>
<td>
<button type="button" data-bind="click: deleteEmployee">Delete</button>
</td>
</tr>
</script>
<script id="selection-table-template" type="text/x-kendo-template">
<tr>
<td data-bind="text: name"></td>
<td data-bind="text: age"></td>
<td>
<input type="checkbox" name="selection" data-bind="checked: isChecked"/>
</td>
</tr>
</script>
<table id="selectionTable">
<thead>
<tr>
<th>Name</th>
<th>Age</th>
</tr>
</thead>
<tbody data-template="selection-table-template" data-bind="source: employees"/>
</table>
<br />
<hr />
<table id="resultTable">
<thead>
<tr>
<th>Name</th>
<th>Age</th>
</tr>
</thead>
<tbody data-template="row-template" data-bind="source: employees"/>
</table>
JAVASCRIPT
var viewModel = kendo.observable({
employees: [
{ name: "Lijo", age: "28", isChecked: true },
{ name: "Binu", age: "33", isChecked: true },
{ name: "Kiran", age: "29", isChecked: true }
],
personName: "",
personAge: "",
addEmployee: function () {
this.get("employees").push({
name: this.get("personName"),
age: this.get("personAge")
});
this.set("personName", "");
this.set("personAge", "");
},
deleteEmployee: function (e) {
var person = e.data;
var employees = this.get("employees");
var index = employees.indexOf(person);
var employee = employees[index];
//set
employee.set('isChecked', false);
}
});
$(document).ready(function () {
kendo.bind($("body"), viewModel);
});
JSFiddle
Fiddle
Summary
Use data-bind="visible: isChecked" in "row-template" to show only selected records in the bottom table.
Make template for checkbox as
<input type="checkbox" name="selection" data-bind="checked: isChecked"/>
In the delete function, use following
employee.set('isChecked', false);
Use a dictionary to store [employee, boolean] to store employee and the checkbox state, and bind the dictionary to the view
Check this