Multiple view model bindings, shared error dialog with bindings - data-binding

I have two view models on a single page that correspond to two different sets of content on different tabs.
I'm binding each view model to its corresponding root element in the markup. However, I want to have a generic section of markup that either model can use (a generic modal dialog for errors on a master page in my scenario) that is not contained within any of the bound root elements.
How should I handle this? Should I nest the multiple models inside of one master view model or something and not bind to specific elements? I'm fairly new to knockout so I'm open to all suggestions.
This shows what I'm trying to accomplish minus the duplicated modal sections. Fiddle: http://jsfiddle.net/z3wGr/3/
JS:
ko.bindingHandlers.errorModal = {
update: function (element, valueAccessor) {
var valueAccessorValue = valueAccessor();
var value = ko.utils.unwrapObservable(valueAccessorValue);
if (value.length > 0) {
// $(element).modal('show');
console.log("would be showing modal here");
} else {
// $(element).modal('hide');
}
}
};
var sectionOneViewModel = {
sectionOne: ko.observable("section one"),
errors: ko.observableArray([]),
aSectionOneMethod: function (self) {
// make AJAX call here
// error returns from server
// push error into array triggering modal because of custom binding handler
self.errors.push({
errorText: "something went wrong."
})
}
}
var sectionTwoViewModel = {
sectionTwo: ko.observable("section two"),
errors: ko.observableArray([]),
aSectionTwoMethod: function (self) {
// make AJAX call here
// error returns from server
// push error into array triggering modal because of custom binding handler
self.errors.push({
errorText: "something went wrong."
})
}
}
ko.applyBindings(sectionOneViewModel, document.getElementById('section-one'));
ko.applyBindings(sectionTwoViewModel, document.getElementById('section-two'));
HTML:
<div id="section-one">
<span data-bind="text: sectionOne"></span>
<br />
<input type="button" value="call section one function" data-bind="click: aSectionOneMethod" />
<!-- This is a generic modal that all of my viewmodels use to show errors, I don't want to include it in every binding section -->
<div id="generic-error-modal" data-bind="errorModal: errors">
<p>I'm a modal dialog that would actually only display when an error is returned from the server after an AJAX call.</p>
<ul data-bind="foreach: errors">
<li data-bind="text: errorText"></li>
</ul>
</div>
</div>
<div id="section-two">
<span data-bind="text: sectionTwo"></span>
<br />
<input type="button" value="call section two function" data-bind="click: aSectionTwoMethod" />
<!-- This is a generic modal that all of my viewmodels use to show errors, I don't want to include it in every binding section -->
<div id="generic-error-modal" data-bind="errorModal: errors">
<p>I'm a modal dialog that would actually only display when an error is returned from the server after an AJAX call.</p>
<ul data-bind="foreach: errors">
<li data-bind="text: errorText"></li>
</ul>
</div>
</div>
**Edit: Working example based off the answer and comments below: http://jsfiddle.net/z3wGr/6/

I'd recommend to have one viewModel which nests the other two models. You may need to pass in the errors to your nested models so they can interact with the errors.
I'd also recommend to use functions for your models and new the objects.
Here is a cutdown version. I haven't tested it so there may be some minor errors:
var ViewModel = function() {
var self = this;
self.errors = new Errors();
self.sectionOne = new SectionOneViewModel(self.errors);
self.sectionTwo = new SectionTwoViewModel(self.errors);
};
var SectionOneViewModel = function(errors) {
var self = this;
self.sectionOne = ko.observable("section one");
self.errors = errors;
//this is how to add an error
self.errors.add("This is an error");
};
var SectionTwoViewModel = function(errors) {
var self = this;
self.sectionTwo = ko.observable("section two");
self.errors = errors;
};
var Errors = function() {
ver self = this;
self.errors = ko.observableArray();
self.add = function (errorText) {
self.errors.push(new Error(errorText));
};
self.clear = function() {
self.errors.removeAll();
};
};
var Error = function(errorText) {
var self = this;
self.errorText = errorText;
};

Related

how to pass key of tabpanel from wiew to controller asp mvc

I have a tab panel that allowed me to switch between two window, I'd sent my choice my controller to execute a different statement in each tabuleur, for example if I have id = proj_tab_1 it returns me to input and if id = proj_tab_2 it returns the output view, but I do not know how to pass the spreadsheet key information between the view and controller here is my code
view :
<div class="tab-content">
<div class="tab-pane fade in active" id="proj_tab_1" >
#Html.Partial("_InputFilesList", Model)
</div>
<div class="tab-pane fade" id="proj_tab_2" >
#Html.Partial("_OutputFilesList", Model)
</div>
</div>
controller :
public ActionResult GetFilesListByType(MonitorFilesModel model)
{
model.FillDDL();
model.GetFilesListByType();
ViewBag.Success = true;
//if tableur 1
return View("_InputFilesList", "~/Views/Shared/_AjaxLayout.cshtml", model);
//if tableur 2
return View("_OutputFilesList", "~/Views/Shared/_AjaxLayout.cshtml", model);
}
In my opinion, you don't need to send key to controller. The easiest way, use javascript (I use jquery but it is up to you), something like that:
<script>
$('dom_element').on('event', function(){
var rel = $(this);
var pane = rel.closest('.tab-pane');
pane.removeClass('in');
pane.removeClass('active');
pane.next('.tab-pane').addClass('in active');
});
</script>
Otherwise, you can set your partial views in form and using standard action on submit button redirect to the action you needed.
By the way, I think that you have to return partial view, but not just views.
Hope, it helps.

How do you update item value(s) in ko.observableArray and rebind?

I have a ko.observableArray that when the page gets initialized 1 item is added. I then use a and a data-bind="foreach items" to create a div for each item in the ko.observableArray. I have a button and textbox on the page that when you add text to the input and click the button a new item gets pushed on to the ko.observableArray. This works fine I can add a new items with each button click. The items in the ko.observableArray have a price associated with them and the price changes. I want to update the price while still being able to add new items to the ko.observableArray. The price and item name are also ko.observable.
self.items= ko.observableArray(ko.utils.arrayMap(items, function(item) {
return { name: ko.observable(item.name), price: ko.observable(item.price) };
How to I update the underlying item values (price) and not recreate the ko.observable array? Do I have to loop through each item in the ko.observable array? The data is coming from a JSON call. BTW I am new to Knockout.js.
Here is my attempt at a JSFiddle but I could not get it fully working. Adding an item works fine but when I update if I have a different amount of item..like less items the ones not getting updated are destroyed. Anyway around this? I do not want to fetch data that does not have any changes in it.
Do something like this instead
Javascript
var itemObject = function(data){
var self = this;
//I recommend using the mapping plugin
//ko.mapping.fromJS(data, {}, self);
//If you use the mapping plugin, you don't have to hand bind each property
this.Id = ko.observable(data.Id);
.... etc ....
};
var baseViewModel = function(){
var self = this;
this.Items = ko.observableArray();
this.Setup = function(items){
//using underscore.js to map the items.
//This turns each item into an "itemObject", which you can
//then manipulate and have all changes shown on the screen, even
//inside the array.
self.Items(_.map(items, function(item){
return new itemObject(item);
}));
};
};
$(function(){
var myApp = new baseViewModel();
myApp.Setup(items);
ko.applyBindings(myApp);
});
html
<div data-bind="foreach: Items">
<div data-bind="text: Id"></div>
<!-- other binding things here -->
</div>

knockout.js - data-bind auto update after function call

I have a case where I databind to a date field inside model in a list:
function Model(data) {
var self = this;
ko.mapping.fromJS(data, {}, this);
}
<div id="fieldOnPage" data-bind="text: formatDate(myDateField())"></div>
Then, in a modal, I display the same date field so it can be edited:
<div id="fieldInModal" data-bind="text: formatDate(myDateField())"></div>
However, since I'm calling the formatDate function does its work on the unwrapped observable, I'm unable to see the changes get written real-time back onto the main page when I edit the value in the modal.
Another caveat is that I using the ko.mapping plugin so I don't necessarily have a specific ko.computed field on myDateField. Is this possible to do with an external function like this? If not, how would I do it using the ko.computed if I had to specifically override the myDateField binding?
You could do something like
function Model(data) {
var self = this;
ko.mapping.fromJS(data, {}, this);
this.formattedDate = ko.computed(function () {
return formatDate(ko.utils.unwrapObservable(self.myDateField));
});
}
The bind to the formatted Date
<div id="fieldInModal" data-bind="text: formattedDate"></div>
Hope this helps.

ObservableArray not notifying when item changed

I try to bind observableArray to div on my page and everything is ok. This array contains simple JSON objects, not observable, obtained from WebService.
After that, I want to be able to modify those objects in array and would like view to be refreshed with each modification. For example, when checkbox gets clicked I would like to change the flag on my JSON object (this seems to work automatically all right) and at the same time my UI should get updated, which does not happen. Could anyone provide me with the reason (is this because those objects are simple, not observable?) and solution?
var DocumentContentModel = function () {
var self = this;
self.content = ko.observableArray();
self.ElementApprovalChanged = function (element) {
DocumentService.DoSomething(
element.Id,
function (result) {
if (!result) {
var negatedApproved = !element.Approved;
element.Approved = negatedApproved;
}
},
function (error) {
alert(error);
});
return true;
};
};
$(document).ready(function () {
var contentModel = new ContentModel();
DocumentService.GetContent(1,
function (result) {
contentModel.content(JSON.parse(result));
});
ko.applyBindings(contentModel);
});
UI
<div class="ContentContainer">
<div data-bind='foreach: content'>
<div class="ContentElement" data-bind='css: { NotApproved: !Approved} '>
<div class="ContentValue" data-bind='html: Value'></div>
<div class="Approval">
<input type="checkbox" data-bind='checked: Approved, click: $root.ElementApprovalChanged' />
</div>
</div>
</div>
</div>
What is happening is on checkbox click I send request to webservice and if this call returns false I want to reset element's Approved flag. And even whithout that, selecting checkbox should change div class attribute to mark it as NotApproved when needed. But none of this happens.
An observableArray only tracks the array. So if something is added, removed or replaced in the array this will trigger an update to your view.
An observableArray does NOT track the state of individual properties on the items in the array. So if you have an Approved flag on your objects this needs to be an observable for the UI to reflect changes to that property.
So you would have something like:
element.Approved = ko.observable(false);
....
....
if (!result) {
var negatedApproved = !element.Approved();
element.Approved(negatedApproved);
}
(or if you want to be more consise:
element.Approved(!element.Approved());
)

Adding a 'target' attribute to the URL of a tabstrip item in Telerik MVC extensions

I am replacing this 'hand-rolled' tabstrip with the Telerik Extensions Tabstrip below it, but I can't fathom how to get the Tabstrip to include the target attribute in the URL for each item. How can I achieve this?
Before:
<ol>
#foreach (var sheet in Model.Sheets)
{
<li>
#sheet.Name</li>
}
</ol>
After:
#Html.Telerik().TabStrip().Name("Card").BindTo(Model.Sheets, (item, tabInfo) =>
{
item.Text = tabInfo.Name;
item.Url = Url.Content(Server.MapUrl(tabInfo.FilePath));
})
You could use the LinkHtmlAttributes property to set additional html attributes:
item.LinkHtmlAttributes = new Dictionary<string, object>
{
{ "target", "selected-worksheet" }
};
Actually I've never used Telerik, so I am not quite sure if you have to instantiate a new dictionary or simply add a key (in case the property is automatically instantiated):
item.LinkHtmlAttributes["target"] = "selected-worksheet";
I think I had a similar problem with the Telerik TreeView and solved it via a detour with jQuery.
My problem was, that it didn't work to pass any (Link)HtmlAttributes, to the TreeViewItems in the View. I tried to add several HtmlAttributes to the TreeViewItem in the Controller: (e.g. one attribute I wanted to add to the < a > element):
newNodes[i].HtmlAttributes.Add("data-ajax-update","#main");
, but after return as JsonResult of the Ajax-Request, all except .Text, .Value, .Enabled, .LoadOnDemand, .Url were then empty in the View.
I found a sensible answer that these elements are not getting serialized in the Telerik forum:
http://www.telerik.com/community/forums/aspnet-mvc/treeview/target-blank-on-treeviewitem-url.aspx#1548458
I solved it then, with adding a special
<span class="treeViewItemAddAjaxLocation"></span>
element to the .Text of the TreeViewItem. Locating these elements then via jQuery in the View and adding the desired html-attributes to the < a > element.
Telerik TreeView Binding method of the 2nd and 3rd stage elements via Ajax:
.DataBinding(databinding => databinding.Ajax().Select("_AjaxLoading", "Menu"))
Controller snippet in Action "_AjaxLoading":
IList<TreeViewItem> newNodes = new List<TreeViewItem>();
foreach (RevisionEntity revision in revisions)
{
newNodes.Add(new TreeViewItem()
{
Text = "Node Name" + "<span class='treeViewItemAddAjaxLocation'></span>", // name + locator element
Value = revision.ID.ToString() + ";RevisionEntity",
Encoded = false,
Enabled = true,
LoadOnDemand = false,
Url("/Menu/NavigateToRevisionDetails"), // creates an < a > element
});
}
return new JsonResult { Data = newNodes };
View:
<div class="t-bot">
<a class="t-link t-in" href="/Menu/NavigateToRevisionDetails" data-ajax-update="#main" data-ajax-mode="replace" data-ajax-method="GET" data-ajax="true">Node Name
<span class="treeViewItemAddAjaxLocation"></span>
</a>
<input class="t-input" type="hidden" value="774336a5-c6eb-42cc-905a-4d215c957fa2;RevisionEntity" name="itemValue">
</div>
function TreeView_onLoad(e) {
var treeView = $(this).data("tTreeView");
$(".treeViewItemAddAjaxLocation", treeView.element)
.each(function () {
var $this = $(this); // put it in jQuery constr
item = $this.closest(".t-link"); // take the a
item.attr('data-ajax-update', '#main');
item.attr('data-ajax-mode', 'replace');
item.attr('data-ajax-method', 'GET');
item.attr('data-ajax', 'true');
$this.remove(); // removes the span element
});
}
Result TreeView element:
<div class="t-bot">
<a class="t-link t-in" href="/Menu/NavigateToRevisionDetails" data-ajax-update="#main" data-ajax-mode="replace" data-ajax-method="GET" data-ajax="true">Node Name</a>
<input class="t-input" type="hidden" value="774336a5-c6eb-42cc-905a-4d215c957fa2;RevisionEntity" name="itemValue">
</div>
Loads the PartialView invoked through "NavigateToRevisionDetails" Action via Ajax in the #main html-element and the rest of the page doesn't get refreshed!

Resources