Highlight error on specific textbox: KnockoutJS - javascript

Using asp.net mvc, c#, knockoutjs
I have a textarea and a ADD button next to it. User can enter in the textarea and press CHECK button or they can click the ADD button to add another textarea and then click CHECK button. There is no restriction to no of textarea they can add.
When they press the CHECK button I the code hits my MVC controller method and does some validations on the input. If the input is fine I proceed else show error.
My issue is highlighting and displaying the correct error. Right now I am just using one generic error label to show error. I want to show error to the textarea which has the error not a generic one. I have my code as below.
<form id="form" asp-controller="Home" asp-action="FilterMaintenance" method="post">
<div class="form-horizontal" style="margin-top:50px">
<div data-bind="foreach: { data: records }">
<div class="form-group">
<div class="col-md-6 ">
<textarea rows="1" class="required text-danger form-control textbox-wide" data-bind="value: $data.input, attr: { name: 'Records[' + $index() + '].Input', id: 'Records[' + $index() + '].Input'}"></textarea>
<span class="help-block-msg field-validation-valid" data-valmsg-replace="true" data-bind="attr: { 'data-valmsg-for': 'Records[' + $index() + '].Input'}"></span>
</div>
<div class="col-md-2">
<input type="button" value="Add Record" class="btn btn-primary" data-bind="click: $parent.addRecord, visible: function(){ return !($index() != $parent.records().length - 1) }()" />
</div>
</div>
</div>
<div class="form-group">
<div class="col-md-2">
<input type="button" value="Check Input" class="btn btn-primary" data-bind='click: checkInput' />
</div>
</div>
<div class="form-group" data-bind="visible: errorFlag">
<span data-bind="text: errorText"></span>
</div>
</div>
Knockout code:
(function () {
var viewModel = function (vmData) {
var self = this;
self.errorFlag = ko.observable(false);
self.errorText = ko.observable();
self.records = ko.observableArray();
self.records([{
input: ""
}]);
self.addRecord = function () {
self.records.push(
{
input: ""
});
};
self.checkInput = function () {
var returnVal = false;
var records = self.records();
var input = JSON.stringify(records);
//With abc as first and def as second texarea entry I get as below
//"[{"input":"abc"},{"input":"def"}]"
$.ajax({
type: "POST",
async: false,
url:"/Home/CheckRecord",
contentType: 'application/x-www-form-urlencoded; charset=utf-8',
data: {
"input": input
},
success: function (data) {
self.errorFlag(true);
if (data == '') {
self.errorText('Input is correct');
returnVal = true;
}
else {
self.errorText('Input is not correct: ' + data);
returnVal = false;
}
}
});
return returnVal;
}
}
var pageVM = new viewModel();
ko.applyBindings(pageVM, $("form")[0]);
})();
MVC Controller:
[HttpPost]
public IActionResult CheckRecord(string input)
{
string parseError = string.Empty;
bool inputCheck = false;
string returnValue = string.Empty;
inputCheck = false;//doing some checks here then return true or false
returnValue = inputCheck ? "" : "error";
var ret = Json(returnValue);
return ret;
}
Also I get the below generated HTML for 2 textarea added:
<div class="form-group">
<div class="col-md-6 ">
<textarea rows="1" class="required text-danger form-control textbox-wide" data-bind="value: $data.input, attr: { name: 'Records[' + $index() + '].Input', id: 'Records[' + $index() + '].Input'}" name="Records[0].Input" id="Records[0].Input"></textarea>
<span class="help-block-msg field-validation-valid" data-valmsg-replace="true" data-bind="attr: { 'data-valmsg-for': 'Records[' + $index() + '].Input'}" data-valmsg-for="Records[0].Input"></span>
</div>
<div class="col-md-2">
<input type="button" value="Add Record" class="btn btn-primary" data-bind="click: $parent.addRecord, visible: function(){ return !($index() != $parent.records().length - 1) }()" style="display: none;">
</div>
</div>
<div class="form-group">
<div class="col-md-6 ">
<textarea rows="1" class="required text-danger form-control textbox-wide" data-bind="value: $data.input, attr: { name: 'Records[' + $index() + '].Input', id: 'Records[' + $index() + '].Input'}" name="Records[1].Input" id="Records[1].Input"></textarea>
<span class="help-block-msg field-validation-valid" data-valmsg-replace="true" data-bind="attr: { 'data-valmsg-for': 'Records[' + $index() + '].Input'}" data-valmsg-for="Records[1].Input"></span>
</div>
<div class="col-md-2">
<input type="button" value="Add Record" class="btn btn-primary" data-bind="click: $parent.addRecord, visible: function(){ return !($index() != $parent.records().length - 1) }()">
</div>
</div>
Sorry for the long post but wanted to show the full code for explanation.
Any inputs are appreciated.
Update:
I have a added this in jsfiddle as well:
https://jsfiddle.net/gmmmh873/

I would recommend looking at Knockout-Validation. It can handle most of this for you, and has great documentation to get you started. This injects error messages per input field when the rules are not met. You can also trigger these from the response back from your xhr call as well.

Related

Using Modal JavaScript in the Partial View of .NET CORE will not work after Ajax Post

I use the Modal display field in the Partial View to input data for the User, and use data-url=#url.action("Create") in the main screen to call Modal.
And wrote Autocomplete JavaScript in Partial View.
It works perfectly before using Ajax Post.
But after going through Ajax, the JavaScript cannot be used when it returns because of an error.
How can I make corrections?
Main View
<div id="PlaceHolderHere" data-url="#Url.Action("Create")"></div>
Ajax Code
$(function () {
var PlaceHolderElement = $('#PlaceHolderHere');
$('button[data-toggle="ajax-modal"]').click(function (event) {
event.preventDefault();
var url = $(this).data('url');
$.get(url).done(function (data) {
PlaceHolderElement.html(data);
PlaceHolderElement.find('.modal').modal('show');
});
});
PlaceHolderElement.on('click', '[data-save="modal"]', function (event) {
event.preventDefault();
var form = $(this).parents('.modal').find('form');
var actionUrl = form.attr('action');
var sendData = new FormData(form.get(0));
console.log(sendData);
$.ajax({
url: actionUrl,
method: 'post',
data: sendData,
processData: false,
contentType: false,
cache: false,
success: function (redata) {
console.log(redata);
if (redata.status === "success") {
PlaceHolderElement.find('.modal').modal('hide');
}
else {
var newBody = $('.modal-body', redata);
var newFoot = $('.modal-footer', redata);
PlaceHolderElement.find('.modal-body').replaceWith(newBody);
PlaceHolderElement.find('.modal-footer').replaceWith(newFoot);
}
},
error: function (message) {
alert(message);
}
})
})
})
Partial View of JavaScript part
#{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
<script src="~/bootstrap-autocomplete/dist/latest/bootstrap-autocomplete.min.js"></script>
$('#BossName').autoComplete({
resolver: 'custom',
minLength: 2,
formatResult: function (item) {
return {
value: item.value,
text: "[" + item.value + "] " + item.text,
}
},
noResultsText:'There is no matching data, please confirm whether there is data in the company field',
events: {
search: function (qry, callback) {
// let's do a custom ajax call
$.ajax(
'#Url.Action("GetRolesByAutoComplete","Roles")',
{
data: {
'q': qry,
'key': document.getElementById('CompanyCode').value
}
}
).done(function (res) {
callback(res)
});
}
}
});
$('#BossName').on('autocomplete.select', function (evt, item) {
console.log(item);
$('#BossID').val(item.value);
$('#BossName').val(item.text);
});
Partial View of Modal
<div class="modal fade" id="AddEditRoles" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="AddEditRolesLabel">Add New Roles</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<form asp-action="Create" id="Edit">
<div class="input-group">
<span class="input-group-text">#Html.DisplayNameFor(m => m.RolesCode)</span>
#if (Model != null && Model.RolesCode != null)
{
<input asp-for="RolesCode" class="form-control" readonly />
}
else
{
<input asp-for="RolesCode" class="form-control" autocomplete="off" />
}
<span asp-validation-for="RolesCode" class="text-danger"></span>
</div>
<div class="input-group">
<span class="input-group-text">#Html.DisplayNameFor(m => m.Title)</span>
<input asp-for="Title" class="form-control" autocomplete="off" />
<span asp-validation-for="Title" class="text-danger"></span>
</div>
<div class="input-group">
<span class="input-group-text">#Html.DisplayNameFor(m => m.CompanyCode)</span>
<input type="text" asp-for="CompanyCode" class="form-control col-md-3" readonly />
<input type="text" id="CompanyName" class="form-control" autocomplete="off"
placeholder="Please type Key word" />
<span asp-validation-for="CompanyCode" class="text-danger"></span>
</div>
<div class="input-group">
<span class="input-group-text">#Html.DisplayNameFor(m => m.BossID)</span>
<input asp-for="BossID" type="text" class="form-control col-md-3" readonly />
<input id="BossName" type="text" class="form-control" autocomplete="off"
placeholder="Please type Key word" />
<span asp-validation-for="BossID" class="text-danger"></span>
</div>
</form>
</div>
<div class="modal-footer">
<div class="text-danger">#Html.ValidationMessage("error")</div>
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button id="Save" type="button" class="btn btn-primary" data-save="modal">Save changes</button>
</div>
</div>
</div>
</div>
You send data to the server, but when it fails you replace modal contents.
Replacing HTML destroys everything that was already there, so you wipe everything that was done by your autocomplete plugin.
All you need to do is to initialize autocomplete again:
success: function (redata) {
if (redata.status === "success") {
} else {
var newBody = $('.modal-body', redata);
var newFoot = $('.modal-footer', redata);
PlaceHolderElement.find('.modal-body').replaceWith(newBody);
PlaceHolderElement.find('.modal-footer').replaceWith(newFoot);
// INITIALIZE AUTOCOMPLETE HERE
}
},

Form in ajax post method sends empty object

I have a webform with several fields I want to capture in an object and send it to a controller method. The form has this code:
<div class="panel-footer">
#using (Html.BeginForm("NuevaOpcion", "Home", FormMethod.Post, new { #id = "frm_nueva_opcion" })) {
#Html.HiddenFor(m => m.Id)
<div class="row">
<div class="col-md-6">
<div class="form-group" style="margin-bottom: .7em;margin-top: .7em;">
<button class="btn btn-success btn-xs" type="button" onclick=" $('#row-nueva-opcion').toggle()" id="add-opcion">
<span class="glyphicon glyphicon-plus-sign"></span> Añadir nueva opción
</button>
</div>
</div>
</div>
<div class="row" id="row-nueva-opcion" style="display:none">
<div class="col-md-10">
<label>
<input type="checkbox" id="opcion-extra" onclick=" $('#nuevo-precio').attr('disabled', !this.checked);" />
Es opción extra
</label>
<div class="input-group" style="margin-bottom:1.7em;">
<input type="text" placeholder="Opción" class="form-control" name="nombre" style="max-width:70%;">
<input type="number" placeholder="Cantidad" min="1" value="1" class="form-control" name="cantidad" style="max-width:15%;">
<input type="number" placeholder="Precio" class="form-control" id="nuevo-precio" name="precio" style="max-width:15%;" disabled>
<input type="hidden" name="idrespuesta" id="idrespuesta" value="#listItems.Select(x=>x.Value).FirstOrDefault()" />
<div class="input-group-addon">€</div>
<span class="input-group-btn">
<a class="btn btn-primary" data-title="Confirmación de acción" data-toggle="modal" data-target="#modal_confirm" onclick="confirmar('frm_nueva_opcion')">
<span class="glyphicon glyphicon-floppy-disk"></span> Guardar
</a>
</span>
</div>
</div>
<div class="col-md-8">
<div class="form-group">
<label>
¿Para que pregunta es la opción?
#Html.DropDownList("OptionSelectedItem", listItems, new { #class = "form-control" })
</label>
</div>
</div>
</div>
}
</div>
To manage it, I have a script that looks like this:
function mostrarModal(idmodal, mensaje, tipo) {
$(idmodal + ' .modal-body h4').addClass(tipo == 'error' ? 'text-danger' : 'text-secondary').html(mensaje);
$(idmodal).modal('show');
}
function enviar(form) {
debugger;
var NuevoPrecio = $('#' + form).attr("nuevo-precio");
if( (NuevoPrecio == null) || (typeof NuevoPrecio === "undefined") ) { var NuevoPrecio = 0; }
var datos = {
Id: $('#' + form).attr("#Id"),
IdPresupuestador: $('#' + form).attr("#idPresupuestador"),
IdRespuesta: $('#' + form).attr("#idrespuesta"),
Cantidad: $('#' + form).attr("#cantidad"),
Nombre: $('#' + form).attr("#nombre"),
Precio: NuevoPrecio,
}
$.post("NuevaOpcion", {
contentType: 'application/json; charset=utf-8',
data: JSON.stringify(datos),
});
}
var modalConfirm = function (callback) {
$("#modal-btn-si").on("click", function () {
callback(true);
$("#modal-confirm").modal('hide');
});
$("#modal-btn-no").on("click", function () {
callback(false);
$("#modal-confirm").modal('hide');
}); };
function confirmar(form, text) {
$("#modal-confirm").modal('show');
modalConfirm(function (confirm) {
if (confirm) {
enviar(form);
}
}); };
Trouble is, I've changed the script on several points and now looks like this because the best I could manage was taking all the form in a single object. I can't work with the properties contained in that object, not on the script and neither on the controller method.
So, the question is, how am I selecting the fields wrong? I've tried with "#", ".", just the name between quotes, and as I said, the best I could get was the entire form in a single object. Thanks in advance.

Save multiple dynamically added file in mongoose

I'm using Ajax to add (and remove) multiple fields in a form, and then submitting them to mongoose to save them. Unfortunatelly I'm able to retrieve and save only 1 array, missing completely the dinamically added ones.
HTML: Here's a form with a form-group visible, where I inizialise the array using name attributes,and a form-group template that I dinamically populate with Ajax
<form id="productAdd" class="form-horizontal" method="post" enctype="multipart/form-data" action="?_csrf={{csrfToken}}">
<section class="panel">
<div class="panel-body">
<div class="form-group" data-option-index="1">
<label class="col-md-3 control-label" for="optionType">Type</label>
<div class="col-md-1">
<select class="form-control" name="options[0][optionType]">
<option value="grams">Gr.</option>
</select>
</div>
<label class="col-md-1 control-label" for="value">Value</label>
<div class="col-md-1">
<input type="text" class="form-control" name="options[0][optionValue]">
</div>
<label class="col-md-1 control-label" for="price">Price</label>
<div class="col-md-1">
<input type="text" class="form-control" name="options[0][optionPrice]">
</div>
<div class="col-md-1">
<button type="button" class="btn btn-default addButton"><i class="fa fa-plus"></i></button>
</div>
</div>
<div class="form-group hide" id="optionTemplate">
<label class="col-md-3 control-label" for="optionType">Type</label>
<div class="col-md-1">
<select class="form-control" name="optionType">
<option value="grams">Gr.</option>
</select>
</div>
<label class="col-md-1 control-label" for="value">Value</label>
<div class="col-md-1">
<input type="text" class="form-control" name="optionValue">
</div>
<label class="col-md-1 control-label" for="price">Price</label>
<div class="col-md-1">
<input type="text" class="form-control" name="optionPrice">
</div>
<div class="col-md-1">
<button type="button" class="btn btn-default removeButton"><i class="fa fa-minus"></i></button>
</div>
</div>
</div>
<footer class="panel-footer">
<div class="row">
<div class="col-sm-9 col-sm-offset-3">
<input type="hidden" name="_csrf" value="{{csrfToken}}">
<button class="btn btn-primary">Submit</button>
<button type="reset" class="btn btn-default">Reset</button>
</div>
</div>
</footer>
</section>
</form>
AJAX: I use this to add rows or remove them, each with 3 inputs. Before submitting the form I use .serialize() to get the arrays by their names
$(document).ready(function() {
var optionIndex = $(".form-group[data-option-index]").length;
$('#productAdd').submit(function(e) { // add product submit function
e.preventDefault();
$(this).ajaxSubmit({
contentType: 'application/json',
data: $('form[name="productAdd"]').serialize(),
error: function(xhr) {
console.log('Error: ' + xhr.status + ' ---- ' + xhr.responseText);
},
success: function(response) {
if (typeof response.redirect === 'string')
window.location = response.redirect;
}
});
return false;
})
// Add button click handler
.on('click', '.addButton', function() {
optionIndex++;
var $template = $('#optionTemplate'),
$clone = $template
.clone()
.removeClass('hide')
.removeAttr('id')
.attr('data-option-index', optionIndex)
.insertBefore($template);
// Update the name attributes
$clone
.find('[name="optionType"]').attr('name', 'options[' + optionIndex + '].optionType').end()
.find('[name="optionValue"]').attr('name', 'options[' + optionIndex + '].optionValue').end()
.find('[name="optionPrice"]').attr('name', 'options[' + optionIndex + '].optionPrice').end();
})
// Remove button click handler
.on('click', '.removeButton', function() {
var $row = $(this).parents('.form-group'),
index = $row.attr('data-option-index');
// Remove fields
$row.find('[name="options[' + index + '].title"]').remove();
$row.find('[name="options[' + index + '].isbn"]').remove();
$row.find('[name="options[' + index + '].price"]').remove();
// Remove element containing the fields
$row.remove();
});
});
NODEJS: Here's my nodejs route, I use multer for managing file upload. I've a foreach should manage the inputs from the form, but it just see the first element.
router.post('/shop/products/add', [isLoggedIn, multer({dest: './public/images/products/'}).single('productPhoto')], function(req, res, next) {
var newProduct = new Product();
newProduct.imagePath = req.file.filename;
newProduct.title = req.body.title;
newProduct.description = req.body.description;
newProduct.price = req.body.price;
newProduct.save()
.then(function (product) {
console.log(req.body.options);
req.body.options.forEach(function (option) {
var newOption = new ProductOption();
newOption.type = option.optionType;
newOption.value = option.optionValue;
newOption.price = option.optionPrice;
newOption.product = product;
newOption.save();
});
})
.then(function (options) {
req.flash('success', 'Product uploaded correctly.');
res.send({redirect: '/user/shop/products/add'});
})
.catch(function (err) {
console.log('Error ' + err.code + ': ', err.message);
res.status(500).send('Failed to save the newAddress to DB: ' + err);
});
});
It was a simple mistake, I named the fields dinamically added, differently from the static ones.
this code
.find('[name="optionType"]').attr('name', 'options[' + optionIndex + '].optionType').end()
should be like this
.find('[name="optionType"]').attr('name', 'options[' + optionIndex + '][optionType]').end()

knockout form validation message

I'd like to use Knockout.js to highlight errors on a form.
Currently i see all errors when i get the form,
I want to get them only if the user press on Save button.
So I'm try to set a flag that will be true only if the user press on it, and then to use it in all form but i don't have success with that.
I will apreciate some help with that. (or any other way to do it)
so i will need someting like that on my html:
<div class='liveExample' data-bind="css: {hideErrors: !hasBeenSubmittedOnce()">
and somewhere in my js file:
this.hasBeenSubmittedOnce = ko.observable(false);
this.save = function(){
this.hasBeenSubmittedOnce(true);
}
that my files
HTML
<div class='liveExample' data-bind="css: {hideErrors: !hasBeenSubmittedOnce()">
<form class="form-horizontal" role="form">
<div class="form-group">
<label class="col-sm-3 control-label">Store:</label>
<div class="col-sm-3" data-bind="css: { error: storeName.hasError() }">
<input data-bind='value: storeName, valueUpdate: "afterkeydown"' />
<span data-bind='text: storeName.validationMessage'> </span>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">Company ID:</label>
<div class="col-sm-3" data-bind="css: { error: companyId.hasError }">
<input data-bind='value: companyId, valueUpdate: "afterkeydown"' />
<span data-bind='text: companyId.validationMessage'> </span>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">Address</label>
<div class="col-sm-3" data-bind="css: { error: address.hasError }">
<input data-bind='value: address, valueUpdate: "afterkeydown"' />
<span data-bind='text: address.validationMessage'> </span>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">Phone:</label>
<div class="col-sm-3" data-bind="css: { error: phone.hasError }">
<input data-bind='value: phone, valueUpdate: "afterkeydown"' />
<span data-bind='text: phone.validationMessage'> </span>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-4">
<button class="btn btn-primary" data-bind="click: save">Add store</button>
</div>
</div>
</form>
Store:
Company ID:
Address
Phone:
Add store
</form>
js
define(['knockout'], function (ko){
ko.extenders.required = function(target, overrideMessage) {
//add some sub-observables to our observable
target.hasError = ko.observable();
target.validationMessage = ko.observable();
//define a function to do validation
function validate(newValue) {
target.hasError(newValue ? false : true);
target.validationMessage(newValue ? "" : overrideMessage || "This field is required");
}
validate(target());
target.subscribe(validate);
return target;
};
function AppViewModel(storeName, companyId, address, phone) {
this.storeName = ko.observable(storeName).extend({ required:"" });
this.companyId = ko.observable(companyId).extend({ required: "" });
this.address = ko.observable(address).extend({ required: "" });
this.phone = ko.observable(phone).extend({ required: ""});
this.hasError = ko.computed(function(){
return this.storeName.hasError() || this.companyId.hasError();
}, this);
this.hasBeenSubmittedOnce = ko.observable(false);
this.save = function(){
this.hasBeenSubmittedOnce(true);
}
}
return AppViewModel;
});
CSS file
.form-group span {
display: inherit;
}
.hideErrors .error span {
display: none;
}
I did some work around for this and not sure it is better way or not.
var showError=ko.observableArray([]);//it will store show Error Message
ko.extenders.required = function(target, overrideMessage) {
//add some sub-observables to our observable
target.hasError = ko.observable();
target.validationMessage = ko.observable();
//define a function to do validation
function validate(newValue) {
target.hasError($.trim(newValue) ? false : true);
target.validationMessage($.trim(newValue) ? "" : overrideMessage || "This field is required");
}
showError.push(function(){validate(target());});
target.subscribe(validate);
return target;
};
function AppViewModel(storeName, companyId, address, phone) {
this.storeName = ko.observable(storeName).extend({ required:"" });
this.companyId = ko.observable(companyId).extend({ required: "xcxcx" });
this.address = ko.observable(address).extend({ required: "" });
this.phone = ko.observable(phone).extend({ required: ""});
this.hasError = ko.computed(function(){
return self.storeName.hasError() || self.companyId.hasError();
}, this);
this.hasBeenSubmittedOnce = ko.observable(false);
this.save = function(){
ko.utils.arrayForEach(showError(),function(func){
func();
});
}
}
ko.applyBindings(new AppViewModel());
Fiddle Demo
You can use the visible knockout binding:
<div data-bind="visible: hasBeenSubmittedOnce">
As for the error class, you can use the define the default knockout validation class: 'validationElement' and call
ko.validation.init({ decorateElement: true});
Here you can find more:
https://github.com/Knockout-Contrib/Knockout-Validation/wiki/Configuration
(please note that decorateElement has been renamed 2013-11-21 to 'decorateInputElement')

$parent.myFunction not returning row as parameter in KnockoutJs

I'm using knockoutjs for the first time in an attempt to have a list of tasks and then to be able to click on one to show it in a popup form. The issue I'm having is with data-bind="click: $parent.edit", which calls ToDoListViewModel/edit.
When I first started writing the list code, it would pass in the current row's todo object as the first parameter into the edit function. After adding the second view model for my add/edit popup form, it no longer behaves this way. Calling $parent.edit still calls the edit function, however now it passes in an empty object { }.
It seems like I'm running into some sort of conflicting bindings issue, any ideas?
Here is the to do list html:
<table class="table table-striped table-hover" data-bind="visible: isVisible">
<thead>
<tr>
<th>To-Do</th>
<th>Estimate</th>
<th>Deadline</th>
#if (Model == null) { <th>Property</th> }
<th>Tenant</th>
<th style="display: none;">Assigned To</th>
<th></th>
</tr>
</thead>
<tbody data-bind="foreach: todos">
<tr data-bind="visible: isVisible()">
<td><a class="pointer" title="Edit To-Do" data-bind="click: $parent.edit, text: Task"></a></td>
<td data-bind="text: FormattedAmount"></td>
<td data-bind="text: FormattedDueDate"></td>
<td data-bind="visible: $parent.showPropertyColumn, text: PropertyName"></td>
<td data-bind="text: TenantName"></td>
<td>
<div class="btn-group pull-right">
<a class="btn btn-primary pointer default-click-action" data-bind="click: $parent.markComplete">Mark Complete</a>
<button class="btn btn-primary dropdown-toggle" data-toggle="dropdown"><span class="caret"></span></button>
<ul class="dropdown-menu">
<li><a class="pointer" data-bind="click: $parent.edit">Edit To-Do</a></li>
<li><a class="pointer" data-bind="click: $parent.remove">Delete To-Do</a></li>
</ul>
</div>
</td>
</tr>
</tbody>
</table>
And here is the popup html:
#model Housters.Schemas.Models.Property.Listing
<div id="popup" class="modal fade" style="display: none;" data-bind="with: form">
#using (Html.BeginForm("SaveTodo", "Properties", FormMethod.Post, new { Id = "form", #class = "form-horizontal" }))
{
<div class="modal-header">
<a class="close" data-dismiss="modal">×</a>
<h3>To-Do Information</h3>
</div>
<div class="modal-body">
<input id="id" name="id" type="hidden" data-bind="value: todo().Id" />
<input id="originalListingId" type="hidden" value="#(Model != null ? Model.IdString : "")" />
<div class="control-group #(Model != null ? " hide" : "")">
<label class="control-label" for="listingId">Property</label>
<div class="controls">
#Html.DropDownList("listingId", ViewBag.Listings as SelectList, "None",
new Dictionary<string, Object> { { "data-bind", "value: todo().ListingId" } })
</div>
</div>
<div class="control-group">
<label class="control-label" for="tenantId">Tenant</label>
<div class="controls">
<select id="tenantId" name="tenantId" class="span11" data-bind="value: todo().TenantId">
<option value="">None</option>
</select>
<p class="help-block">Is this task related to a certain tenant?</p>
</div>
</div>
<div class="control-group">
<label class="control-label" for="amount">Estimate to Complete</label>
<div class="controls">
<div class="input-prepend"><span class="add-on">$</span><input type="text" id="amount" name="todo.Amount" maxlength="10"
class="span11 number" data-bind="value: todo().Amount" /></div>
</div>
</div>
<div class="control-group">
<label class="control-label" for="dueDate">Due Date</label>
<div class="controls">
<input type="text" id="dueDate" name="todo.DueDate" maxlength="10"
class="span11 date" data-bind="value: todo().DueDate" />
</div>
</div>
<div class="control-group">
<label class="control-label" for="task">Task<span class="required-asterisk">*</span></label>
<div class="controls">
<textarea id="task" name="todo.Task" rows="4" class="span11 required" data-bind="value: todo().Task"></textarea>
</div>
</div>
</div>
<div class="modal-footer">
<a class="btn pointer" data-dismiss="modal">Close</a>
<a id="mark-complete-popup" class="btn btn-primary" data-dismiss="modal" data-bind="visible: (todo().Id && !todo().IsComplete), click: markComplete">Mark Complete</a>
<button type="button" class="btn btn-primary" data-bind="click: save">Save</button>
</div>
}
</div>
And lastly, here is the javascript defining the view models:
function ToDoList () {
if(!(this instanceof arguments.callee))
return new arguments.callee();
var parent = this;
this.showCompleted = ko.observable(false);
this.tenantFilter = new PropertyTenantFilter();
this.viewModel = {
list: new ToDoListViewModel(),
form: new ToDoFormViewModel()
};
this.init = function () {
//get all tenants.
utils.block($("#grid-content"), "Loading");
this.tenantFilter.init(function () {
//initialize view model.
ko.applyBindings(this.viewModel);
//setup controls & events.
$("#dueDate").datepicker();
$("#listingId").change(this.tenantFilter.getByListing.bind(this.tenantFilter)).change();
} .bind(this));
};
function ToDoListViewModel() {
//init.
var self = this;
self.todos = ko.observableArray([]);
//computed.
self.showPropertyColumn = ko.computed(function () {
return $("#originalListingId").val().length == 0;
});
self.isVisible = ko.computed(function () {
return _.find(self.todos(), function (todo) { return todo.isVisible(); }) != null;
});
//operations.
self.add = function () {
//set form field values.
parent.viewModel.form.fill(new schemas.ToDo({}, parent));
//show popup.
$("#popup").modal("show");
};
self.edit = function (todo) {
console.debug("edit: " + JSON.stringify(todo));
//set form field values.
parent.viewModel.form.fill(todo);
//update tenants dropdown for selected listing.
parent.tenantFilter.getByListing();
//show popup.
$("#popup").modal("show");
};
self.markComplete = function (todo) {
parent.markComplete(todo);
};
self.remove = function (todo) {
var result = confirm("Are you sure that you want to delete this To-Do?");
if (result) {
//save changes.
utils.ajax(basePath + "properties/deletetodo",
{ id: todo.Id },
function (success) {
//refresh results.
self.todos.remove(todo);
//show result.
utils.showSuccess('The To-Do has been deleted successfully');
}
);
}
};
self.toggleShowCompleted = function () {
parent.showCompleted(!parent.showCompleted());
$("#showCompletedTodos").text(parent.showCompleted() ? "Show Active" : "Show Completed");
};
self.update = function (todo) {
var existingToDo = _.find(self.todos(), function (item) { return item.Id() == todo.Id(); });
existingToDo = todo;
};
//load todos from server.
utils.ajax(basePath + "properties/gettodos",
{ id: $("#originalListingId").val(), showCompleted: parent.showCompleted() },
function (results) {
var mappedTodos = $.map(results, function (item) { return new schemas.ToDo(item, parent); });
self.todos(mappedTodos);
$("#grid-content").unblock();
}
);
}
function ToDoFormViewModel() {
//init.
var self = this;
self.todo = ko.observable({});
utils.setupPopupForm(self.saved);
//operations.
self.fill = function (todo, isEdit) {
//set form field values.
self.todo = todo;
if (todo.Id()) {
//update tenants dropdown for selected listing.
parent.tenantFilter.getByListing();
}
//show popup.
$("#popup").modal("show");
};
self.save = function (todo) {
self.todo = todo;
$("#form").submit();
};
self.saved = function (result) {
var todo = new schemas.ToDo(result.todo, parent);
if (result.isInsert)
parent.viewModel.list.todos().push(todo);
else
parent.viewModel.list.update(todo);
utils.showSuccess("Your To-Do has been saved successfully.");
};
self.markComplete = function (todo) {
parent.markComplete(todo);
};
}
this.markComplete = function (todo) {
var result = confirm("Are you sure that you want to mark this To-Do complete?");
if (result) {
//save changes.
utils.ajax(basePath + "properties/marktodocomplete", {
id: todo.Id()
},
function () {
todo.IsComplete(true);
//show success.
utils.showSuccess('Your To-Do has been marked completed');
} .bind(this)
);
}
}
this.init();
}
One possible solution is to replace:
data-bind="click: $parent.edit"
with
data-bind="click:$root.viewModel.list.edit"

Categories

Resources