knockout.js - deferred databinding for modal? - javascript

I am using knockout.js to display a list of employees. I have a single hidden modal markup on the page. When the "details" button for a single employees is clicked, I want to data-bind that employee to the modal popup. I am using the ko.applyBindings(employee, element) but the problem is when the page loads, it is expecting the modal to start off as bound to something.
So I'm wondering, is there a trick/strategy to do a late/deferred databinding? I looked into virtual bindings but the documentation was not helpful enough.
Thanks!

I would like to propose a different way to work with modals in MVVVM. In MVVM, the ViewModel is data for the View, and the View is responsible for the UI. If we examine this proposal:
this.detailedEmployee = ko.observable({}),
var self = this;
this.showDetails = function(employee){
self.detailedEmployee(employee);
$("#dialog").dialog("show"); //or however your dialog works
}
I strongly agree with this.detailedEmployee = ko.observable({}), but I am in strong disagreement with this line: $("#dialog").dialog("show");. This code is placed in the ViewModel and shows the modal window, wherein fact it is View's responsibility, so we screw-up the MVVM approach. I would say this piece of code will solve your current task but it could cause lots of problems in future.
When closing the popup, you should set detailedEmployee to undefined to have your main ViewModel in a consistent state.
When closing the popup, you might want to have validation and the possibility to discard the close operation when you want to use another modal's component in the application
As for me, these points are very critical, so I would like to propose a different way. If we "forget" that you need to display data in popup, binding with could solve your issue.
this.detailedEmployee = ko.observable(undefined);
var self = this;
this.showDetails = function(employee){
self.detailedEmployee(employee);
}
<div data-bind="with: detailedEmployee">
Data to show
</div>
As you can see, your ViewModel don't know anything about how data should be shown. It knows only about data that should be shown. The with binding will display content only when detailedEmployee is defined. Next, we should find a binding similar to with but one that will display content in the popup. Let's give it the name modal. Its code is like this:
ko.bindingHandlers['modal'] = {
init: function(element) {
$(element).modal('init');
return ko.bindingHandlers['with'].init.apply(this, arguments);
},
update: function(element, valueAccessor) {
var value = ko.utils.unwrapObservable(valueAccessor());
var returnValue = ko.bindingHandlers['with'].update.apply(this, arguments);
if (value) {
$(element).modal('show');
} else {
$(element).modal('hide');
}
return returnValue;
}
};
As you can see, it uses the with plugin internally, and shows or hide a popup depending on value passed to binding. If it is defined - 'show'. If not - 'hide'. Its usage will be the as with:
<div data-bind="modal: detailedEmployee">
Data to show
</div>
The only thing you need to do is to use your favorite modals plugin. I prepared an example with the Twitter Bootstrap popup component: http://jsfiddle.net/euvNr/embedded/result/
In this example, custom binding is a bit more powerful; you could subscribe the onBeforeClose event and cancel this event if needed. Hope this helps.

The JSFiddle linked to in the answer provided by #Romanych didn't seem to work anymore.
So, I built my own example (based upon his original fiddle) with full CRUD support and basic validation using Bootstrap 3 and the Bootstrap Modal library: https://jsfiddle.net/BitWiseGuy/4u5egybp/
Custom Binding Handlers
ko.bindingHandlers['modal'] = {
init: function(element, valueAccessor, allBindingsAccessor) {
var allBindings = allBindingsAccessor();
var $element = $(element);
$element.addClass('hide modal');
if (allBindings.modalOptions && allBindings.modalOptions.beforeClose) {
$element.on('hide', function() {
var value = ko.utils.unwrapObservable(valueAccessor());
return allBindings.modalOptions.beforeClose(value);
});
}
},
update: function(element, valueAccessor) {
var value = ko.utils.unwrapObservable(valueAccessor());
if (value) {
$(element).removeClass('hide').modal('show');
} else {
$(element).modal('hide');
}
}
};
Example Usage
The View
<div data-bind="modal: UserBeingEdited" class="fade" role="dialog" tabindex="-1">
<form data-bind="submit: $root.SaveUser">
<div class="modal-header">
<a class="close" data-dismiss="modal">×</a>
<h3>User Details</h3>
</div>
<div class="modal-body">
<div class="form-group">
<label for="NameInput">Name</label>
<input type="text" class="form-control" id="NameInput" placeholder="User's name"
data-bind="value: UserBeingEdited() && UserBeingEdited().Name, valueUpdate: 'afterkeydown'">
</div>
<div class="form-group">
<label for="AgeInput">Age</label>
<input type="text" class="form-control" id="AgeInput" placeholder="User's age"
data-bind="value: UserBeingEdited() && UserBeingEdited().Age, valueUpdate: 'afterkeydown'">
</div>
<!-- ko if: ValidationErrors() && ValidationErrors().length > 0 -->
<div class="alert alert-danger" style="margin: 20px 0 0">
Please correct the following errors:
<ul data-bind="foreach: { data: ValidationErrors, as: 'errorMessage' }">
<li data-bind="text: errorMessage"></li>
</ul>
</div>
<!-- /ko -->
</div>
<div class="modal-footer">
<button type="button" data-dismiss="modal" class="btn btn-default">Cancel</button>
<button type="submit" class="btn btn-primary">Save Changes</button>
</div>
</form>
</div>
The ViewModel
/* ViewModel for the individual records in our collection. */
var User = function(name, age) {
var self = this;
self.Name = ko.observable(ko.utils.unwrapObservable(name));
self.Age = ko.observable(ko.utils.unwrapObservable(age));
}
/* The page's main ViewModel. */
var ViewModel = function() {
var self = this;
self.Users = ko.observableArray();
self.ValidationErrors = ko.observableArray([]);
// Logic to ensure that user being edited is in a valid state
self.ValidateUser = function(user) {
if (!user) {
return false;
}
var currentUser = ko.utils.unwrapObservable(user);
var currentName = ko.utils.unwrapObservable(currentUser.Name);
var currentAge = ko.utils.unwrapObservable(currentUser.Age);
self.ValidationErrors.removeAll(); // Clear out any previous errors
if (!currentName)
self.ValidationErrors.push("The user's name is required.");
if (!currentAge) {
self.ValidationErrors.push("Please enter the user's age.");
} else { // Just some arbitrary checks here...
if (Number(currentAge) == currentAge && currentAge % 1 === 0) { // is a whole number
if (currentAge < 2) {
self.ValidationErrors.push("The user's age must be 2 or greater.");
} else if (currentAge > 99) {
self.ValidationErrors.push("The user's age must be 99 or less.");
}
} else {
self.ValidationErrors.push("Please enter a valid whole number for the user's age.");
}
}
return self.ValidationErrors().length <= 0;
};
// The instance of the user currently being edited.
self.UserBeingEdited = ko.observable();
// Used to keep a reference back to the original user record being edited
self.OriginalUserInstance = ko.observable();
self.AddNewUser = function() {
// Load up a new user instance to be edited
self.UserBeingEdited(new User());
self.OriginalUserInstance(undefined);
};
self.EditUser = function(user) {
// Keep a copy of the original instance so we don't modify it's values in the editor
self.OriginalUserInstance(user);
// Copy the user data into a new instance for editing
self.UserBeingEdited(new User(user.Name, user.Age));
};
// Save the changes back to the original instance in the collection.
self.SaveUser = function() {
var updatedUser = ko.utils.unwrapObservable(self.UserBeingEdited);
if (!self.ValidateUser(updatedUser)) {
// Don't allow users to save users that aren't valid
return false;
}
var userName = ko.utils.unwrapObservable(updatedUser.Name);
var userAge = ko.utils.unwrapObservable(updatedUser.Age);
if (self.OriginalUserInstance() === undefined) {
// Adding a new user
self.Users.push(new User(userName, userAge));
} else {
// Updating an existing user
self.OriginalUserInstance().Name(userName);
self.OriginalUserInstance().Age(userAge);
}
// Clear out any reference to a user being edited
self.UserBeingEdited(undefined);
self.OriginalUserInstance(undefined);
}
// Remove the selected user from the collection
self.DeleteUser = function(user) {
if (!user) {
return falase;
}
var userName = ko.utils.unwrapObservable(ko.utils.unwrapObservable(user).Name);
// We could use another modal here to display a prettier dialog, but for the
// sake of simplicity, we're just using the browser's built-in functionality.
if (confirm('Are you sure that you want to delete ' + userName + '?')) {
// Find the index of the current user and remove them from the array
var index = self.Users.indexOf(user);
if (index > -1) {
self.Users.splice(index, 1);
}
}
};
}
Initializing Knockout with the View and the ViewModel
var viewModel = new ViewModel();
// Populate the ViewModel with some dummy data
for (var i = 1; i <= 10; i++) {
var letter = String.fromCharCode(i + 64);
var userName = 'User ' + letter;
var userAge = i * 2;
viewModel.Users.push(new User(userName, userAge));
}
// Let Knockout do its magic!
ko.applyBindings(viewModel);

I would create another observable that wraps the employee.
this.detailedEmployee = ko.observable({}),
var self = this;
this.showDetails = function(employee){
self.detailedEmployee(employee);
$("#dialog").dialog("show"); //or however your dialog works
}
Attach the click to showDetails. Then you can just call applyBindings on page load.

Related

Hide button everywhere besides specific URL in knockout

I have the following button:
<button class="btn actionButtonIcon" id="DashboardEdit" data-bind="click: changeButtonText">
<figure>
<img src="../../../Images/NotesPink.png" />
<figcaption data-bind="text: $data.ProcurementbuttonText() ? 'Save': 'Edit'"></figcaption>
</figure>
</button>
I want to only show it in this specific url
http://localhost:5595/#scorecard/ec5aa8ed-2798-4e71-b13d-f3e525994538/dashboard/PrefDashBoard
Bearing in mind that ec5aa8ed-2798-4e71-b13d-f3e525994538 is an id, thats always changing but i want it to show with all ids as well for example the button should show here as well
http://localhost:5595/#scorecard/2356789-234-234d-g3g3-reg456452/dashboard/PrefDashBoard
and i want to hide it where this isnt the url.
I tried the following code but it doesnt seem to work:
<script>
$(document).ready(function(){
if(window.location.pathname.match(/\/dashboard/PrefDashBoard))
{
$(".DashboardEdit").show();
}
else
{
$(".DashboardEdit").hide();
}
});
</script>
Here is the JS of that button:
self.ProcurementbuttonText = ko.observable(false);
self.changeButtonText = function(){
self.ProcurementbuttonText(!self.ProcurementbuttonText())
if (!self.ProcurementbuttonText()){
var data = {
'ScorecardId':ko.observable(localStorage.getItem('scorecardId'))(),
'DashboardConfig':ko.observable(localStorage.getItem('ElementDataWidget'))()
};
PreferentialProcurementDashboardApi.Save(data);
}
}
self.DashboardEdit = ko.computed({
read: function () {
var text = 'Customise your dashboard';
if (!self.EnableScorecardFeatures()) {
text = 'This feature is currently unavailable for this scorecard type';
} else {
if (!self.HasDocumentsRole()) {
text = 'You do not have sufficient rights to access the Supporting Documents view';
}
}
return text;
}
});
i think you can take advantage of the visible binding to show/hide the button based on your criteria
function PageViewModel() {
var self = this;
self.buttonVisible = ko.observable(true);
self.changeButtonText = function() {
self.ProcurementbuttonText(!self.ProcurementbuttonText());
}
self.ProcurementbuttonText = ko.observable(false);
self.buttonText = ko.pureComputed(function() {
return self.ProcurementbuttonText() ? "Save" : "Edit";
});
self.isButtonVisible = ko.computed(function() {
//do some your logic here. just need to return a true or false value;
return self.buttonVisible();
});
self.labelText = ko.pureComputed(function() {
var messageStart = "click to ";
var state = self.buttonVisible() ? 'hide' : 'show';
var messageEnd = " button";
return messageStart + state + messageEnd;
});
}
ko.applyBindings(new PageViewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<button class="btn actionButtonIcon" id="DashboardEdit" data-bind="click: changeButtonText, visible: isButtonVisible, text: buttonText">
Click me.
</button>
<br/>
<br/>
<label><span data-bind="text: labelText " ></span>
<input type="checkbox" data-bind="checked: buttonVisible" />
</label>
If you have Durandal's router plugin installed and configured, you can also use the activeInstruction() observable to get info about the current route. You can then use this in your computed to check if the current fragment matches your page route.
More info here: http://durandaljs.com/documentation/api#class/Router/property/activeInstruction

Ajax search doesn't work the second time (ASP.NET MVC)

I have a problem changing items after searching.
I looked at similar threads but found no solution there :(
It looks like the first time the page loads well - the first time the entire Index.cshtml page is loaded which contains a collection of books in the selected category.
There is a search engine on the page - after searching for "manual" - ajax correctly replaces elements with those containing "manual" in the name.
Then when I enter something into the search engine a second time (for example "exercises") - the content of the page does not change any more.
I tried to debug and I see that new items are correctly downloaded from the database - the condition "if (Request.IsAjaxRequest ())" is true and the items are passed to partial view - there the "foreach" loop goes through them. Unfortunately, after _Partial, nothing happens.
I can't find a mistake - the strangest thing is that the first ajax call works fine - only the second (and subsequent) bad.
CatalogController.cs
public ActionResult Index(string categoryName = null, string searchQuery = null)
{
if (categoryName == null)
categoryName = (db.Categories.Find(1)).Name;
var category = db.Categories.Include("Books").Where(x => x.Name.ToLower() == categoryName).Single();
var books = category.Books.Where(x => (searchQuery == null || x.Title.ToLower().Contains(searchQuery.ToLower()) || x.SubTitle.ToLower().Contains(searchQuery.ToLower()) || x.Level.ToLower().Contains(searchQuery.ToLower())) && !x.Inaccessible);
if (Request.IsAjaxRequest())
return PartialView("_PartialBooksList", books);
else
return View(books);
}
Index.cshtml
<form class="o-search-form" id="search-form" method="get" data-ajax="true" data-ajax-target="#booksList">
<input class="o-search-input" id="search-filter" type="search" name="searchQuery" data-autocomplete-source="#Url.Action("SearchTips")" placeholder="Search" />
<input class="o-search-submit" type="submit" value="" />
</form>
<div class="row" id="booksList">
#Html.Partial("_PartialBooksList")
</div>
#section Scripts
{
<script src="~/Scripts/jquery-3.5.0.js"></script>
<script src="~/Scripts/jquery-ui-1.12.1.js"></script>
<script>
$(function () {
var setupAutoComplete = function () {
var $input = $(this);
var options =
{
source: $input.attr("data-autocomplete-source"),
select: function (event, ui) {
$input = $(this);
$input.val(ui.item.label);
var $form = $input.parents("form:first");
$form.submit();
}
};
$input.autocomplete(options);
};
var ajaxSubmit = function () {
var $form = $(this);
var settings = {
data: $(this).serialize(),
url: $(this).attr("action"),
type: $(this).attr("method")
};
$.ajax(settings).done(function (result) {
var $targetElement = $($form.data("ajax-target"));
var $newContent = $(result);
$($targetElement).replaceWith($newContent);
$newContent.effect("slide");
});
return false;
};
$("#search-filter").each(setupAutoComplete);
$("#search-form").submit(ajaxSubmit);
});
</script>
}
_PartialBooksList
#model IEnumerable<ImpressDev.Models.Book>
#using ImpressDev.Infrastructure
<div class="row">
#foreach (var book in Model)
{
<div class="col-12 col-xl-4">
<a class="o-shop-link" href="#Url.Action("Details", "Catalog", new { bookId = book.BookId })">
<div class="o-shop-item">
<img class="o-shop-img" src="#Url.BookPhotoSourcePath(book.PhotoSource)" />
<div class="o-shop-text">
<h2>#book.Title</h2>
<h6>#book.SubTitle - #book.Level - <b>#book.Price zł.</b></h6>
+ Add to cart
</div>
</div>
</a>
</div>
}
</div>
Please help
I am not sure if this is the case, but try to change this code:
$($targetElement).replaceWith($newContent);
To this:
$($targetElement).html($newContent);
I think the problem is the div element with id="booksList" is replaced after first search. So you don't have this element in the second search.
I looked through the code step by step and found a solution to my problem.
In the first search, replace id="booksList"
<div class="row" id="booksList">
#Html.Partial("_PartialBooksList")
</div>
partial view in which there was only without id = booksLists.
In the next search there was no ID in this place and there was nothing to replace.

Implementing a Modal Factory?

I'be been using modals as a means to communicate to users in my apps for some time now via several different front end frameworks. The logic is usually the same, defining the modal's html then rendering it via some click event.
As my applications grow, so do the number of modals I use for a user prompt or confirmation - these modals can have anything from text inputs to forms to dropdowns and so on.
My current method is to write out each separate modal in a single html file and simply call them by their IDs but I feel this is inefficient as there is plenty of duplicate boilerplate code, so I'm wondering the best way would be to create modals dynamically while keeping the code as light andclean as possible?
I've been thinking of something like a "modal factory" where you pass the content of the modal along with the height, width, styling, etc. would this be a good approach?
Thanks for any input!
Well what I do for Forms/HTML Content loaded from the server - is create a div with an ID - PartialViewDialog at the end of my page -(I load Partial Views inside a Dialog)
This one is Bootstrap 3.* based - (HTML structure based on Frontend framework
So the HTML is like this:
<body>
<!-- Other page content -->
<div class="modal fade" id="PartialViewDialog">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
<h4 class="modal-title" data-modal="title"></h4>
</div>
<div class="modal-body" data-modal="content">
</div>
<div class="modal-footer" data-modal="footer">
</div>
</div>
</div>
</div>
</body>
Then in JS, I create a dialog Manager:
var MyApp = MyApp || {};
MyApp.UIHelper = MyApp.UIHelper || {};
MyApp.UIHelper.DialogManager = (function () {
"use strict";
var self = {};
self.divId = null;
self.dialog = null;
self.dialogBody = null;
self.dialogTitle = null;
self.dialogFooter = null;
self.actionUrl = "";
self.modalObject = null;
self.options = {};
function Initilize(divId, options) {
self.options = $.extend({ buttons: [] }, options);
self.divId = divId;
self.dialog = $(self.divId);
self.dialogBody = self.dialog.find('*[data-modal="content"]');
self.dialogTitle = self.dialog.find('*[data-modal="title"]');
self.dialogFooter = self.dialog.find('*[data-modal="footer"]');
self.BootgridObject = null;
};
function OpenPartialViewDialog(url, title, preprocessingFunction, postProcessingFunction) {
// Create the buttons
var options = self.GetPartialViewButtons(url, preprocessingFunction, postProcessingFunction);
// Initialise the PartialViewDialog with Buttons
Initilize('#PartialViewDialog', options);
// Set the URL for Ajax content load and Form Post
self.actionUrl = url;
// Set Dialog Title
self.dialogTitle.html(title);
// Open the PartialViewDialog
self.OpenModel();
};
// This Method creates the buttons for the Form dialog
// e.g Save, Cancel, Ok buttons
self.GetPartialViewButtons = function (url, preprocessingFunction, postProcessingFunction) {
// I only need Save and Cancel buttons always so I create them here
var buttons = {
buttons: {
// I create a save button which Posts back the Form in the Dialog
Save: {
Text: "Save",
css: "btn btn-success",
click: function () {
// Call a function before sending the Ajax request to submit form
if (preprocessingFunction) { preprocessingFunction(); }
$.ajax({
type: "POST",
url: url,
// This Dialog has a Form - which is Post back to server
data: self.dialogBody.find("form").serialize(),
success: function (response) {
// TODO: Check if response is success -
// Apply your own logic here
if (response.hasOwnProperty("IsSuccess")) {
if (response.IsSuccess) {
self.dialogBody.html("");
self.dialog.modal("hide");
// TODO: Show Success Message
// You can call another function if you want
if (postProcessingFunction) {
postProcessingFunction();
}
} else {
// If failure show Error Message
}
}
},
error: function (response) {
// If failure show Error Message
}
});
}
},
Cancel: {
Text: "Cancel",
css: "btn btn-danger",
click: function () {
self.dialogBody.html("");
self.dialogFooter.html("");
self.dialogTitle.html("");
self.dialog.modal("hide");
}
}
}
};
return buttons;
};
// dynamic creating the button objects
self.CreateButtonsHtml = function () {
var htmlButtons = [];
$.each(self.options.buttons, function (name, props) {
var tempBtn = $("<button/>", {
text: props.Text,
id: "btn_" + props.Text,
"class": props.css + "",
click: props.click
}).attr({ "style": "margin-right: 5px;" });
htmlButtons.push(tempBtn);
});
return htmlButtons;
};
// This method will load the content/form from server and assign the modal body - it will assign the buttons to the Modal Footer and Open the Dialog for user
self.OpenModel = function () {
$.ajax({
url: self.actionUrl,
type: "GET",
success: function (response) {
// Handle response from server -
// I send JSON object if there is Error in loading the content - otherwise the result is HTML
if (response.hasOwnProperty("HasErrors")) {
// Means some error has occured loading the content - you will have to apply your own logic
} else {
//Server return HTML - assign to the modal body HTML
self.dialogBody.html(response);
self.modalObject = self.dialog.modal();
// show modal
self.modalObject.show();
}
}
});
// Create the buttons in the Dialog footer
var buttons = self.CreateButtonsHtml();
self.dialogFooter.html('');
for (var i = 0; i < buttons.length; i++) {
self.dialogFooter.append(buttons[i]);
}
};
return {
OpenPartialViewDialog: OpenPartialViewDialog,
};
})();
Then whenever I need to open a dialog from the server I can call it like this:
MyApp.UIHelper.DialogManager
.OpenPartialViewDialog('/Content/Load', "My Title",
function(){alert('pre-process')},
function(){alert('post-process')}
);
Note: The PreProcess + PostProcess are called when the Save button is clicked
Here is a working/demo example which shows what the above JS does - Hope it helps
In the demo I load Dummy HTML from a div id="dummycontent"
Fiddle: https://jsfiddle.net/1L0eLazf/
Button Fiddle: https://jsfiddle.net/1L0eLazf/1/

Knockout bindings not working as expected for manipulating observable array

We have a view using Razor and Knockout.js that displays a form. Part of the form asks the user to enter a list of values, and we're using a ko.observablearray to keep track of them. This list is represented as a bunch of text boxes, one per value, with a "Delete" button next to each box and a single "Add" button underneath all of them. It works similarly to the demo project at http://learn.knockoutjs.com/#/?tutorial=collections.
Our form is acting unexpectedly in two ways:
When a delete button is clicked, it removes all values from the ko.observablearray, not just the one corresponding to what was clicked.
When the "Submit" button for the overall form is clicked, it adds a new element to the ko.observablearray instead of submitting the form to our server.
Why are we seeing this behavior? (I know that these are two separate issues, but I'm not sure if they're caused by the same underlying problem or not, which is why I'm posting them in one question.)
Here is our Razor view:
#model OurProject.Models.Input.InputModel
#{
ViewBag.Title = "Input";
}
<h2>Inputs</h2>
<div id="inputForm">
<!-- snip - lots of input elements to fill in that are bound to KO -->
<div>
#Html.LabelFor(model => model.POSTransactionCodes)
</div>
<div>
<span class="help-block">Separate values by commas.</span>
</div>
<div>
<ul data-bind="foreach: POSTransactionCodes">
<li><input data-bind="value: $data" /> Delete</li>
</ul>
<button data-bind="click: addPOSTransactionCode">Add another POS Transaction Code</button>
#Html.ValidationMessageFor(model => model.POSTransactionCodes, null, new { #class = "help-inline" })
</div>
<!-- snip - more input elements -->
<button data-bind="click: save">Submit</button>
</div>
<script type="text/javascript" src='~/Scripts/jquery-1.8.2.min.js'></script>
<script type="text/javascript" src='~/Scripts/knockout-2.1.0.js'></script>
<script type="text/javascript" src='~/Scripts/OP/OP.js'></script>
<script type="text/javascript" src='~/Scripts/OP/Input/OP.Input.Input.Form.js'></script>
<script type="text/javascript" src='~/Scripts/OP/Input/OP.Input.Input.Data.js'></script>
<script type="text/javascript">
var elementToBindTo = $("#inputForm")[0];
OP.Input.Input.Form.init(elementToBindTo);
</script>
Here is our main piece of Knockout code, OP.Input.Input.Form.js:
extend(OP, 'OP.Input.Input.Form');
OP.Input.Input.Form = function (jQuery) {
//The ViewModel for the page
var ViewModel = function () {
var self = this;
//Fields
/* snip - lots of ko.observables() */
self.POSTransactionCodes = ko.observableArray([]); //is a list of transaction codes
/* snip - lots of ko.observables() */
//Set up with initial data
self.initialize = function () {
var c = function (data, status, response) {
if (status === "success") {
/* snip - lots of ko.observables() */
ko.utils.arrayPushAll(self.POSTransactionCodes, data.POSTransactionCodes);
self.POSTransactionCodes.valueHasMutated();
/* snip - lots of ko.observables() */
} else {
}
};
OP.Input.Input.Data.GetInput(c);
}
//When saving, submit data to server
self.save = function (model) {
var c = function (data, status, response) {
if (status === "success") {
//After succesfully submitting input data, go to /Input/Submitted
//in order to let MVC determine where to send the user next
window.location.href = "~/Input/Submitted";
} else {
}
};
OP.Input.Input.Data.SaveInput(model, c);
}
//Modifying POSTransactionCodes array
self.removePOSTransactionCode = function (POScode) {
self.POSTransactionCodes.remove(POScode)
}
self.addPOSTransactionCode = function () {
self.POSTransactionCodes.push("");
}
};
//Connect KO form to HTML
return {
init: function (elToBind) {
var model = new ViewModel();
ko.applyBindings(model, elToBind);
model.initialize();
}
};
} ($);
Here is OP.Input.Input.Data.js:
extend(OP, 'OP.Input.Input.Data');
OP.Input.Input.Data = {
GetInput: function (callback) {
$.get("/API/Input/InputAPI/GetInputModel", callback);
},
SaveInput: function (input, callback) {
$.ajax({
url: "/API/Input/InputAPI/SaveInput",
type: "post",
data: input,
complete: callback
});
}
};
You need to be pushing a new ViewModel into your observable array. Which will contain observable properties.
So to do this I created a new view model called TransactionCodeView
var TransactionCodeView = function() {
var self = this;
self.code = ko.observable("");
};
Then when the user clicks "Add another POS Transaction Code":
self.addPOSTransactionCode = function () {
self.POSTransactionCodes.push(new TransactionCodeView());
}
The only other thing changed was in the HTML binding:
<li><input data-bind="value: code" /> Delete</li>
Because code is the observable property in the new viewmodel we bind the input value to that.
Take a look at this jsfiddle. I haven't tested the submit functionality for obvious reasons ;-)
This is why the submit functionality wasn't working on my form:
In the view, I had this Razor:
<div>
<ul data-bind="foreach: POSTransactionCodes">
<li><input data-bind="value: $data" /> Delete</li>
</ul>
<button data-bind="click: addPOSTransactionCode">Add another POS Transaction Code</button>
#Html.ValidationMessageFor(model => model.POSTransactionCodes, null, new { #class = "help-inline" })
</div>
Using the button element for my "Add" button was causing it to respond to the user pressing enter instead of the submit button at the end of the form. When I changed the button into an input element instead, it started working as expected.
<input type="button" value="Add another POS Transaction Code"
data-bind="click: addPOSTransactionCode" />

KnockoutJS: template is not updated on observable array change (only on add, works on remove)

So, I have observable array with sites, which is shown via template. If I'll add site to this array, template is not updated, but if I'll remove site from array – voila! template became updated and all previously added sites became displayed too.
If I'll use nifty hack (commented in code) with replacement of whole array to new one then everything works.
BTW, I load template via AJAX and use "ko.applyBindings(viewModel)" after. I assume that works fine, because initial sites are displayed correctly.
$(function(){
//site entry in user's sites list
var siteObject = function(url, lastChecked, status){
this.url = url;
this.lastChecked = (lastChecked == 'undefined') ? '' : lastChecked;
this.status = (status == 'undefined') ? 'not_checked_yet' : status;
this.toDelete = false;
this.remove = function() {viewModel.sites.remove(this)};
};
viewModel = {
//=========== sites list managment ==========================
sites: ko.observableArray(),
//on "add" click in "add site" form
addSite: function(){
var $form = $('#add_site_form');
var siteUrl = $form.find('input[name="site"]').val();
/*nifty hack <----
var sites = this.sites();
sites.push(new siteObject(siteUrl));
this.sites(sites);*/
this.sites.push(new siteObject(siteUrl));
},
//on "remove sites" button click
removeSites: function() {
var sitesToRemove = [];
$.each(this.sites(), function(){
if (this.toDelete) sitesToRemove.push(this);
});
if (sitesToRemove.length == 0)
alert("Ни одного сайта не было выбрано для удаления.");
else {
var message = "Вы точно хотите перестать отслеживать";
for (var i in sitesToRemove) {
message += "\n\"" + sitesToRemove[i].url + "\"";
}
message += "?";
if (confirm(message)) {
$.each(sitesToRemove, function(){this.remove()});
//save new sites list to db
this.saveSitesListToDb();
}
}
//hide form
$('#remove_sites_form').slideToggle();
//toggle checkboxes
$('#content_sites_list .site_info input[type="checkbox"]').slideToggle();
};
And the template:
<!-- end of menu -->
<div id="content_sites_list"
class="grid_12"
data-bind="template: {name: 'sites_list_template', foreach: sites}"></div>
<!-- Templates -->
<script id="sites_list_template" type="text/x-jquery-tmpl">
<div class="site">
<div class="site_panel grid_12">
<div class="site_info">
–
<input type="checkbox" value="${url}"
class="delete_checkbox" data-bind="checked: toDelete" />
${url.substr(7)}
{{if status == "200"}}
<img src="img/green_light.png" alt="ok"/>
{{/if}}
</div>
<div class="site_stat">
<div class="site_last_check">Последняя проверка: ${dateTimestamp}</div>
</div>
</div>
</div>
</script>
I've tried this on latest beta on knockoutjs and on stable one.
I have made a jsFiddle which works fine.
There were some problems that JSLint was complaining about in the removeSites function of the viewModel. I fixed those and added a button and input field to be able to give some input, and everything ran smooth.
So you could try updating your removeSites function and see if it helps you,

Categories