UI saying my observableArray().length == 0 one minute, then == 1 the next? - asp.net-mvc

I am using knockout.js. I declare 2 model objects:
1)
var model =
{
products: ko.observableArray([])
}
2)
var customerModel =
{
cart: ko.observableArray([]),
filteredProducts: ko.observableArray([]),
currentView: ko.observable("list")
}
when the document is ready:
$(document).ready
(
function () {
getProducts();
}
);
it calls getProducts which sets the model.products observable array to have a single object:
var getProducts = function () {
model.products.removeAll();
model.products.push
(
{
Id: 1,
Name: "Product 1",
Description: "A nice product",
Price: 666.66,
Category: "Category 1"
}
)
}
i have a view that renders an ASP.NET partial view. the default customerModel.currentView is 'list' (as seen from the model object i declared):
<div data-bind="visible: customerModel.currentView() == 'list'">
#Html.Partial("ProductList")
</div>
in the partial view (ProductList.cshtml) is the following code:
<div data-bind="foreach: model.products()">
<span data-bind="text: $data.Description"></span>
<button data-bind="click: addToCart">Add to Cart</button>
</div>
clicking on the button invokes the addToCart function which adds a product to the cart and sets the view:
var addToCart = function (product) {
var cart = customerModel.cart;
cart().push
(
{
product: product,
count: 1
}
);
setView('cart');
}
setView is like this:
var setView = function (view) {
customerModel.currentView(view);
}
when set to 'cart', the following code renders a partial view:
<div data-bind="visible: customerModel.currentView() == 'cart'">
#Html.Partial("ProductCart")
</div>
now HERE is the kicker. ProductCart.cshtml looks like this:
<script>
var testCart = function () {
alert("There is " + customerModel.cart().length + " item incustomerModel.cart()");
}
</script>
<div data-bind="if: customerModel.cart().length == 0">There are no items in customerModel.cart( </div>
<button data-bind="click: testCart">Test Cart</button>
so guess what? the div that says "There are no items in customerModel" shows up because customerModel.cart().length == 0, however when i click the button the alert tells me that customerModel.cart().length is equal to 1.

It seems that you need to change cart().push to cart.push, as you are doing with model.products.
Thats how observableArrays work. See documentation.
When you call cart(), it returns a new array, with the values inside the observable. You are pushing your new value to that array, which is being lost in limbo.
You need to call the push method through the ko.observableArray API, this way it will notify all subscribers for that change, and the new value will be added to the observableArray.

Related

how to disabled asp-action link after one click

i would like to disabled asp=action link after one click
i have a foreach loop to display my book in table in my view
my asp-action for add book in the cart is in the loop but i want to disabled just the book who already in the cart not all books
<a asp-action="AcheterLivre" asp-Controller="Achat" asp-route-id="#item.Isbn" id="disabled" onclick="return myFunction(this);"> Ajouter
i try something with jquery but its dont work well
i tried this
<script>
function myFunction() {
$("#disabled").one("click", function () {
alert("this book is already in the cart");
});
}
i have this function in my class
its verify if the books is in the cart maybe i should use it ?
public bool IsPresent(string isbn)
{
//rechercher si l'isbn correspond a un livre de la liste
AchatLivreViewModel livre = ListeElements.Find(element => element.Isbn == isbn);
if (livre != null)
{
return true;
}
else
{
return false;
}
}
Why not trying this simple approach:
<tbody>
#foreach (var item in Model)
{
<tr>
<td>
#item.Isbn
</td>
<td>
#item.Titre
</td>
<td>
<label class="btn btn-primary" style="padding:0">Add to Cart</label>
</td>
<td>
<label class="btn btn-danger" style="padding:0">Remove From Cart</label>
</td>
</tr>
}
</tbody>
And in your javascript, if you don't want to use Ajax, you can manage your cart items all on client side using an array of objects, Let's name it CartItems:
var CartItems = [];
$('.ADD2CART').click(function () {
if ($(this).closest('tr').hasClass("ExistingInCart")) {
alert('Already in Cart !!');
}
else {
// add this item to the Cart through Ajax or
// local javascript object: e.g.:
CartItems.push({
ISBN: $(this).closest('tr').find('td:eq(0)').text().trim(),
Title: $(this).closest('tr').find('td:eq(1)').text().trim(),
});
$(this).closest('tr').addClass("ExistingInCart");
}
return false; //to prevent <a> from navigating to another address
});
$('.RemoveFromCART').click(function () {
$(this).closest('tr').removeClass("ExistingInCart");
var isbn = $(this).closest('tr').find('td:eq(0)').text().trim();
CartItems = CartItems.filter(x => x.ISBN !== isbn);
return false;
});
Once you need to submit or post the page, you have all the already selected books in CartItems array.
To add this javascript code to your view, choose one of the options:
Put this block at the bottom of your view and copy the above script inside the <script></script> tag:
#section scripts{
<script>
.... copy it here ...
</script>
}
copy the script code inside a newFile.js and add it to your view
<script src="~/Scripts/jquery-3.3.1.min.js"></script>
<script src="~/Scripts/newFile.js"></script>
You may decide to bundle this newFile.js
try this:
Ajouter
And your Javascript:
function foo(input) {
if ($(input).attr('yetClickable') === '1') {
$(input).attr('yetClickable', '0');
return true;
}
else {
// this false returning will counteract the effect of click event on the anchor tag
return false;
}
}
Once an Item is removed from the cart, again you need javascript to select that Item by its Id and change the yetClickable attribute back to 1 (in order to be clickable).
Note: This idea above (upon your scenario) works until the page is not reloaded. Otherwise, you need to handle ADD/Remove operations on the Cart through Ajax.
Hope this helps.

knockout.js: how to make a dependent cascading dropdown unconditionally visible?

Getting started with knockout, I have been playing with the pattern found at http://knockoutjs.com/examples/cartEditor.html. I have cascading select menus where the second one's options depend on the state of the first -- no problem so far. But whatever I do, I haven't figured a way to change the out-of-the-box behavior whereby the second element is not visible -- not rendered, I would imagine -- until the first element has a true-ish value (except by taking out the optionsCaption and instead stuffing in an empty record at the top of my data -- more on that below.) The markup:
<div id="test" class="border">
<div class="form-row form-group">
<label class="col-form-label col-md-3 text-right pr-2">
language
</label>
<div class="col-md-9">
<select class="form-control" name="language"
data-bind="options: roster,
optionsText: 'language',
optionsCaption: '',
value: language">
</select>
</div>
</div>
<div class="form-row form-group">
<label class="col-form-label col-md-3 text-right pr-2">
interpreter
</label>
<div class="col-md-9" data-bind="with: language">
<select class="form-control" name="interpreter"
data-bind="options: interpreters,
optionsText : 'name',
optionsCaption: '',
value: $parent.interpreter"
</select>
</div>
</div>
</div>
Code:
function Thing() {
var self = this;
self.language = ko.observable();
self.interpreter = ko.observable();
self.language.subscribe(function() {
self.interpreter(undefined);
});
};
ko.applyBindings(new Thing());
my sample data:
roster = [
{ "language": "Spanish",
"interpreters": [
{"name" : "Paula"},
{"name" : "David"},
{"name" : "Humberto"},
{"name" : "Erika"},
{"name" : "Francisco"},
]
},
{"language":"Russian",
"interpreters":[{"name":"Yana"},{"name":"Boris"}]
},
{"language":"Foochow",
"interpreters":[{"name":"Lily"},{"name":"Patsy"}]
},
/* etc */
Now, I did figure out that I can hack around this and get the desired effect by putting
{ "language":"", "interpreters":[] }
at the front of my roster data structure, and that's what I guess I will do unless one of you cognoscenti can show me the more elegant way that I am overlooking.
After using both Knockout and Vuejs, I found Vuejs much easier to work with. Knockout is a bit out dated and no longer supported by any one or group.
Having said that, here is how I addressed your issue. The comments here refer to the link you provided not your code so I could build my own test case.
My working sample is at http://jsbin.com/gediwal/edit?js,console,output
I removed the optionsCaption from both select boxes.
Added the following item to the data (note that this has to be the first item in the arry):
{"products":{"name":"Waiting", "price":0}, "name":"Select..."},
I added the disable:isDisabled to the second selectbox cause I want it to be disabled when nothing is selected in the first selectbox.
added self.isDisabled = ko.observable(true); to the cartline model
altered the subscription to check the new value. If it is the select option the second one gets lock.
function formatCurrency(value) {
return "$" + value.toFixed(2);
}
var CartLine = function() {
var self = this;
// added this to enable/disable second select
self.isDisabled = ko.observable(true);
self.category = ko.observable();
self.product = ko.observable();
self.quantity = ko.observable(1);
self.subtotal = ko.computed(function() {
return self.product() ? self.product().price * parseInt("0" + self.quantity(), 10) : 0;
});
// Whenever the category changes, reset the product selection
// added the val argument. Its the new value whenever category lchanges.
self.category.subscribe(function(val) {
self.product(undefined);
// check to see if it should be disabled or not.
self.isDisabled("Select..." == val.name);
});
};
var Cart = function() {
// Stores an array of lines, and from these, can work out the grandTotal
var self = this;
self.lines = ko.observableArray([new CartLine()]); // Put one line in by default
self.grandTotal = ko.computed(function() {
var total = 0;
$.each(self.lines(), function() { total += this.subtotal() })
return total;
});
// Operations
self.addLine = function() { self.lines.push(new CartLine()) };
self.removeLine = function(line) { self.lines.remove(line) };
self.save = function() {
var dataToSave = $.map(self.lines(), function(line) {
return line.product() ? {
productName: line.product().name,
quantity: line.quantity()
} : undefined
});
alert("Could now send this to server: " + JSON.stringify(dataToSave));
};
};

KnockoutJS with MVC Partialview

I am having problems persisting data from KO observableArray in a MVC Partialview...
I have a side navigation menu where one of the nodes is dynamically built using KOjs 'foreach', and this navigation menu should persist across the site. The menu is rendered ok on the first View page but when I navigate on other page (clicking on menu option or any way) the dynamic node is empty, although the observableArrays were fetched and loaded with the correct data.
It seems like the navigaition menu (which is a partial view) is not refreshing/reloading to render the menu nodes/options again.
Any help si much appreciated. TIA!
self.reports = ko.observableArray([]);
self.reportCategories = ko.observableArray([]);
self.getReports = function () {
App.Server.get("/.../reports/")
.then(function (vm) {
self.reports(vm.reports);
self.reportCategories(vm.categories);
});
};
self.init = function () {
self.getReports();
};
self.init();
<li>
<a class="level1" data-toggle="collapse" href="#report-submenu" aria-expanded="false" aria-controls="collapseExample">Reports</a>
<ul class="level1 collapse" id="report-submenu" data-bind="foreach: reportCategories">
<li>
<a class="level2 collapse" data-toggle="collapse" data-bind="text: label, attr: { href: '#' + value }"
aria-expanded="false" aria-controls="collapseExample"></a>
<ul class="level2 collapse" data-bind="foreach: reports, attr: { id: value }">
<li data-bind="if: category == $parent.categoryId">
<a class="level3" data-bind="text: menuName, attr: { href: reportName }"></a>
</li>
</ul>
</li>
Here's the other part of the code (a Nancy GET method):
Get["/"] = _ =>
{
var reportModel = new ReportModel();
var reports = reportService.GetList();
if (reports != null)
{
// Categories
reportModel.Categories = reports.Select(s => s.Category).Distinct().Select(c => new ReportCategoryModel()
{
CategoryId = c,
Label = c.TrimStart("Reports/".ToCharArray()),
Value = c.ToLower().Replace('/', '-')
}).ToList();
// Reports
reportModel.Reports = reports.Select(r => new ReportRecord()
{
MenuName = r.MenuName,
ReportName = r.ReportName,
Category = r.Category,
}).ToList();
}
return Response.AsJson(reportModel);
};

Backbone.js View not showing in browser after render call

JS code below
Model
var EntryName = Backbone.Model.extend({
defaults : {
name : ""
},
});
Model Collection
var EntryNames = Backbone.Collection.extend({
model : EntryName,
initialize : function() {
}
});
ModelView
var EntryNameView = Backbone.View.extend({
tagName : 'li',
// Cache the template function for a single item.
entrynametpl : _.template('<li><a href="#" ></a></li>'),
// Re-render.
Render function
render : function() {
this.$el.html(this.entrynametpl(this.model.toJSON()));
return this;
},
});
ModelCollectionView
var EntryNamesView = Backbone.View.extend({
// tagName: "ul",
// className: "nav-search",
el : $('#entriestree'),
initialize : function() {
//this.template = _.template($('#entries-template').html());
_.bindAll(this, 'render');
},
Render function
render : function() {
var item, self = this;
//var template = $("#item-template");
this.collection.each(function(entry) {
item = new EntryNameView({
model : entry
});
self.$el.append(item.render().el);
});
console.log($(this.el));
return this;
}
});
Model Collection
Model Collection This is where and how i call render.
function onDeviceReady()
{
// console.log("Opening panel");
$("#nav-panel").panel( "open");
console.log("creating collection");
var donuts = new EntryNames();
donuts.reset([ {"name" : "Boston Cream"}, {"name" : "Lemon-Filled"}, {"name" : "Rusty Iron Shavings"}]);
console.log("created collection");
var donutCollectionView = new EntryNamesView({collection : donuts});
donutCollectionView.render();
$("#nav-panel" ).trigger( "updatelayout" );
}
Model Collection
The HTML code is below Model Collection
<body>
<div id="panel-fixed-page1" class="jqm-demos ui-responsive-panel"
data-url="panel-fixed-page1" data-role="page">
<div data-role="header" data-theme="f" data-position="fixed">
<div data-role="navbar" data-grid="d">
<ul>
<li><a class="ui-btn-active" href="#" data- theme="a">Entry</a></li>
<li>Addresses</li>
<li>Attachments</li>
<li>Delivery Collection</li>
</ul>
</div>
<!-- /navbar -->
Menu
</div>
<!-- /header -->
<div class="jqm-content" data-role="content">
</div>
<!-- /content -->
<div id="nav-panel" data-role="panel"
data-position-fixed="true">
<li><a href="#" data-rel="close" >INBOX</a></li>
<ul id="entriestree" class="nav-search" data-role="listview" data- theme="a">
</ul>
</div>
</div>
</body>
Model Collection
Model Collection
In your EntryNamesView.render, this is not pointing to the view, because it is inside each callback function scope. Try change it to use a self:
render : function() {
var item, self = this;
//var template = $("#item-template");
this.collection.each(function(entry) {
item = new EntryNameView({
model : entry
});
self.$el.append(item.render().el);
});
console.log($(this.el));
return this;
}
Your _.underscore template has no placeholder variables:
entrynametpl : _.template('<li><a href="#" ></a></li>') //renders empty li's
correct this to:
entrynametpl: _.template('<li><%= name %></li>')
okay i figured out the issue, I set the view's element "el" property outside of the constructor (the initialize function), so it was never getting set with the proper dom element because the dom element came after the script reference in the html file. once i moved the script import to the end of the page, voila life is good..silly mistakes cost you sooo much time!!!

Dynamically adding dropdowns to a form and validating them in ASP.NET MVC

I have a form with various inputs. I have a bunch of optional parameters that have some number of choices. I'd like to allow the user to select these optional parameters in the following way:
First, the user clicks the Add Component button at the bottom of the form and two new dropdowns appear above the button. The first dropdown has a list of Types that can be selected and the second one will be disabled. When the user selects a valid choice in the first dropdown, I want to populate the second dropdown with some Values that are specific to the specified Type. The user should be able to continue adding new Components (the pair of dropdowns) until all the desired optional Components are added. Ideally the form wouldn't be posted until all the fields have been filled out and the desired Components added.
My question is this: How do I design this so that when the form is submitted and there are errors, that the dynamically added fields (the Components) will remain on the page and display the correct values?
I was planning on having the Add Component button be an Ajax.ActionLink that retrieves a partialview:
<div id="divComponentHolder"></div>
<%= Ajax.ActionLink("Add a Component", "GetComponentSelector", new AjaxOptions { UpdateTargetId = "divComponentHolder", InsertionMode = InsertionMode.InsertAfter}) %>
This partial view would look something like this:
<%# Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<MVCAndWebFormsTest.Models.ComponentSelectorModel>" %>
<%= Html.Encode("Type:")%>
<%= Html.DropDownList("ComponentType", Model.ComponentTypes, "", new {onchange = "updateCompValues(this);"}) %>
<%= Html.Encode("File/Folder:")%>
<div id="selectdiv">
<% Html.RenderPartial("ComponentValueSelector", Model.ComponentValues); %>
</div>
<br/>
<script type="text/javascript" language="javascript">
function updateCompValues(obj) {
$.ajax({
url: <% Url.Action("GetCompValues") %>,
async: true,
type: 'POST',
data: { type: obj.value },
dataType: 'text',
success: function(data) { $("#selectdiv").html(data); },
error: function() {
console.log('Erreur');
}
});
}
</script>
And the ComponentValueSelector partial would be pretty simple:
<%# Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<MVCAndWebFormsTest.Controllers.ViewModels.ComponentValueModel>" %>
<%= Html.DropDownList("CompValue", Model.SelectList) %>
Take a look at submitting list in MVC, here are a few useful sites:
http://blogs.teamb.com/craigstuntz/2009/02/11/38013/
http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx
http://www.hanselman.com/blog/ASPNETWireFormatForModelBindingToArraysListsCollectionsDictionaries.aspx
This is useful for submitting your dynamic DOM you are building up.
Another way instead of making an ajax call to render a partial view you could always directly add elements to the DOM with jquery. For example use the jquery clone ( $('element').clone(); ) method that would copy your list boxes then do some regex to change the id's of the input boxes so they have unique id/names.
As you are passing through a List of these 'choices' to your controller, you would then have to set them back in your Model and have your View iterate through them to display the correct amount of choices added.
Here is a bare bones example. This may not be the best implementation for yourself or someone else may have better ideas.
View
<% for (int i = 0; i < in Model.Results.Count; i++) { %>
//render better HTML but you should get the point!
<%= Html.Hidden("choices[" + i + "].ID", i) %>
<%= Html.DropDownList("choices[" + i + "].Choice1", ...) %>
<%= Html.DropDownList("choices[" + i + "].Choice2", ...) %>
<% } %>
- add button
jQuery
$('#addButton').click(function()
{
//say if your choice drop downs were in a table then take the last
//row and clone it
var row = $('table tr:last').clone(true);
var newId = //work out the new id from how many rows in the table
//make sure to update the id and name parameters of inputs
//of the cloned row
row.find(':input')
.attr('id', function()
{
return $(this).attr('id').replace(/\[[\d+]\]/g, '[' + newlId + ']');
//this replaces the cloned [id] with a new id
})
.attr('name', function()
{
return $(this).attr('name').replace(/\[[\d+]\]/g, '[' + newId + ']');
});
row.find(':hidden').val(newId); //update the value of the hidden input
//alert(row.html()); //debug to check your cloned html is correct!
//TODO: setup ajax call for 1st drop down list to render 2nd drop down
$('table tr:last').after(row);//add the row
return false;
});
Controller
public ActionResult YourMethod(IList<YourObject> choices, any other parameters)
{
YourViewModel model = new YourViewModel();
model.Results = choices; //where Results is IList<YourObject>
return View(model);
}
Based on advice from David Liddle, I found a different design that was a bit more elegant. It uses more jQuery and fewer partial views and Ajax requests.
Instead of adding a bunch of DropDownLists, I decided to go with a table, a pair of dropdowns and an "Add" button. When the user selects a Type option in the first dropdown, ajax is still used to retrieve the partial view for populating the second Value dropdown. Once a Value option has been selected, the user then clicks the Add button.
Using jQuery, two hidden inputs are added to the page. The naming convention in the links from David are used to name these elements (comps[0].Type and comps[0].Value). Also, a new row is added to the table with the same Type and Value for visual feedback to the user showing what has been added.
I also defined a Component class that just has Type and Value properties and added a List to the Model. In the view, I iterate over this list and add all components in the Model to the table and as hidden inputs.
IndexView
<table id="componentTable">
<tr>
<th>Type</th>
<th>Deploy From</th>
</tr>
<% foreach (Component comp in Model.comps) { %>
<tr>
<td><%= Html.Encode(comp.Type) %></td>
<td><%= Html.Encode(comp.Value) %></td>
</tr>
<% } %>
</table>
<div id="hiddenComponentFields">
<% var index = 0;%>
<% foreach (Component comp in Model.comps) { %>
<input type="hidden" name="comps[<%= Html.Encode(index) %>].Type" value="<%= Html.Encode(comp.Type) %>" />
<input type="hidden" name="comps[<%= Html.Encode(index) %>].Value" value="<%= Html.Encode(comp.value) %>" />
<% index++; %>
<% } %>
</div>
<%= Html.DropDownList("ComponentTypeDropDown", Model.ComponentTypes, "", new { onchange = "updateCompValues();"}) %>
<span id="CompValueContainer">
<% Html.RenderPartial("ComponentValueSelector", new ComponentValueModel()); %>
</span>
<span class="button" id="addComponentButton" onclick="AddComponentButtonClicked()">Add the File</span>
<span id="componentStatus"></span>
ComponentValueSelector PartialView
<%# Control Language="C#" Inherits="ViewUserControl<ComponentValueModel>" %>
<% if(Model.SelectList == null) { %>
<select id="CompValue" name="CompValue" disabled="true">
<option></option>
</select>
<% } else { %>
<%= Html.DropDownList("CompValue", Model.SelectList, "") %>
<% } %>
jQuery
function updateCompValues() {
$.ajax({
url: '<%= Url.Action("GetComponentValues") %>',
async: true,
type: 'POST',
data: { type: $("#CompValue").value },
dataType: 'text',
success: function(data) { $("#CompValueContainer").html(data); enable($("#CompValue")) },
error: function() {
console.log('Erreur');
}
});
}
function AddComponentButtonClicked() {
UpdateCompStatus("info", "Updating...");
var type = $("#ComponentTypeDropDown").val();
var value = $("#CompValue").val();
if (type == "" || value == "") { // No values selected
UpdateCompStatus("warning", "* Please select both a type and a value");
return; // Don't add the component
}
AddComponent(type, value);
}
function AddComponent(type, setting_1) {
// Add hidden fields
var newIndex = GetLastCompsIndex() + 1;
var toAdd = '<input type="hidden" name="comps[' + newIndex + '].Type" value="' + type + '" />' +
'<input type="hidden" name="comps[' + newIndex + '].Setting_1" value="' + setting_1 + '" />';
$("#hiddenComponentFields").append(toAdd);
// Add to page
// Note: there will always be one row of headers so the selector should always work.
$('#componentTable tr:last').after('<tr><td>'+type+'</td><td>'+setting_1+'</td>remove</tr>');
}
function GetLastCompsIndex() {
// TODO
alert("GetLastCompsIndex unimplemented!");
// haven't figured this out yet but something like
// $("#hiddenComponentFields input:hidden" :last).useRegExToExtractIndexFromName(); :)
}
function UpdateCompStatus(level, message) {
var statusSpan = $("#componentStatus");
// TODO Change the class to reflect level (warning, info, error?, success?)
// statusSpan.addClassName(...)
statusSpan.html(message);
}
Controller
public ActionResult Index() {
SelectList compTypes = repos.GetAllComponentTypesAsSelectList();
return View(new IndexViewModel(compTypes));
}
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Index(Component[] comps, other params...) {
foreach(Component comp in comps) {
// Do something with comp.Type and comp.Value
}
return RedirectToAction(...);
}
public ActionResult GetComponentValues(string type) {
ComponentValueModel valueModel = new ComponentValueModel();
valueModel.SelectList = repos.GetAllComponentValuesForTypeAsSelectList(type);
return PartialView("ComponentValueSelector", valueModel);
}
IndexViewModel
public class IndexViewModel {
public List<Component> comps { get; set; }
public SelectList ComponentTypes { get; set; }
public IndexViewModel(SelectList types) {
comps = new List<Component>();
ComponentTypes = types;
}
}

Resources