Knockout.js multiple models editable list
Lets assume that we want to create editable list of something, this is really easy with knockout, but what if we need to be able to host multiple different models in our list?
Here is demo: http://mac-blog.org.ua/examples/knockout/editablelist.html
And here is commented code:
<!DOCTYPE html>
<html>
<head>
<title>editable list</title>
<meta charset="utf-8" />
<link href="http://ajax.googleapis.com/ajax/libs/jqueryui/1/themes/base/jquery-ui.css" rel="stylesheet" />
<link href="http://netdna.bootstrapcdn.com/twitter-bootstrap/latest/css/bootstrap-combined.min.css" rel="stylesheet" />
</head>
<body>
<div class="container-fluid">
<table class="table">
<thead>
<tr>
<th>id</th>
<th>name</th>
<th>answer</th>
<th>importance</th>
<th></th>
</tr>
</thead>
<tbody data-bind="foreach: items">
<tr>
<td data-bind="text: id"></td>
<td data-bind="text: name"></td>
<td data-bind="text: answer"></td>
<td data-bind="text: importance"></td>
<td>
<a href="#" class="btn btn-mini" data-bind="click: function(){$parent.editItem($data);}">Edit</a>
<a href="#" class="btn btn-mini" data-bind="click: function(){$parent.items.remove($data);}">Delete</a>
</td>
</tr>
</tbody>
</table>
<div class="row-fluid">
<div class="span6">
<a href="#" class="btn btn-block" data-bind="click: addLanguage">Add Language Question</a>
</div>
<div class="span6">
<a href="#" class="btn btn-block" data-bind="click: addExperience">Add Experience Question</a>
</div>
</div>
<div id="experience" class="form-horizontal" style="display:none" title="Experience question">
<div class="control-group">
<label class="control-label">id</label>
<div class="controls">
<input class="input-block-level" type="number" data-bind="value: experience.id" disabled />
</div>
</div>
<div class="control-group">
<label class="control-label">name</label>
<div class="controls">
<input class="input-block-level" type="text" data-bind="value: experience.name" />
</div>
</div>
<div class="control-group">
<label class="control-label">answer</label>
<div class="controls">
<select class="input-block-level" data-bind="options: experienceAnswerOptions, value: experience.answer"></select>
</div>
</div>
<div class="control-group">
<label class="control-label">importance</label>
<div class="controls">
<select class="input-block-level" data-bind="options: importanceOptions, value: experience.importance"></select>
</div>
</div>
</div>
<div id="language" class="form-horizontal" style="display:none" title="Language question">
<div class="control-group">
<label class="control-label">id</label>
<div class="controls">
<input class="input-block-level" type="number" data-bind="value: language.id" disabled />
</div>
</div>
<div class="control-group">
<label class="control-label">name</label>
<div class="controls">
<input class="input-block-level" type="text" data-bind="value: language.name" disabled />
</div>
</div>
<div class="control-group">
<label class="control-label">language</label>
<div class="controls">
<select class="input-block-level" data-bind="options: languageOptions, value: language.language"></select>
</div>
</div>
<div class="control-group">
<label class="control-label">answer</label>
<div class="controls">
<select class="input-block-level" data-bind="options: languageAnswerOptions, value: language.answer"></select>
</div>
</div>
<div class="control-group">
<label class="control-label">importance</label>
<div class="controls">
<select class="input-block-level" data-bind="options: importanceOptions, value: language.importance"></select>
</div>
</div>
</div>
</div>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/jqueryui/1/jquery-ui.min.js"></script>
<script type="text/javascript" src="http://knockoutjs.com/downloads/knockout-2.2.1.js"></script>
<script type="text/javascript">
;(function ($) {
// we are creating own dialog widget to get rid of duplicate code in our model definition (see later)
$.widget('rua.callbackdialog', $.ui.dialog, {
options: {
modal: true,
resizable: false,
width: 400,
callback: function () {
return true
},
saveButtonText: 'Save',
cancelButtonText: 'Cancel'
},
_create: function () {
// before calling parents constructor we are adding dialog buttons with our code
if (!this.options.buttons.length) {
this.options.buttons = []
if (this.options.saveButtonText) {
this.options.buttons.push({
text: this.options.saveButtonText,
click: function () {
// here is main part, "Save" button will call callback and if it will return true - will close the dialog
if ($(this).callbackdialog('option', 'callback')()) {
$(this).callbackdialog('close')
}
}
})
}
if (this.options.cancelButtonText) {
this.options.buttons.push({
text: this.options.cancelButtonText,
click: function () {
$(this).callbackdialog('close')
}
})
}
}
$.ui.dialog.prototype._create.call(this)
},
destroy: function () {
$.ui.dialog.prototype.destroy.call(this)
},
_setOption: function () {
$.ui.dialog.prototype._setOption.apply(this, arguments)
}
})
})(jQuery)
</script>
<script type="text/javascript">
// Base model, that will be inherited by other models
// it need for setting values via options argument
function QuestionBaseModel(options) {
this.init = function (options) {
var val
if (options) {
for (key in options) {
// here we are checking if concrete options attribute is function (observable)
val = typeof options[key] === 'function' ? options[key]() : options[key]
// try catch need here because there are cases when this code will try to change computed value, knockout will throw an exception in that case
try {
if (typeof this[key] === 'function') this[key](val)
else this[key] = val
} catch (err) {}
}
}
}
this.init.apply(this, arguments)
}
// out first model
function QuestionExperienceModel(options) {
this.id = ko.observable()
this.name = ko.observable()
this.importance = ko.observable()
this.answer = ko.observable()
this.init.apply(this, arguments) // apply given options
}
QuestionExperienceModel.prototype = new QuestionBaseModel() // inherit it from base model
// our second model
function QuestionLanguageModel(options) {
this.id = ko.observable()
this.language = ko.observable()
this.name = ko.computed(function () {
return 'Your knowledge of ' + this.language()
}, this)
this.importance = ko.observable()
this.answer = ko.observable()
this.init.apply(this, arguments) // apply given options
}
QuestionLanguageModel.prototype = new QuestionBaseModel() // inherit it from base model
// main model which will contain observable array of QuestionExperienceModel and QuestionLanguageModel models
function List() {
var self = this
self.importanceOptions = ['Non important', 'Semi important', 'Important']
self.languageOptions = ['English', 'Ukrainian']
self.experienceAnswerOptions = ['1+ year', '2+ years', '5+ years']
self.languageAnswerOptions = ['little', 'good', 'carrier']
self.items = ko.observableArray([
new QuestionExperienceModel({ id: 1, name: 'Your experience in JS', importance: 'Important', answer: '2+ years' }),
new QuestionLanguageModel({ id: 2, importance: 'Semi important', answer: 'good', language: 'Ukrainian' })
])
// dummy items that is used by editor dialogs
self.experience = new QuestionExperienceModel()
self.language = new QuestionLanguageModel()
// on adding we are just creating new instance of desired model with default arguments and editing it
// notice second boolean argument indicating that this is new item that must be added to items array later
self.addLanguage = function () {
var item = new QuestionLanguageModel({ id: 0, importance: 'Important', answer: 'good', language: 'English' })
self.editItem(item, true)
}
// on adding we are just creating new instance of desired model with default arguments and editing it
// notice second boolean argument indicating that this is new item that must be added to items array later
self.addExperience = function () {
var item = new QuestionExperienceModel({ id: 0, name: 'Your experience in ...', importance: 'Important', answer: '2+ years' })
self.editItem(item, true)
}
// we are calling this method in both cases on creating and editing items
// second boolean argument indicating that this is new item that must be added to items array later
self.editItem = function (item, isNew) {
if (item instanceof QuestionLanguageModel) self.editLanguage(item, isNew)
else self.editExperience(item, isNew)
}
self.editExperience = function (item, isNew) {
// fill dummy experience with given item
self.experience.init(item)
// show edit dialog
$('#experience').callbackdialog({
callback: function () {
//TODO: validate here and return false if need
// if all ok save data back to item from dummy experience
item.init(self.experience)
// if this is a new item - add it to items array
if (isNew) self.items.push(item)
return true
}
})
}
self.editLanguage = function (item, isNew) {
// fill dummy language with given item
self.language.init(item)
// show edit dialog
$('#language').callbackdialog({
callback: function () {
//TODO: validate here and return false if need
// if all ok save data back to item from dummy language
item.init(self.language)
// if this is a new item - add it to items array
if (isNew) self.items.push(item)
return true
}
})
}
}
ko.applyBindings(new List())
</script>
</body>
</html>