I'm just starting out with backbone / grails and i've been struggling to figure out how to get everything to work.
I'm building a pricing configurator where a user selects a product type from radio group A and radio group B containing the quantity / pricing / discount data will do an ajax call to the backend for updated pricing data. I do not want to expose my pricing algorithm to the front end, so I was thinking I would use backbone to handle my ajax request / template.
I do not want to fully rely on js to create my UI, so on the initial page load, I'll build the gsp view with grails. Only problem I've noticed was my gsp view was being replaced by my handlebars template on initial page load. I guess this is fine, except it does two identical queries which isn't optimal.
Anyhow my code that does not seem to be working.
<script id="priceTemplate" type="text/x-handlebars-template">
<tr>
<td><input type="radio" value="" name="quantity">{{quantity}}</td>
<td class="price"><span>{{price}}</span></td>
<td class="discount"><span>{{discount}}</span></td>
</tr>
</script>
<asset:javascript src="bb_product/config.js"/>
<script>
var prices = new models.PriceList([],{productId:${productInstance.id}});
var priceView = new PriceView({collection: prices});
prices.fetch();
</script>
Models
var models = {};
models.PriceModel = Backbone.Model.extend({
//Is the model automatically populated from the collections json response?
})
models.PriceList = Backbone.Collection.extend({
initialize: function(models, options) {
this.productId = options.productId;
},
model: models.PriceModel,
url: function() {
return '../product/pricing/' + this.productId + '.json'
}
});
View
var PriceView = Backbone.View.extend({
el: '#product-quantities',
template: Handlebars.compile($("#priceTemplate").html()),
initialize: function(){
this.render();
},
render: function() {
console.log('collection ' + this.collection.toJSON()) //comes back empty
this.$el.html( this.template(this.collection.toJSON()));
}
});
json returned from url
[{"id":1,"quantity":10,"price":"10","discount":"10"},{"id":2,"quantity":50,"price":"20","discount"
:"10"}]
To initially get this up and working, what am I missing to display all items in the json object?
I've also see this code around, not sure what it does this.listenTo(this.collection, 'reset', this.render);
The reason you don't see any items is that the items aren't actually in the collection until after the view is rendered. Look at these two lines of code:
var priceView = new PriceView({collection: prices});
prices.fetch();
The first line renders the view (since you're calling render from within initialize). However, at that time, the prices collection is empty. Then, the second line fetches the data from the server and loads it into the collection; but by that time, the view has been rendered.
That last line of code you posted is the key to fixing this:
this.listenTo(this.collection, 'reset', this.render);
Usually, you'll put this inside the initialize function in your view class. What this does is "listen" to the collection instance, and when the reset event occurs, it will call the this.render function. (Of course, the method this.listenTo can "listen" to other objects for other events; see more details in the Backbone documentation).
If you add that line to the view's initialize function, the view will re-render whenever a "reset" event happens on the collection.
HOWEVER, by default, the "reset" event happens when all the models in the collection are replaced with another set of models, and this doesn't happen by default when you call a collection's fetch method (instead, the collection will try to "smart-update"). To force a reset of the collection when using fetch, pass {reset: true} as a parameter:
prices.fetch({reset: true});
Related
I'm having trouble with a knockout model that is not binding on a subscribed update. I have a C# MVC page that delivers a model to the template which is parsed to Json and delivered raw as part of a ViewModel assignment for ko.applyBindings. I have a subscription to an observable that calls a method to perform an update of the viewModel's data. Irrelevant stuff pulled out and renamed for example usage:
var myViewModel = function (data) {
var self = this;
self.CurrentPage = ko.observable();
self.SomeComplexArray= ko.observableArray([]);
self.Pager().CurrentPage.subscribe(function (newPage) {
self.UpdateMyViewModel(newPage);
});
self.UpdateMyViewModel= function (newPage) {
var postData = { PageNumber: newPage };
$.post('/Article/GetMyModelSearchByPage', postData, function (data) {
ko.mapping.fromJS(data, {}, self);;
});
};
When I perform logging, I can see all of the data, and it all looks correct. The same method is used to produce both the initial model and the updated model. I've used this technique on other pages and it worked flawlessly each time. In this case however, I'm looking for it to bind/update SomeComplexArray, and that's just not happening. If I attempt to do it manually, I don't get a proper bind on the array I get blank. I'm wondering if there is something obvious that I'm doing wrong that I'm just flat out missing.
Edit: I don't know that ko.mapping can be pointed to as the culprit. Standard model changes are also not affecting the interface. Here is something that is not working in a bound sense. I have a p element with visible bound to the length of the array and a div element with a click bound to a function that pops items off of SomeComplexArray. I can see in the console log that it is performing its function (and subsequent clicks result in 'undefined' not having that function). However, the p element never displays. The initial array has only 2 items so a single click empties it:
<p data-bind="visible: SomeComplexArray().length === 0">nothing found</p>
<div data-bind="click: function() { UpdateArray(); }">try it manually</div>
-- in js model
self.UpdateArray = function () {
console.log(self.SomeComplexArray());
console.log(self.SomeComplexArray().pop());
console.log(self.SomeComplexArray());
console.log(self.SomeComplexArray().pop());
console.log(self.SomeComplexArray());
});
Edit 2: from the comment #Matt Burland, I've modified how the pop is called and the manual method now works to modify the elements dynamically. However, the ko.mapping is still not functioning as I would expect. In a test, I did a console.log of a specific row before calling ko.mapping and after. No change was made to the observableArray.
I created a test of your knockout situation in JSFiddle.
You have to call your array function without paranthesis. I tested this part:
self.UpdateArray = function () {
self.SomeComplexArray.pop();
};
It seems to be working on JSFiddle side.
I'm not really sure why, but it would seem that ko.mapping is having difficulty remapping the viewmodel at all. Since none of the fields are being mapped into self my assumption is that there is an exception occurring somewhere that ko.mapping is simply swallowing or it is not being reported for some other reason. Given that I could manually manipulate the array with a helpful tip from #MattBurland, I decided to backtrack a bit and update only the elements that needed to change directly on the data load. I ended up creating an Init function for my viewModel and using ko.mapping to populate the items directly there:
self.Init = function (jsonData) {
self.CurrentPage(0);
self.Items(ko.mapping.fromJS(jsonData.Items)());
self.TotalItems(jsonData.TotalItems);
// More stuff below here not relevant to question
}
The primary difference here is that the ko.mapping.fromJS result needed to be called as a function before the observableArray would recognize it as such. Given that this worked and that my controller would be providing an identical object back during the AJAX request, it was almost copy/past:
self.UpdateMyViewModel= function (newPage) {
var postData = { PageNumber: newPage };
$.post('/Article/GetMyModelSearchByPage', postData, function (data) {
self.Items(ko.mapping.fromJS(JSON.parse(data).Items)());
});
};
This is probably not ideal for most situations, but since there is not a large manipulation of the viewModel occurring during the update this provides a working solution. I would still like to know why ko.mapping would not remap the viewModel at the top level, but in retrospect it probably would have been a disaster anyway since there was "modified" data in the viewModel that the server would have had to replace. This solution is quick and simple enough.
I have been looking for methods on how to disable a jqGrid and I found some:
Using BlockUI plugin: http://jquery.malsup.com/block/
Using jqGrid options: loadui and set it to 'block'
First option is a great solution (I have not tried yet) and it is clearer maybe but I want to avoid using plugins if I can whenever I can do it by setting object properties so I am trying the second option but it is not working for me, jqGrid continues enabled.
My jqgrid in my asp.net mvc 4 view:
<div id="jqGrid">
#Html.Partial("../Grids/_PartialGrid")
</div>
and _PartialGrid:
<table id="_compGrid" cellpadding="0" cellspacing="0">
</table>
<div id="_compPager" style="text-align: center;">
</div>
so in the view, in script section I perform below on document ready and depending on the status of a property in my model (I disable it if id>0, otherwise I enable it on page reload):
#section scripts
{
#Content.Script("/Grids/CompGrid.js", Url) // Content is a helper javascript loader (see end of this post)
}
<script type="text/javascript">
$(document).ready(function () {
showGrid();
var disableCompGrid = #Html.Raw(Json.Encode(Model.ItemCompViewModel));
setStatusCompGrid(disableCompGrid.id > 0);
}
</script>
CompGrid.js is:
function showGrid() {
$('#_compGrid').jqGrid({
caption: paramFromView.Caption,
colNames: ....
}
function setStatusCompGrid(disabled) {
$('#_compGrid').jqGrid({
loadui: 'block',
loadtext: 'Processing...'
});
}
In the code above, also I have tried to pass as parameter disabled to showGrid function and depending on if it is true or false to set a variable to 'block' or 'enable' respectively and then setting loadui property with this variable but it is not working.
Content.cshtml:
#using System.Web.Mvc;
#helper Script(string scriptName, UrlHelper url)
{
<script src="#url.Content(string.Format("~/Scripts/{0}", scriptName))" type="text/javascript"></script>
}
Any ideas?
It's important to understand that the call $('#_compGrid').jqGrid({...}); converts initial empty <table id="_compGrid"></table> element to relatively complex structure of dives and tables. So you can do such call only once. Such call creates and initialize the grid. In other words the function showGrid has bad name. The function can be called only once. The second call of it will test that the grid already exist and it will do nothing. If you need to change some parameters of existing grid you can use setGridParam method.
In the case you can use absolutely another solution to block the grid. After the call $('#_compGrid').jqGrid({...}); the DOM element of the initial table get some expandos - new property or method. For example $('#_compGrid')[0] will contains grid property which contains beginReq and endReq methods. So you can first create the grid (in the showGrid function) and include options loadui: 'block' and loadtext: 'Processing...' in the list of options which you use. Then if you need to block the grid later you can use
$('#_compGrid')[0].grid.beginReq();
and the code
$('#_compGrid')[0].grid.endReq();
to remove blocking. See the demo which demonstrates this. Alternatively you can show overlays created by jqGrid manually like I described in the answer. The code will be simple enough:
var gridId = "_compGrid"; // id of the grid
...
$("#lui_" + gridId).show();
$("#load_" + gridId).text("Processing...").show();
to show the overlay and
$("#lui_" + gridId).hide();
$("#load_" + gridId).hide();
to hide the overlay. See another demo which works exactly like the first one.
you don't need any plugin. Just add/remove css:
.disabled {
pointer-events: none;
//optional
opacity: 0.4;
}
DEMO
As the title suggests, my main objective is to render a dynamic scss(.erb) file after an ajax call.
assets/javascripts/header.js
// onChange of a checkbox, a database boolean field should be toggled via AJAX
$( document ).ready(function() {
$('input[class=collection_cb]').change(function() {
// get the id of the item
var collection_id = $(this).parent().attr("data-collection-id");
// show a loading animation
$("#coll-loading").removeClass("vhidden");
// AJAX call
$.ajax({
type : 'PUT',
url : "/collections/" + collection_id + "/toggle",
success : function() {
// removal of loading animation, a bit delayed, as it would be too fast otherwise
setTimeout(function() {
$("#coll_loading").addClass("vhidden");
}, 300);
},
});
});
});
controller/collections_controller.rb
def toggle
# safety measure to check if the user changes his collection
if current_user.id == Collection.find(params[:id]).user_id
collection = Collection.find(params[:id])
# toggle the collection
collection.toggle! :auto_add_item
else
# redirect the user to error page, alert page
end
render :nothing => true
end
All worked very smooth when I solely toggled the database object.
Now I wanted to add some extra spices and change the CSS of my 50+ li's accordingly to the currently selected collections of the user.
My desired CSS looks like this, it checks li elements if they belong to the collections and give them a border color if so.
ul#list > li[data-collections~='8'][data-collections~='2']
{
border-color: #ff2900;
}
I added this to my controller to generate the []-conditions:
def toggle
# .
# .
# toggle function
# return the currently selected collection ids in the [data-collections]-format
#active_collections = ""
c_ids = current_user.collections.where(:auto_add_item => true).pluck('collections.id')
if c_ids.size != 0
c_ids.each { |id| #active_collections += "[data-collections~='#{id}']" }
end
# this is what gets retrieved
# #active_collections => [data-collections~='8'][data-collections~='2']
end
now I need a way to put those brackets in a scss file that gets generated dynamically.
I tried adding:
respond_to do |format|
format.css
end
to my controller, having the file views/collections/toggle.css.erb
ul#list<%= raw active_collections %> > li<%= raw active_collections %> {
border-color: #ff2900;
}
It didn't work, another way was rendering the css file from my controller, and then passing it to a view as described by Manuel Meurer
Did I mess up with the file names? Like using css instead of scss? Do you have any ideas how I should proceed?
Thanks for your help!
Why dynamic CSS? - reasoning
I know that this should normally happen by adding classes via JavaScript. My reasoning to why I need a dynamic css is that when the user decides to change the selected collections, he does this very concentrated. Something like 4 calls in 3 seconds, then a 5 minutes pause, then 5 calls in 4 seconds. The JavaScript would simply take too long to loop through the 50+ li's after every call.
UPDATE
As it turns out, JavaScript was very fast at handling my "long" list... Thanks y'all for pointing out the errors in my thinking!
In my opinion, the problem you've got isn't to do with CSS; it's to do with how your system works
CSS is loaded static (from the http request), which means when the page is rendered, it will not update if you change the CSS files on the server
JS is client side and is designed to interact with rendered HTML elements (through the DOM). This means that JS by its nature is dynamic, and is why we can use it with technologies like Ajax to change parts of the page
Here's where I think your problem comes in...
Your JS call is not reloading the page, which means the CSS stays static. There is currently no way to reload the CSS and have them render without refreshing (sending an HTTP request). This means that any updating you do with JS will have to include per-loaded CSS
As per the comments to your OP, you should really look at updating the classes of your list elements. If you use something like this it should work instantaneously:
$('li').addClass('new');
Hope this helps?
If I understood your feature correctly, actually all you need can be realized by JavaScript simply, no need for any hack.
Let me organize your feature at first
Given an user visiting the page
When he checks a checkbox
He will see a loading sign which implies this is an interaction with server
When the loading sign stopped
He will see the row(or 'li") he checked has a border which implies his action has been accepted by server
Then comes the solution. For readability I will simplify your loading sign code into named functions instead of real code.
$(document).ready(function() {
$('input[class=collection_cb]').change(function() {
// Use a variable to store parent of current scope for using later
var $parent = $(this).parent();
// get the id of the item
var collection_id = $parent.attr("data-collection-id");
show_loading_sign();
// AJAX call
$.ajax({
type : 'PUT',
url : "/collections/" + collection_id + "/toggle",
success : function() {
// This is the effect you need.
$parent.addClass('green_color_border');
},
error: function() {
$parent.addClass('red_color_border');
},
complete: function() {
close_loading_sign(); /*Close the sign no matter success or error*/
}
});
});
});
Let me know if my understanding of feature is correct and if this could solve the problem.
What if, when the user toggles a collection selection, you use jquery change one class on the ul and then define static styles based on that?
For example, your original markup might be:
ul#list.no_selection
li.collection8.collection2
li.collection1
And your css would have, statically:
ul.collection1 li.collection1,
ul.collection2 li.collection2,
...
ul.collection8 li.collection8 {
border-color: #ff2900;
}
So by default, there wouldn't be a border. But if the user selects collection 8, your jquery would do:
$('ul#list').addClass('collection8')
and voila, border around the li that's in collection8-- without looping over all the lis in javascript and without loading a stylesheet dynamically.
What do you think, would this work in your case?
I have a table with a bunch of items. Each row can be clicked (on any columns in this row) and navigate to another page based on the clicked row. I have a javascript file attached to this page where I manage events (clicked row, ...).
In my ASP.NET MVC solution, I would like to choose a goo way to manage this scenario.
Below is the one I think:
My view:
<table>
<tbody>
#foreach (var item in Model)
{
<tr data-url="#Url.Action("General", "Transport", new { transportID = item.TransportID })">
<td>aaaa</td>
<td>bbbb</td>
<td>cccc</td>
</tr>
}
</tbody>
</table>
My js file:
$("table.search-transport tbody tr td:not(:first-child)").live("click", function () {
var url = $(this).parents('tr').data('url');
window.location.href = url;
});
Because we cannot inject server code inside javascript files, I inject the url directly inside each rows inside my view. When a row is clicked, I get this "specific" url and navigate to it from my javascript file.
I would like to know if this solution is steady enough in an MVC project.
Following up from the comments, Nikola is right in saying that this does not impact the usage of the ASP.NET MVC framework. Injecting the URL into a data field in your case is the only option as far as I can see it (but I am open to other suggestions).
For the js part, I would use event delegation to avoid attaching events to every single cell:
$("table.search-transport tbody tr").on("click", "td:not(:first-child)", function () {
var url = $(this).parents('tr').data('url');
// also: $(this).parent().data('url'); will work...
window.location.href = url;
});
This will attach the event to each row but fire only when you click on a td which is not the first child. Alternatively, you could attach the whole event once to the table, thus also having the option of adding rows dynamically and still having the event attached; like this:
$("table.search-transport").on("click", "tr td:not(:first-child)", function () {
[...]
});
I've been trying to create a custom binding for updating flot charts and it seems like it works when it first loads, but as I navigate around it quits.
Here's the scenario, I have a list view on one page (this is in jQuery Mobile) with little thumb nails of graphs, next to the graph is a slider that is bound to a property of the same view model that causes the graph points to be recalculated. When you click on one of the list items, it moves to another page that shows a much larger version of the graph and lets you change the value by typing in a textbox (later, you'll be able to click directly on the graph). The binding looks something like this:
ko.bindingHandlers["plot"] = {
init: function (element, valueAccessor, allBindingsAccessor) {
var qe = $(element);
var page = qe.closest("div[data-role='page']");
page.bind("pageshow", function () {
ko.bindingHandlers["plot"].update(element, valueAccessor);
});
},
update: function (element, valueAccessor, allBindingsAccessor) {
var qe = $(element);
var page = qe.closest("div[data-role='page']");
var curr = $.mobile.activePage;
var val = ko.utils.unwrapObservable(valueAccessor());
var data = val.plotData();
if(data && page.prop("id") == curr.prop("id")) {
var marker = val.markerData();
var opt = val.chartOptions();
opt.yaxis.show = opt.xaxis.show = !qe.hasClass("graphThumb");
marker.points.radius = opt.yaxis.show ? 5 : 3.5;
$.plot(qe, [
data,
marker
], opt);
}
}
};
The init handler sets it up to draw the graph on a page show because flot doesn't work right when drawing to a non-visible div. The update will check if the currently displayed page is the same as the one with the binding and redraw the graph as required.
For the graphs in the list view, they are immediately draw by the update method and work correctly. However, for the initially hidden pages, the function to draw the graph fires, the graph draws, but the updates will no longer work. Then, worse, when you go back to the initial page, the function bound to the pageshow event fires, redraws the graphs, but now they've also quit updating.
The view model looks something like this:
var viewModel = (function () {
this.current = ko.observable(0);
this.plotData = ko.computed(function () {
var points = [];
// a bunch of calculations that depend on the value of current of this and other viewModels in a collection
return points;
}
}
I can stick a break point in the computed plotData and see that it is getting update correctly. It just that those updates aren't trigger the binding handler.
The HTML binding looks something like this:
<!-- the first, visible page -->
<div data-role="page" id="index">
<ul data-role="listview" data-bind="foreach: factors">
<li data-bind="attr: {id: listId}">
<a data-bind="attr: {href: idLink}">
<div class="graphThumb" data-bind="plot: $data"></div>
</a>
</li>
</ul>
</div>
<!-- hidden details pages -->
<!-- ko foreach: factors -->
<div data-role="page" data-bind="attr: { id: id }">
<div class="graphPlaceHolder" data-bind="plot: $data"></div>
</div>
<!-- /ko -->
Update: I changed my binding slightly because I realized that I can just call the update on the pageshow event handler, which simplifies things, but doesn't fix the problem. It seems that doing that won't make knockout update it's dependencies for the binding.
Update: another update, assigning val.plotData() to a variable didn't work, neither did including it in my if statement. However, I have another computed observable that depends on the the current value and another property of the parent view model that I could retrieve and add to my if statement that works! However, my solution is probably to specific to be generally useful. The short story is that knockout will reassess the dependencies of a binding with each update, so you need to make sure that it is evaluating something important regardless of any conditional logic or it will stop updating.
So I can wrap up and mark this question as answered, I will briefly summarize my experience.
Custom binding are implemented the same was as computed properties in Knockout (according to the KO docs), and one thing that computed properties do is reassess which properties they are dependent on every time they are executed. What this means, is if you have a conditional in computed property (or a custom binding), only the properties accessed in the branch of the condition that actually gets executed will be monitored for changes by knockout. So, for example, if you have a property like this:
var myComputedProperty = ko.computed(function() {
if(this.myBool()) {
$("#someElement").text(this.foo());
}
else {
$("#someElement").text(this.bar());
}
});
KO will keep track of the value of myBool and recalculate the property if it changes, but, if myBool is true, it will also track foo, if myBool is false, it will also track bar, but it won't track both - because it doesn't need to. Most of the time this works just fine.
In my case it failed because I had a conditional that wasn't part of the view model (and therefore wasn't observable) and I needed it to keep track of the view model properties regardless of whether or not the condition evaluated to true or false. So I had something that looked like this:
if(page.prop("id") == curr.prop("id")) {
$("#someElement").text(this.foo());
}
Here the comparison is between the id of the page that the binding lives on and the $.mobile.activePage provided by jQuery Mobile (and, obviously not observable). If those id match, then knockout will update the binding when foo changes. However, if they don't, then knockout will lose the dependency on foo and even if the id do match at some later time, it will have lost the dependency and won't reevaluate when foo changes.
The way around this is to ensure that any properties that need to be tracked are evaluated regardless of the condition. So, something like this should solve the general case:
if(this.foo() && page.prop("id") == curr.prop("id")) {
$("#someElement").text(this.foo());
}
As to why I needed the condition at all is because flot gets very confused when it tries to draw a graph to a non-visible div, so I need to skip drawing the graph when it wasn't the current page.