Last active
August 31, 2021 09:19
-
-
Save bbrt3/1a466a84b7753c409a33d43c3d8e33aa to your computer and use it in GitHub Desktop.
KnockoutJS
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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"> </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()); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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> | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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