I am using Knockout.js on an MVC application.
I am attempting to use inheritance, but I think may BaseViewModel is being reused, instead of a new one being created for each ViewModel instance.
I have a single BaseViewModel, and multiple ViewModels that extend it. The behavior is the same for all Views\ViewModels - They display a list of items as an html table, and clicking the edit button of a row pops up the selected row in a modal window so that you can edit and save the changes to that row.
These all work just fine on their independent views. However, when I try to display them all on a single view, I am having issues.
The lists for each View are all binding just fine.
Clicking edit on a selected row of the LAST list/table works fine.
But clicking edit on a selected row of the FIRST list/table fails with error.
Uncaught ReferenceError: Unable to process binding "value: function (){return Name }"
"Name" is a property of the model on the the second ViewModel, not the first.
It appears that error is because only one BaseViewModel is getting created, and so properties of that BaseViewModel are being overwritten every time I add a new ViewModel to the View.
Here is a fiddle with Example of the issue.
JSFiddle
Take a look at the editItem function and bring up the console.
Then click on Edit of the second table, you will see it properly prints to console the values passed into the base class, and the Modal window opens fine.
If you then cancel and click edit of the first table, you will see it prints the SAME values that the second ViewModel passed into the BaseVM, and NOT the values that it itself passed in.
Here is my knockout BaseViewModel as well the derived ViewModels.
EnvironmentAssetBaseViewModel = function(modalWindowName, apiResourceName, modelDefinition) {
var self = this;
self.modalWindowName = modalWindowName;
self.apiResourceName = apiResourceName;
self.modelDefinition = modelDefinition;
self.itemList = ko.observableArray([]);
self.selectedRow = ko.observable(modelDefinition);
function getData() {
//make ajax API call and store data in itemList
var data = [];
if (apiResourceName == 'activityLogs') {
data = [
{ Id: "1", Action: "a", Details: "d"},
{ Id: "2", Action: "b", Details: "e"},
{ Id: "3", Action: "c", Details: "f"}
]
} else {
data = [
{ Id: "109", Name: "a", Description: "d" },
{ Id: "209", Name: "b", Description: "e" },
{ Id: "309", Name: "c", Description: "f" }
]
}
self.itemList(data);
}
function editItem(item) {
console.log(self.modalWindowName);
console.log(self.apiResourceName);
console.log(self.modelDefinition);
self.selectedRow(item);
$(modalWindowName).modal('show');
}
function cancel(item) {
$(modalWindowName).modal('hide');
}
return {
self: self,
itemList: self.itemList,
selectedRow: self.selectedRow,
getData: getData,
editItem: editItem,
cancel: cancel
}
};
ActivityLogsViewModel = function() {
var self = this;
var modalName = "#activityLogModal";
var apiResourcename = "activityLogs";
var modelDefinition = { Id: "", Action: "", Details: "" };
ko.utils.extend(self, new EnvironmentAssetBaseViewModel(modalName, apiResourcename, modelDefinition));
getData(); //initial data load
return {
self: self,
editItem: editItem,
cancel: cancel
}
};
ko.applyBindings(ActivityLogsViewModel, document.getElementById('activityLogsDiv'));
DomainsViewModel = function() {
var self = this;
var modalName = "#domainModal";
var apiResourcename = "domains";
var modelDefinition = { Id: "", Name: "", Description: "" };
ko.utils.extend(self, new EnvironmentAssetBaseViewModel(modalName, apiResourcename, modelDefinition));
getData(); //initial data load
return {
self: self,
editItem: editItem,
cancel: cancel
}
};
ko.applyBindings(DomainsViewModel, document.getElementById('domainsDiv'));
You're mixing up some methods of creating "instances" of functions.
You've defined constructor functions that "manually" return a plain object. This is perfectly fine, but it means you don't call them with the new keyword.
The new keyword creates a new this context, but since you're not using it, your first line:
var self = this;
will set self to whatever the context of the caller of the factory method is. In this case, window.
This means all your constructor functions/factories are polluting the window namespace, resulting in a weird combination of properties that are missing/overridden.
Try replacing var self = this with var self = {} to create a new object. Make sure you call your functions to create viewmodels: var activityVM = ActivityLogsViewModel().
You'll run in to some other bugs (for example, getData() needs to be self.getData()), but you should be able to figure it out.
Let me know if you need more help.
Related
I have an application that records a transaction and the user can pick the category from a drop down.
Categories are loaded up at application startup as they are "mostly" static / rarely going to change.
So, in my datacontext.js I do the usual and prime my data;
var primeData = function () {
var promise = Q.all([
getLookups(),
getBankAccountPartials(null, true)])
.then(applyValidators);
return promise.then(success);
function success() {
datacontext.lookups = {
categories: getLocal('Categories', 'name', true),
transactiontypes: getLocal('TransactionTypes', 'name', true),
payees: getLocal('Payees', 'name', true)
};
log('Primed data', datacontext.lookups);
}
function applyValidators() {
model.applyBankAccountValidators(manager.metadataStore);
}
};
function getLookups() {
return EntityQuery.from('Lookups')
.using(manager).execute()
.then(processLookups)
.fail(queryFailed);
}
Now, occasionally in an Admin screen the user can edit and add a category.
In the categoryadd.js viewmodel my save code looks something like this (extract shown);
save = function () {
isSaving(true);
datacontext.saveChanges()
.then(goToEditView).fin(complete);
function goToEditView(result) {
router.replaceLocation('#/categorydetail/' + category().id());
}
function complete() {
isSaving(false);
}
},
How do I refresh just the Categories lookup data? Or, am I just doing this wrong and should perhaps NOT have categories as a lookup?
Thanks.
Breeze.js synchronises automatically and knows to search out the Category and update it in its lookup list.
I checked this by calling datacontext.lookups from the browser console after the save had been performed and inspecting the objects it showed me the category name had been refreshed.
My collection and model like this:
detail_userid = 0;
detail_contactid = 0;
var ContactDetail = Backbone.Model.extend({
urlRoot: URL_CONTACTS1+detail_userid+"/"+detail_contactid
});
var ContactDetailCollection = Backbone.Collection.extend({
model: ContactDetail,
url: URL_CONTACTS1+detail_userid+"/"+detail_contactid
})
The entrance is:
ContactDetailManagePageModel.prototype.init = function(m,n){
detail_userid = m;
detail_contactid = n;
var myContactDetails = new ContactDetailCollection();
var contactDetailListView = new ContactDetailListView({
collection: myContactDetails
});
myContactDetails.fetch({reset:true});
}
But when it runs,the url is :http://localhost:8080/ws/users/contacts/0/0,it means that the assignment to detail_userid and detail_contactid is unsuccessful,I don't know why.
Hope for your help.Thanks.
I think you are statically definining the urlRoot and url properties before you are running the init of the PageModel (not quite sure where you are getting m and n from though...)
Both url and urlRoot can be a function, so you can pass in options during instantiation and have them dynamically set on the model.
Simple example covering defining the collection and then creating one
var ContactDetailCollection = Backbone.Collection.extend({
model: ContactDetail,
url: function(){
return URL_CONTACTS1 + this.options.detail_userid + "/" + this.options.detail_contactid;
}
});
var myContactDetails = new ContactDetailCollection({
detail_userid: foo,
detail_contactid: bar
});
As I mentioned, I'm not sure what your init function is doing, I'm guessing it's something custom from your app that I don't need to worry about.
I'm fairly sure the main thing to take away is to set url and urlRoot dynamically
I would fulfill the accepted answer with few remarks.
First parameter when initializing Backbone.Collection is array of models, then options. To create an empty collection with options you should do next
var c = new Backbone.Collection(null, {opt1: val1, opt2: val2});
Actually, you can't access this.options in url function, bec. there are no options like in a model. What you can do, is assign required properties from options upon initialization.
initialize: function (models, options) {
// `parseInt()` is used for consistency that `id` is numeric, just to be sure
this.detail_userid = parseInt(options.detail_userid);
this.detail_contactid = parseInt(options.detail_contactid);
}
Later you can access them like this:
url: function() {
return URL_CONTACTS1 + this.detail_userid + "/" + this.detail_contactid;
}
I wanted to use the HATEOAS href from one model to fetch data of another model. It worked to simply set the url on the newly created collection instead of defining it right away in the constructor.
var DailyMeasuresCollection = Backbone.Collection.extend({
//url : set dynamically with collection.url = url
model : DailyMeasuresModel,
parse : function(data) {
return data._embedded.dailyMeasures;
}
});
var DailyMeasuresTopicListItemView = Backbone.View.extend({
//...
events : {
'click .select-topic' : 'onClick'
},
onClick : function() {
var topicMeasures = new DailyMeasuresCollection()
topicMeasures.url = this.model.attributes._links.measures.href // <- here assign
var topicMeasuresView = new DailyMeasuresListView({
collection : topicMeasures
});
topicMeasures.fetch()
}
});
Problem
I have a view with 6 drop downs. Each of which is being populated by a Web API call. I want
to use breeze to run the query locally once it has populated from the remote server
The code runs fine when the data call is against the server. The issue is when trying to query the local cache. I never get any results returned. Is my approach flawed or am I doing something wrong ?
SERVER SIDE
View model
class genericDropDown()
{
public int value{get;set;}
public string option{get;set;}
}
The WebAPI [A single sample method]
[HttpGet]
// GET api/<controller>
public object GetSomeVals()
{
return _context.getClinician();
}
The Repository [A single sample method]
public IEnumerable<genericDropDown> getDropDownVal()
{
return context.somemodel(a=>new{a.id,a.firstname,a.lastname}).ToList().
Select(x => new GenericDropDown
{ value = x.id, option = x.firstname+ " " + x.lastname});}
}
CLIENT SIDE
Datacontext.js
var _manager = new breeze.EntityManager("EndPoint");
//Being called from my view model
var getDropDownBindings = function(KO1, KO2) {
//First add the entity to the local metadatastore then populate the entity
$.when(
addDD('clinicianDropDown', webAPIMethod),
getData(KO1, webAPIMethod, null, 'clinicianDropDown'),
addDD('docTypeDropDown', webAPIMethod);
getData(KO2, webAPIMethod, null, 'docTypeDropDown'),
).then(querySucceeded).fail(queryFailed);
function querySucceeded(data) {
logger.log('Got drop down vals', "", 'dataContext', true);
}
};
//Add the entity to local store. First param is typename and second is
resource name (Web API method)
var addDD = function(shortName,resName) {
_manager.metadataStore.addEntityType({
shortName: shortName,
namespace: "Namespace",
autoGeneratedKeyType: breeze.AutoGeneratedKeyType.Identity,
defaultResourceName:resName,
dataProperties: {
value: { dataType: DataType.Int32,
isNullable: false, isPartOfKey: true },
option: { dataType: DataType.String, isNullable: false }
}
});
return _manager.metadataStore.registerEntityTypeCtor(shortName, null, null);
};
//Get the data
var getData = function(observableArray, dataEndPoint, parameters, mapto) {
if (observableArray != null)
observableArray([]);
//TO DO: Incorporate logic for server or local call depending on
// whether this method is accessed for the first time
var query = breeze.EntityQuery.from(dataEndPoint);
if (mapto != null && mapto != "")
query = query.toType(mapto);
if (parameters != null)
query = query.withParameters(parameters);
//This approach doesnt work on local querying as Jquery complains
//there is no 'then' method. Not sure how to implement promises
//when querying locally
/* return _manager.executeQuery(query).then(querySucceeded).fail(queryFailed);
function querySucceeded(data) {
if (observableArray != null)
observableArray(data.results);
}
*/
//The array length from this query is always 0
var data = _manager.executeQueryLocally(query);
observableArray(data.results);
return;
};
//Generic error handler
function queryFailed(error) {
logger.log(error.message, null, 'dataContext', true);
}
viewmodel.js
//In Durandal's activate method populate the observable arrays
dataContext.getDropDownBindings (KO1,KO2);
Viewmodel.html
<select class="dropdown" data-bind="options: KO1, optionsText: 'option', value: 'value', optionsCaption: 'Clinicians'"></select>
<select class="dropdown" data-bind="options: KO2 optionsText: 'option', value: 'value', optionsCaption: 'Document Types'"></select>
You can only execute local queries against types that are described by metadata.
Without more information I can't be sure, but my guess is that your GetSomeVals method is not returning 'entities' but just loose data. In other words, the types of objects returned from the GetSomeVals method must be entities (or contain entities within a projection) in order for breeze to be able to perform a local query. This is because Breeze knows how to cache and query entities but has no ideas how to cache 'arbitrary' query results.
Note that you can return an anonymous type containing entities of different types from the server, (in order to populate mostly static small datasets), but the individual items must be 'entities'. In this case, Breeze will take apart the anon result and pick out any entities to include in the EntityManager cache.
Per you question of how to perform an local query with promises, use the FetchStrategy.FromLocalCache with the using method.
i.e. this query
var results = em.executeQueryLocally(query)
can also be expressed as:
query = query.using(FetchStrategy.FromLocalCache);
return em.executeQuery(query).then(data) {
var results = data.results;
}
The local query is still executed synchonously but is made to look async.
I am using Lib.Web.Mvc (version 6.1.0) to generate jqgrid in client.
Most attribute of JqGridColumnModel work fine but CellAttributes has not any effection here.
This is my Code:
configuration.Settings.ColumnsModels.AddRange(new JqGridColumnModel[]
{
new JqGridColumnModel("ProductID") { Index = "ProductID" },
new JqGridColumnModel("ProductName") { Index = "ProductName" },
new JqGridColumnModel("SupplierID") { Index = "SupplierID" },
new JqGridColumnModel("CategoryID") { Index = "CategoryID" },
new JqGridColumnModel("QuantityPerUnit") { Index = "QuantityPerUnit"},
new JqGridColumnModel("UnitPrice") { Index = "UnitPrice", CellAttributes*="value=1"}
});
I don't know if CellAttributes still not work in lastes version 6.1.0? Or I don't know how to use it.
Please give me idea to add more attributes to cell from server side (controller).
The JqGridColumnModel.CellAttributes is a proxy to cellattr property of colModel. This property takes a JavaScript function which should return string with attributes. Below you can find corresponding part from jqGrid documentation:
This function add attributes to the cell during the creation of the data - i.e dynamically. By example all valid attributes for the table cell can be used or a style attribute with different properties. The function should return string. ...
And from Lib.Web.Mvc documentation:
Gets or sets the function which can add attributes to the cell during the creation of the data (dynamically).
So in your case the code should look like this:
configuration.Settings.ColumnsModels.AddRange(new JqGridColumnModel[]
{
new JqGridColumnModel("ProductID") { Index = "ProductID" },
new JqGridColumnModel("ProductName") { Index = "ProductName" },
new JqGridColumnModel("SupplierID") { Index = "SupplierID" },
new JqGridColumnModel("CategoryID") { Index = "CategoryID" },
new JqGridColumnModel("QuantityPerUnit") { Index = "QuantityPerUnit" },
new JqGridColumnModel("UnitPrice") { Index = "UnitPrice", CellAttributes = "function() { return 'value=1'; }" }
});
UPDATE
There is one special case, the configuration import and export functionality. In this case all the settings for JavaScript functions, events and callbacks are ignored as those can't be serialized to JSON (this is by design).
I have based my code on this example
http://jsfiddle.net/rniemeyer/WpnTU/
When you select an item I want the title of the dialog to have a observable's value
I managed to to it by creating another custom binding
ko.bindingHandlers.dialogOptions = {
update: function(element, valueAccessor) {
var options = ko.utils.unwrapObservable(valueAccessor());
if (options ) {
$(element).dialog(options);
}
}
}
Added a new observable to viewmodel and set it when the item is selected
this.selectProduct = function(product) {
self.dialogOptions({ title: product.name() });
self.selectedProduct(product);
}
Working example: http://jsfiddle.net/WpnTU/76/
It works but I do not like it, it adds a new observable which is very coupled with the GUI, it would be much nicer if I could use the already exiting selectProduct observable and point out the name property in the GUI something like { title: selectProduct.name }
Here is a sample that moves the .dialog calls into the update function and unwraps the options, so that it will be triggered any time that something changes.
//custom binding to initialize a jQuery UI dialog
ko.bindingHandlers.jqDialog = {
init: function(element) {
ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
$(element).dialog("destroy");
});
},
update: function(element, valueAccessor) {
var options = ko.toJS(valueAccessor());
if (options) {
$(element).dialog(options);
}
}
};
I added a computed observable to your sample just to handle the selectedProduct being null (could be done in-line).
http://jsfiddle.net/rniemeyer/Gt5Hw/