Durandal List with Composition (ASP .NET MVC) - javascript

I am trying to use Durandal to generate a list of data. Because this data will be updated by a dialog I need the list item to represent a view model. For this, view composition seems appropriate, but I must be missing something with how to do this. Note: I am using ASP .NET MVC so my pathing is a bit different. However, at this point, I am not seeing any web requests for my view nor are the debugger; lines in my View Model being hit.
Here is the code:
The HTML:
<tbody data-bind="foreach: workItems">
<!-- ko compose: {model: 'viewmodels/project/workItem', view: 'views/project/workitem'} -->
<!-- /ko -->
</tbody>
Here is the JS for the view model intended to represent each line in the list:
define(['knockout'], function(ko) {
var vm = function () {
var that = this;
this.activate = function(ctx) {
debugger;
};
};
return vm;
});
Here is the code which handles the GET request returning the data:
this.activate = function (ctx) {
var q = null;
if (ctx == undefined) {
q = that._getCurrent();
} else {
q = that._getPath(ctx);
}
q.then(function (response) {
that.workItems(response);
app.trigger('viewmodel:loadingComplete');
});
};
I have tried using system.acquire so as to add VMs to my observable array, this failed to show any results. My thinking was that I need composition so I get the binding to fire for each line. (this may be wrong). I am really looking for an example of this working. Quite perplexed that I have not been able to find anything on SO or Google.
Thanks in advance

1) Check that workItems really does contain data. Use a simple div foreach to output the results.
2) Is there a reason you are not using the Singleton pattern?
3) Return a promise from the activate to make sure the data is loaded before starting the composition.
define(['knockout'], function (ko) {
var items = ko.observableArray([]);
var activate = function() {
console.log("calling composelist:activate");
items.push({name:"Hello"});
items.push({name:"world"});
};
return {
activate: activate,
items: items
};
});

Are you trying to load different v/vm based on a url parameter? If so why not use the basic shell /routes?
Can you submit your code?

Related

Backbone: Best way to handle variable common to all models

I'm currently developing my first Backbone single page app project and I'm facing an issue.
Basically I have a menu (html select input element) implemented as a View. Its value is used to control pretty much every other data requests since it specifies which kind of data to show in the other Views.
Right now I handle the DOM event and trigger a global event so that every model can catch it and keep track internally of the new value. That's because that value is then needed when requesting new data. But this doesn't look like a good solution because A) I end up writing the same function (event handler) in every model and B) I get several models with the same variable.
var Metrics = Backbone.Collection.extend({
url: "dummy-metrics.json",
model: MetricsItem,
initialize: function () {
this.metric = undefined;
},
setMetric: function (metric) {
this.metric = metric;
globalEvents.trigger("metric:change", this.get(metric));
}
});
var GlobalComplexity = Backbone.Collection.extend({
url: function () {
var url = "http://asd/global.json?metric=" + this.metric;
return url;
}, //"dummy-global.json",
model: GlobalComplexyItem,
initialize: function () {
this.metric = undefined;
this.listenTo(globalEvents, "metric:change", this.updateMetric);
},
updateMetric: function (metric) {
this.metric = metric.get("id");
this.fetch({ reset: true });
}
});
All my other Collections are structured like GlobalComplexity.
What's the cleanest way to solve this problem?
Thank you very much.
Define a global parametersManager. Export an instance (singleton) then require it when you need it.
On "globalupdate" you update the parametersManager then trigger "update" for all your model/collections so they'll look what are the current parameters in the parametersManager.

Load up multiple remote data sources for knockout.js data binding

I'm working on a knockout.js wizard and need to get data from multiple remote data sources (via AJAX) before I can properly render the drop-down menus in the wizard.
Additionally, there are 4 dropdowns and while #1 and #2 can be loaded up first, #3 and #4 depend on the choices selected in the first two.
So far I've experimented with using jQuery promises and also just nesting data calls and their associated callbacks, but are there any better ways to structure my view model code for the wizard?
Below is some of the data loading code. I'm happy to provide more if needed.
var postobj = {
id: workoutId
};
var getWorkout = $.post("./RowingWorkouts/GetWorkoutUsingId", postobj);
var getDiet = $.post("./Diet/GetDietUsingId", postobj);
var getFeedback = $.post("./RowingWorkouts/GetWorkoutFeedback", postobj);
// When all three are successful - I haven't gotten the when syntax to actually work yet
$.when(getWorkout, getDiet, getFeedback).done(function (workout, diet, feedback) {
//each of the parameter is an array
renderCharts(workout[0], diet[0], feedback[0])
// Here are more dropdowns that depend on the choices from the above ones
self.suggestedWorkouts = ko.observableArray();
// pseudo-code for data call for getting suggested workouts
$.post("./RowingWorkouts/GetSuggested", { id: selectedOldWorkout }, function(result) {
self.suggestedWorkouts(result);
});
});
This goes several levels deeper, and I would prefer avoiding it if at all possible. Are there any design patterns I'm missing or is this plain coded wrong?
You can use lazy loading observable to get data into your viewModel observables, and computed to subscribe on load of the parent level observables.
function ViewModel() {
this.workout = ko.onDemandObservable(ViewModel.prototype.getWorkout, this);
this.diet = ko.onDemandObservable(ViewModel.prototype.getDiet, this);
this.feedback= ko.onDemandObservable(ViewModel.prototype.getFeedback, this);
this.suggestedWorkouts = ko.observable();
ko.computed(ViewModel.prototype.listsLoaded, this);
}
ViewModel.prototype.listsLoaded= function () {
if (this.workout.loaded() && this.diet.loaded() && this.feedback.loaded()) {
this.loadSuggestedWorkouts();
}
}
ViewModel.prototype.getWorkout = function () {
...
}
ViewModel.prototype.getDiet = function () {
...
}
ViewModel.prototype.getFeedback = function () {
...
}
ViewModel.prototype.loadSuggestedWorkouts = function () {
...
}

Backbone architecture and view management

I am struggling with when to destroy backbone views. I know I need to destroy the view somewhere, but I am not sure where.
I have the following code in router.js
routes: {
"names/search": "nameSearch",
"companies/search": "companySearch"
},
initialize: function(){
Backbone.history.start();
this.navigate("#/", true);
}
nameSearch: function () {
require(["app/views/RecordSearch"], function (RecordSearchView) {
var obj = {};
obj.Status = [utils.xlate("On Assignment"), utils.xlate("Candidate")];
var view = new RecordSearchView({ model: obj, el: $(".content") }, { "modelName": "Candidate" });
view.delegateEvents();
});
},
companySearch: function () {
require(["app/views/RecordSearch"], function (RecordSearchView) {
var view = new RecordSearchView({ model: {}, el: $(".content") }, { "modelName": "Company" });
view.delegateEvents();
});
}
And then in RecordSearchView.js I have the following function that is called when a user clicks the search button
doSearch: function () {
require(["app/utils/SearchHelper", "app/models/" + modelName, "app/views/SearchResults"], function (SearchHelper, Model, SearchResultsView) {
var obj = $("#searchForm").serializeArray();
var params = SearchHelper.getQuery(obj);
params["page"] = 1;
params["resultsPerPage"] = 25;
var collection = new Model[modelName + "Collection"]({}, { searchParams: params });
params["Fields"] = collection.getSearchFields();
collection.getPage(params["page"], function (data) {
require(["app/views/SearchResults"], function (SearchResultsView) {
App.Router.navigate(modelName + "/search/results");
var view = new SearchResultsView({ collection: data, el: $(".content") });
view.delegateEvents();
});
});
return false;
});
And SearchResults.js
return BaseView.extend({
init: function () {
this.render();
},
render: function () {
var data = this.collection.convertToSearchResults();
this.$el.html(template(data));
return this;
}
});
The problem is the second time I perform any search (calling the doSearch function from RecordSearch.js). As soon as I perform the second search, the data shown is that belonging to the previous search I performed. (For example I do a name search and it works, then do a company search but the screen shows company search results but then is quickly replaced with name search results).
My questions are
I suspect I need to call some cleanup code on the view before it is re-used. Where is the proper place within a backbone application to run this.
Is there anything wrong with the way I load SearchResults view from within RecordSearch view? SearchResults does not have a path on my router, but it is basically a form post, so I assume it shouldn't?
Any help is appreciated.
This problem is quite common and is known as Zombie Views. Derick Bailey explains this issue very well here: http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/
However unfortunately you can't simply solve it without changing the way you are loading your views.
Because you are loading them inside RequireJS modules that will keep it in the local var scope, you are losing the reference to the views once the route has been fully processed.
In order to solve this problem, you would need to keep the reference of the current view somewhere, and then properly dispose it before calling another view, something like this:
showView: function(view) {
this.currentView && this.currentView.remove();
this.currentView = view;
this.currentView.render();
$('#content').html(this.currentView.el);
}
More about this solution here: http://tiagorg.com/talk-backbone-tricks-or-treats-html5devconf/#/6
I personally suggest you adopting a solution that will take care of this for you, like Marionette.js
It will handle this and quite many other issues, by providing the missing gaps of every Backbone-based architecture.

ExtJS refreshing a view from the controller

I have the following controller in ExtJs:
Ext.define('FileBrowser.controller.BrowserController', {
extend: 'Ext.app.Controller',
views: ['browser.tree_dir', 'browser.grid_file'],
stores: ['store_dir', 'store_file'],
init: function () {
this.control({
'window > tree_dir': {
itemclick: {
fn: function (view, record, item, index, event) {
if (record.isLeaf() == false) {
Ext.getStore('store_file').load({
params: {
dir: record.data.id
}
});
var parentOfCurrentFiles = record.data.id
nodeId = record.data.id;
htmlId = item.id;
var grid_view = this.getView('browser.grid_file');
var grid_view_v = grid_view.getView();
grid_view_v.refresh();
}
}
}
}
});
},
onPanelRendered: function () {
console.log('The panel was rendered');
}
});
If you notice under 'itemclick' I am trying to refresh one of my views, my approach is not working. Can anyone explain to me how I can refresh the view? Thank you.
Replace var grid_view= this.getView('browser.grid_file'); with var grid_view= this.getView('browser.grid_file').create(); to get a real instance (as I already told you, getView() only return the view config, not a instance!) or if you have already created that grid and only one instance exist use the xtype along with a component query to receive it var grid_view=Ext.ComponentQuery('grid_file')[0]
Now to the refresh()
Basically you never need to call this method cause your grid is bound to a store and any change made on this store is directly reflected to your grid.
I would also recommend you to store view instances when creating them instead of using queries or directly use the ref property and let ExtJS do the work for you. The last one will the best solution you I guess... Take a look at ref's within the API examples and give it a try.
So what you are trying to do is, load the store and have the data reflect once you refresh the grid_view...?
In that case, you haven't done a setStore() to the grid, or if you have done that elsewhere, you are't doing a setData() to the store. Also you should call the refresh on the grid.

basic javascript and knockout.js in MVC 3 app: problem with for in loop over an array of myCustomObject items

Problem:
I am trying to use knockout.js with jquery templates. The problem is that an $.ajax call returns values correctly, but when I try to insert them into the corresponding ko.observableArray, I get the correct number of rows, but the values are all undefined
The problem, according to my debugging is located in a for in loop in the success callback for an $.ajax call.
It seems that if I write:
for (item in myArray)
{
alert(item.myProperty); // shows undefined
}
What am I doing wrong??!
Detailed setup follows.
Setup:
My template is:
<fieldset style="padding-top:10px;">
<legend>Associated Cost Centres</legend>
<table>
<thead>
<tr>
<th>
Cost Centre
</th>
<th></th>
</tr>
</thead>
<tbody data-bind="template: {name:'actividadesAsociadas', foreach: viewModel.costCentres}"></tbody>
</table>
</fieldset>
<script type="text/x-jquery-tmpl" id="actividadesAsociadas">
<tr>
<td data-bind="text: NameCC"></td>
<td data-bind="text: CostCentreId"></td>
<td>Delete</td>
</tr>
</script>
My javascript is:
function costCentre(CostCentreId, IdTransactionType, NameCC, ownerViewModel) {
this.CostCentreId = ko.observable(CostCentreId);
this.IdTransactionType = ko.observable(IdTransactionType);
this.NameCC = ko.observable(NameCC);
this.remove = function() { ownerViewModel.costCentres.destroy(this) }
}
function costCentreViewModel() {
// other methods
this.costCentres = ko.observableArray([]);
var self = this;self = this;
$.ajax({url: "/[...route...]/GetCostCentres/" + #id,
dataType: 'json',
data: {},
type: 'POST',
success: function (jsonResult) {
var mappedCostCentres = $.map(jsonResult, function(item) {
return new costCentre(item.CostCentreId, item.IdTransactionType, item.Name, self)
});
for (cc in mappedCostCentres)
{
self.costCentres.push(new costCentre(cc.CostCentreId, cc.IdTransactionType, cc.NameCC, self));
}
},
error: function (result) {
$('#ErrorDisplay').show().html('<p>' + result.responseText + '</p>');
}
});
// test data
this.costCentres.push(new costCentre(55, 54, "test", this));
// other methods
};
var viewModel = new costCentreViewModel();
jQuery(document).ready(function () { ko.applyBindings(viewModel); });
The bit where the problem happens in the javascript code is:
for (cc in mappedCostCentres)
{
self.costCentres.push(new costCentre(cc.CostCentreId, cc.IdTransactionType, cc.NameCC, self));
}
The reason being that cc.CostCentreId, cc.IdTransactionType and cc.NameCC all evaluate to undefined.
I know that this is the case, because the test data is displayed correctly by the jquery.tmpl template, whereas the rows that have been brought in by the $.ajax call just display as empty tags.
jsonResult:
The jsonResult returned by the $.ajax call looks like this (this is correct):
[{"CostCentreId":5,"IdTransactionType":2,"Name":"Impuestos"},
{"CostCentreId":14,"IdTransactionType":3,"Name":"Transferencias Internas"}]
Questions:
Having set up my for in loop (for(cc in mappedCostCentres)), why does:
cc.NameCC
evaluate to undefined, despite being able to see in firebug that the items in mappedCostCentres have the values that I expect?
What better, or rather, working way is there to fill one array from another array?
Edit:
I am now trying the following code:
in my ViewModel (costCentreViewModel), I define:
this.GetCostCentres = function() {
$.ajax({url: "/Admin/Accounts/GetCostCentres/" + #id,
dataType: 'json',
data: {},
type: 'POST',
success: function (jsonResult) {
var mapped = $.map(jsonResult, function(item) {
return new costCentre(item.CostCentreId, item.IdTransactionType, item.Name, self)
});
self.costCentres = ko.observableArray(mapped);
alert(self.costCentres.length);
},
error: function (result) {
$('#ErrorDisplay').show().html('<p>' + result.responseText + '</p>');
}
});
};
Then I call (from outside the viewmodel definition):
var viewModel = new costCentreViewModel();
viewModel.GetCostCentres();
jQuery(document).ready(function () { ko.applyBindings(viewModel); });
It still doesn't work. The problem in my mind is:
Why doesn't this line work (everything else does):
self.costCentres = ko.observableArray(mapped);
I can only think it is the way I have defined my viewmodel object, using the constructor pattern, ie:
function myViewModel() {
this.costCentres= ko.observableArray([]);
...
this.GetCostCentres = function() {
$.ajax(....);
}
but I honestly haven't a clue. JavaScript is defeating me right now. Maybe I should go back to Quantum Cosmology?
Looks like the main issue was that when trying to set the value of the observableArray on the result of an AJAX request, you were setting it equal to a new observableArray rather than setting the value of the existing one that was already bound to your UI.
So, self.CostCentres = ko.observableArray(mapped)` will create a new observableArray that your UI is not currently bound against.
self.CostCentres(mapped) would be the appropriate way to set an existing observableArray equal to an entirely new array.
In a previous attempt, it did look like you were constructing a CostCentre twice to push it onto your observableArray. It is only necessary to create each CostCentre once.
The problem has largely been due to:
my lack of knowledge of javascript.
copying the knockoutjs.com tutorial for loading and saving data using knocking and not realising that the tutorial framework was missing any lines of js code to call the viewmodel object.
So when it didn't work, I set off on a number of red herrings that just drove me deeper and deeper into the mud.
Niemeyer's comments below the question helped me to get back on track. But for anyone with similar levels of javascript ignorance as me, you can copy or adapt the tutorial code on knockoutjs.com, just remember to add, at the bottom the equivalent of (ie, depending on what your code does):
var viewModel = new costCentreViewModel();
viewModel.GetCostCentres();
jQuery(document).ready(function () { ko.applyBindings(viewModel); });
This effectively runs your code, as opposed to odd things happening, that I am not equipped to describe or analyse...
Note
I have added this answer in case anyone comes looking with the same problem as me, and to hightlight what did work, which was following RP Niemeyer's suggestions and getting the thing to work.
If RP Niemeyer adds an answer, summarising his suggestions, I will delete this answer and mark his as the solution.

Categories