Skip to content

Instantly share code, notes, and snippets.

@bbrt3
Last active August 31, 2021 09:19
Show Gist options
  • Save bbrt3/1a466a84b7753c409a33d43c3d8e33aa to your computer and use it in GitHub Desktop.
Save bbrt3/1a466a84b7753c409a33d43c3d8e33aa to your computer and use it in GitHub Desktop.
KnockoutJS
// View
<h3>Tasks</h3>
<form data-bind="submit: addTask">
Add task: <input data-bind="value: newTaskText" placeholder="What needs to be done?" />
<button type="submit">Add</button>
</form>
<ul data-bind="foreach: tasks, visible: tasks().length > 0">
<li>
<input type="checkbox" data-bind="checked: isDone" />
<input data-bind="value: title, disable: isDone" />
<a href="#" data-bind="click: $parent.removeTask">Delete</a>
</li>
</ul>
You have <b data-bind="text: incompleteTasks().length">&nbsp;</b> incomplete task(s)
<span data-bind="visible: incompleteTasks().length == 0"> - it's beer time!</span>
// sending data to backend
<form action="/tasks/saveform" method="post">
<input type="hidden" name="tasks" data-bind="value: ko.toJSON(tasks)" />
<button type="submit">Save</button>
</form>
// alternative version
// useful for tracking complete state
<button data-bind="click: save">Save</button>
// ViewModel
function Task(data) {
this.title = ko.observable(data.title);
this.isDone = ko.observable(data.isDone);
}
function TaskListViewModel() {
// Data
var self = this;
self.tasks = ko.observableArray([]);
self.newTaskText = ko.observable();
self.incompleteTasks = ko.computed(function() {
return ko.utils.arrayFilter(self.tasks(), function(task) { return !task.isDone() });
});
// Operations
self.addTask = function() {
self.tasks.push(new Task({ title: this.newTaskText() }));
self.newTaskText("");
};
self.removeTask = function(task) { self.tasks.remove(task) };
// Load initial state from server, convert it to Task instances, then populate self.tasks
// getting data from backend
$.getJSON("/tasks", function(allData) {
var mappedTasks = $.map(allData, function(item) { return new Task(item) });
self.tasks(mappedTasks);
});
// alternative
self.save = function() {
$.ajax("/tasks", {
data: ko.toJSON({ tasks: self.tasks }),
type: "post", contentType: "application/json",
success: function(result) { alert(result) }
});
};
}
ko.applyBindings(new TaskListViewModel());
// Computed properties are observables and they are computed based on the values of other obeservables
// OBSERVABLE + OBSERVABLE = COMPUTED PROPERTY
function AppViewModel() {
this.firstName = ko.observable("Bert");
this.lastName = ko.observable("Bertington");
// using callback function to create full name from other observables
this.fullName = ko.computed(function() {
return this.firstName() + " " + this.lastName();
}, this);
}
// Activates knockout.js
ko.applyBindings(new AppViewModel());
// displaying full name
// Things stay in sync because of automatic dependency tracking
// <strong> DEPENDS ON fullName DEPENDS ON firstName+lastName (which can be altered by editing textboxes)
<p><strong>Full name: <strong data-bind="text: fullName"></strong></p>
// bindings are the intermendiaries; they perform updates in both directions
// bindings notice viewmodel changes and correspondingly update the view's DOM
// bingins catch DOM events and correspondingly update viewmodel properties
// custom bindings are a also a good idea if you want to use some 3rd party components
// Reusable bindings - ideally kept in a separate file
// binding handlers live outside of our ViewModel!!
// animated text
ko.bindingHandlers.fadeVisible = {
init: function(element, valueAccessor) {
// Start visible/invisible according to initial value
var shouldDisplay = valueAccessor();
$(element).toggle(shouldDisplay);
},
update: function(element, valueAccessor) {
// On update, fade in/out
var shouldDisplay = valueAccessor();
shouldDisplay ? $(element).fadeIn() : $(element).fadeOut();
}
};
// using 3rd party component
ko.bindingHandlers.jqButton = {
init: function(element) {
$(element).button(); // Turns the element into a jQuery UI button
},
update: function(element, valueAccessor) {
var currentValue = valueAccessor();
// Here we just update the "disabled" state, but you could update other properties too
$(element).button("option", "disabled", currentValue.enable === false);
}
};
// View
<h3 data-bind="fadeVisible: pointsUsed() > pointsBudget">You've used too many points! Please remove some.</h3>
// starRating system example
// ViewModel
// ----------------------------------------------------------------------------
// Reusable bindings - ideally kept in a separate file
ko.bindingHandlers.fadeVisible = {
init: function(element, valueAccessor) {
// Start visible/invisible according to initial value
var shouldDisplay = valueAccessor();
$(element).toggle(shouldDisplay);
},
update: function(element, valueAccessor) {
// On update, fade in/out
var shouldDisplay = valueAccessor();
shouldDisplay ? $(element).fadeIn() : $(element).fadeOut();
}
};
ko.bindingHandlers.jqButton = {
init: function(element) {
$(element).button(); // Turns the element into a jQuery UI button
},
update: function(element, valueAccessor) {
var currentValue = valueAccessor();
// Here we just update the "disabled" state, but you could update other properties too
$(element).button("option", "disabled", currentValue.enable === false);
}
};
ko.bindingHandlers.starRating = {
init: function(element, valueAccessor) {
$(element).addClass("starRating");
for (var i = 0; i < 5; i++)
$("<span>").appendTo(element);
},
update: function(element, valueAccessor) {
var observable = valueAccessor();
$("span", element).each(function(index) {
$(this).toggleClass("chosen", index < observable());
});
// Handle mouse events on the stars
$("span", element).each(function(index) {
$(this).hover(
function() { $(this).prevAll().add(this).addClass("hoverChosen") },
function() { $(this).prevAll().add(this).removeClass("hoverChosen") }
).click(function() {
var observable = valueAccessor(); // Get the associated observable
observable(index+1); // Write the new rating to it
});
});
}
}
// ----------------------------------------------------------------------------
// Page viewmodel
function Answer(text) { this.answerText = text; this.points = ko.observable(1); }
function SurveyViewModel(question, pointsBudget, answers) {
this.question = question;
this.pointsBudget = pointsBudget;
this.answers = $.map(answers, function(text) { return new Answer(text) });
this.save = function() { alert('To do') };
this.pointsUsed = ko.computed(function() {
var total = 0;
for (var i = 0; i < this.answers.length; i++)
total += this.answers[i].points();
return total;
}, this);
}
ko.applyBindings(new SurveyViewModel("Which factors affect your technology choices?", 10, [
"Functionality, compatibility, pricing - all that boring stuff",
"How often it is mentioned on Hacker News",
"Number of gradients/dropshadows on project homepage",
"Totally believable testimonials on project homepage"
]));
// View
<h3 data-bind="text: question"></h3>
<p>Please distribute <b data-bind="text: pointsBudget"></b> points between the following options.</p>
<table>
<thead><tr><th>Option</th><th>Importance</th></tr></thead>
<tbody data-bind="foreach: answers">
<tr>
<td data-bind="text: answerText"></td>
<td><select data-bind="options: [1,2,3,4,5], value: points"></select></td>
<td data-bind="starRating: points"></td>
</tr>
</tbody>
</table>
<h3 data-bind="fadeVisible: pointsUsed() > pointsBudget">You've used too many points! Please remove some.</h3>
<p>You've got <b data-bind="text: pointsBudget - pointsUsed()"></b> points left to use.</p>
<button data-bind="jqButton: { enable: pointsUsed() <= pointsBudget }, click: save">Finished</button>
// This is a simple *viewmodel* - JavaScript that defines the data and behavior of your UI
function AppViewModel() {
// regular data binding for strings
this.firstName = "Bert";
// observable is a property that will automatically inform us whenever their value changes
this.lastName = ko.observable("Bertington");
}
// Activates knockout.js and creating viewModel
ko.applyBindings(new AppViewModel());
<p>First name: <strong data-bind="text: firstName"></strong></p>
<p>Last name: <strong data-bind="text: lastName"></strong></p>
// value will set input's value property
<p>First name: <input data-bind="value: firstName" /></p>
// wil also track it as it uses observable here!
<p>Last name: <input data-bind="value: lastName" /></p>
// ViewModel
// Class to represent a row in the seat reservations grid
// $root prefix causes knockout to look for a specified handler on our top=level viewmodel
// instead of current instance that is being bounded
function SeatReservation(name, initialMeal) {
var self = this;
self.name = name;
self.meal = ko.observable(initialMeal);
// adding new computed property
self.formattedPrice = ko.computed(function() {
var price = self.meal().price;
return price ? "$" + price.toFixed(2) : "None";
});
}
// Overall viewmodel for this screen, along with initial state
function ReservationsViewModel() {
var self = this;
// Non-editable catalog data - would come from the server
// observable array of meals
self.availableMeals = [
{ mealName: "Standard (sandwich)", price: 0 },
{ mealName: "Premium (lobster)", price: 34.95 },
{ mealName: "Ultimate (whole zebra)", price: 290 }
];
// Editable data
self.seats = ko.observableArray([
new SeatReservation("Steve", self.availableMeals[0]),
new SeatReservation("Bert", self.availableMeals[1])
]);
self.addSeat = function() {
self.seats.push(new SeatReservation("", self.availableMeals[2]));
}
}
ko.applyBindings(new ReservationsViewModel());
// View
<h2>Your seat reservations</h2>
<table>
<thead><tr>
<th>Passenger name</th><th>Meal</th><th>Surcharge</th><th></th>
</tr></thead>
<tbody data-bind="foreach: seats">
<tr>
<td data-bind="value: name"></td>
// options specify where we get data from, in our case its observable array called availableMeals
// value specifies item name
// optionsText specifies property which will be displayed
<td><select data-bind="options: $root.availableMeals, value: meal, optionsText: 'mealName'"></select></td>
// <td data-bind="text: meal().price"></td>
// displaying formatted price computed property
<td data-bind="text: formattedPrice"></td>
</tr>
</tbody>
</table>
<button data-bind="click: addSeat">Reserve another seat</button>
// ViewModel
function AppViewModel() {
this.lastName = ko.observable("Bertington");
this.capitalizeLastName = function() {
var currentVal = this.lastName(); // Read the current value
this.lastName(currentVal.toUpperCase()); // Write back a modified value
};
}
ko.applyBindings(new AppViewModel());
// View
<p>Last name: <strong data-bind="text: lastName"></strong></p>
<button data-bind="click: capitalizeLastName">Go caps</button>
// this example uses sammy.js for navigation
// ViewModel
function WebmailViewModel() {
// Data
var self = this;
self.folders = ['Inbox', 'Archive', 'Sent', 'Spam'];
self.chosenFolderId = ko.observable();
self.chosenFolderData = ko.observable();
self.chosenMailData = ko.observable();
// Behaviours
self.goToFolder = function(folder) {
location.hash = folder
};
self.goToMail = function(mail) {
location.hash = mail.folder + '/' + mail.id
};
// Show inbox by default
self.goToFolder('Inbox');
Sammy(function() {
this.get('#:folder', function() {
self.chosenFolderId(this.params.folder);
self.chosenMailData(null);
$.get("/mail", { folder: this.params.folder }, self.chosenFolderData);
});
this.get('#:folder/:mailId', function() {
self.chosenFolderId(this.params.folder);
self.chosenFolderData(null);
$.get("/mail", { mailId: this.params.mailId }, self.chosenMailData);
this.get('', function() { this.app.runRoute('get', '#Inbox') });
});
}).run();
};
ko.applyBindings(new WebmailViewModel());
// View
<script src="/scripts/lib/sammy.js" type="text/javascript"></script>
<!-- Folders -->
<ul class="folders" data-bind="foreach: folders">
<li data-bind="text: $data,
css: { selected: $data == $root.chosenFolderId() },
click: $root.goToFolder"></li>
</ul>
<!-- Mails grid -->
<table class="mails" data-bind="with: chosenFolderData">
<thead><tr><th>From</th><th>To</th><th>Subject</th><th>Date</th></tr></thead>
<tbody data-bind="foreach: mails">
<tr data-bind="click: $root.goToMail">
<td data-bind="text: from"></td>
<td data-bind="text: to"></td>
<td data-bind="text: subject"></td>
<td data-bind="text: date"></td>
</tr>
</tbody>
</table>
<!-- Chosen mail -->
<div class="viewMail" data-bind="with: chosenMailData">
<div class="mailInfo">
<h1 data-bind="text: subject"></h1>
<p><label>From</label>: <span data-bind="text: from"></span></p>
<p><label>To</label>: <span data-bind="text: to"></span></p>
<p><label>Date</label>: <span data-bind="text: date"></span></p>
</div>
<p class="message" data-bind="html: messageContent" />
</div>
// When we want to generate repeating blocks of UI elements we can use observable arrays and foreach binding.
// foreach is part of a family of control flow bindings which include:
// foreach, if, ifnot, with
// These make it possible to construct and kind of iterative or nested UI based on your dynamic viewmodel.
// ViewModel
// Class to represent a row in the seat reservations grid
function SeatReservation(name, initialMeal) {
var self = this;
self.name = name;
self.meal = ko.observable(initialMeal);
}
// Overall viewmodel for this screen, along with initial state
function ReservationsViewModel() {
var self = this;
// Non-editable catalog data - would come from the server
self.availableMeals = [
{ mealName: "Standard (sandwich)", price: 0 },
{ mealName: "Premium (lobster)", price: 34.95 },
{ mealName: "Ultimate (whole zebra)", price: 290 }
];
// Editable data
self.seats = ko.observableArray([
new SeatReservation("Steve", self.availableMeals[0]),
new SeatReservation("Bert", self.availableMeals[1])
]);
// function to add another entry to observable array
// UI will be updated automatically!
// knockout doesn't regenerate entire UI, but only elements that have changed
// which it keeps track of
self.addSeat = function() {
self.seats.push(new SeatReservation("", self.availableMeals[2]));
}
}
ko.applyBindings(new ReservationsViewModel());
// View
<h2>Your seat reservations</h2>
<table>
<thead><tr>
<th>Passenger name</th><th>Meal</th><th>Surcharge</th><th></th>
</tr></thead>
<tbody data-bind="foreach: seats">
<tr>
<td data-bind="text: name"></td>
// because meal is a property of observable we need to invoke it as a function!!!
// so meal().price not meal.price
<td data-bind="text: meal().mealName"></td>
<td data-bind="text: meal().price"></td>
</tr>
</tbody>
</table>
<button data-bind="click: addSeat">Reserve another seat</button>
// the visible binding makes an element visible or invisible as your data changes
// it internaly modifies css display style
// we only display information if it's greater than zero for example (*ngIf)
// ViewModel
self.totalSurcharge = ko.computed(function() {
var total = 0;
for (var i = 0; i < self.seats().length; i++)
total += self.seats()[i].meal().price;
return total;
});
// View
<h3 data-bind="visible: totalSurcharge() > 0">
Total surcharge: $<span data-bind="text: totalSurcharge().toFixed(2)"></span>
</h3>
// Here we display property of array
<h2>Your seat reservations: <span data-bind="text: seats().length"></span></h2>
// here we add limit for button, when we have 5 seats it will get disabled and we won't be able
// to ad new ones!!
<button data-bind="click: addSeat, enable: seats().length < 5">Reserve another seat</button>
// with creates binding context that will be used when binding any elements inside it.
// here everything inside <table> will be bound to chosenFolderData,
// so it's not necessary to use chosenFolderData. as a prefix before mails
<table class="mails" data-bind="with: chosenFolderData">
<thead><tr><th>From</th><th>To</th><th>Subject</th><th>Date</th></tr></thead>
<tbody data-bind="foreach: mails">
<tr>
<td data-bind="text: from"></td>
<td data-bind="text: to"></td>
<td data-bind="text: subject"></td>
<td data-bind="text: date"></td>
</tr>
</tbody>
</table>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment