I'm having issues with my knockoutjs implementation. Seems to be working fine on Chrome and Safari IE and FF have thrown a hissy fit.
The message which I encounter is as follows:
Unable to parse bindings. Message: TypeError: 'AccountName' is
undefined; Bindings value: value: AccountName
The issue is happening within a script tag which serves as a knockout template:
<div id="newAccountDialog" class="dialog" data-bind="dialog: { autoOpen: false, resizable: false, modal: true, width: 350, title: 'Exchange Account'}, template: { name: 'dialogFormTemplate', data: CurrentAccount }, openDialog: IsNew"></div>
<script id="dialogFormTemplate" type="text/html">
<form id="dialogForm">
<h1>Exchange Account Manager</h1>
<p>Add new or edit an existing exchange account settings.</p>
<label for="AccountName">
Account
</label>
<input id="AccountName" name="AccountName" type="text" data-bind="value: AccountName, valueUpdate: 'afterkeydown'" class="ui-widget-content ui-corner-all" />
<div class="buttonsContainer floatRight">
<button id="Save" data-bind="click: $root.SaveAccount, dialogcmd: { id: 'newAccountDialog', cmd: 'close'}, jqButton: { icons: { primary: 'ui-icon-disk' } }">Save & Close</button>
</div>
</form>
</script>
I assume some sort of early binding is being triggered on the template
data : CurrentAccount
where an undefined / null is being passed into CurrentAccount. I have seen this issue outside of script tags, but only if the observable is not defined or null.
My viewmodel looks as following:
var AccountModel = function () {
var self = this;
self.Accounts = ko.observableArray([]);
self.CurrentAccount = ko.observable(null);
self.IsNew = ko.observable(false);
self.LoadAccounts = function () {
$account.invoke("GetAccounts", {}, function (data) {
var mapped = $.map(data, function (item) {
var account = new Account(item);
var innerMapped = $.map(item.Mailboxes, function (mailbox) {
return new Mailbox(mailbox);
});
account.Mailboxes(innerMapped);
return account;
});
self.Accounts(mapped);
});
}
self.EditAccount = function (data) {
self.CurrentAccount(data);
self.IsNew(true);
}
self.SaveAccount = function () {
if (self.CurrentAccount().Id() <= 0) {
$account.invoke('AddAccount', ko.toJS(self.CurrentAccount()), function (data) {
self.Accounts.push(new Account(data));
self.CurrentAccount(new Account(data));
self.IsNew(true);
});
} else {
$account.invoke('UpdateAccount', ko.toJS(self.CurrentAccount()), function (data) {
//self.CurrentAccount(new Account(data));
});
}
}
self.CreateAccount = function () {
self.IsNew(true);
var account = { Id: 0, AccountName: '', IsNTLM: -1, Email: '', Password: '', Domain: 'mydomain', ExchangeVersion: 1, Mailboxes: [] };
self.CurrentAccount(new Account(account));
}
};
My dialog bindingHandler is defined as follows:
ko.bindingHandlers.dialog = {
init: function (element, valueAccessor) {
var options = ko.utils.unwrapObservable(valueAccessor()) || {};
ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
$(element).dialog('destroy');
});
$(element).dialog(options);
}
};
I have ommited the Account object, as it is possibly not required in this context.
I would appreciate any help.
Thank you in advance.
There is no "early" binding in Knockout. Everything is bound when you call ko.applyBindings. But certain bindings can stop or delay binding of their descendant elements. template is one of those when you use the if or ifnot options. In your case, you can use the if option like this:
template: { name: 'dialogFormTemplate', data: CurrentAccount, 'if': CurrentAccount }
Note: The quotes around if are required in some older browsers.
Related
I am trying to get present date and pass it to the function - same-or-earlier-than But in the run-time, it does not pass date value to this class. Below is my html code. I am trying to get date from javascript and printing it for test.
the date is printed, but when passing it to the function, it doesnt work.
<div class="row">
<div class="hh-dock-page-view panel">
<div class="panel-body">
<div class="container">
<h2 class="hh-page-header">Issue Detail</h2>
<h3 class="hh-action-heading"><span ng-hide="ctrl.readOnly">Edit </span>Details - {{ctrl.returnCurDate()}} - {{ctrl.issue.issue_date}}"</h3>
<hh-model-form name="issueForm" model="ctrl.issue" page-controller="ctrl" submit-success="ctrl.submitSuccess" hide-actions="ctrl.readOnly">
<h2 class="hh-page-header extra-margin">{{modelFormCtrl.model.provider_name_spec_prac}} - {{ctrl.issue.issue_date}}</h2>
<div class="row">
<div class="col-sm-6">
<hh-field-messages>
<label for="issue_date">Issue Date</label>
<hh-american-date-field
id="issue_date"
name="issue_date"
ng-model="modelFormCtrl.model.issue_date"
ng-blur="modelFormCtrl.pageController.setStatus(this)"
ng-disabled="modelFormCtrl.pageController.readOnly"
same-or-earlier-than="{{ctrl.issue.issue_date}}"
same-or-earlier-than-msg="Enter a date that is on or before Resolution Date."
>
</hh-american-date-field>
</hh-field-messages>
<div class="hh-breath"></div>
</div>
</div>
</hh-model-form>
</div>
</div>
and here below is my angularjs file:
angular.module('Prm')
.controller('IssueDetailController',
['$scope', '$q', '$route', '$location', '$routeParams', 'moment', 'IssueService', 'IssuePriorityService', 'ActivityService',
'ProviderService', 'DirtyFormCheckingService', 'ConfirmBox', 'ErrorAlertService', 'AlertBoxService', 'DateService',
'ReferenceTableDataAdapter',
function ($scope, $q, $route, $location, $routeParams, moment, IssueService, IssuePriorityService, ActivityService, ProviderService,
DirtyFormCheckingService, ConfirmBox, ErrorAlertService, AlertBoxService, DateService,
ReferenceTableDataAdapter){
'use strict';
var ctrl = this,
isExisting = false,
issuePriority = null,
initPromises = [];
var currentUser = cpm.authentication.user.report_ownership_role_code;
ctrl.isAdmin = cpm.authentication.user.dss_role_list.split(",").includes('USERADMIN');
ctrl.readOnly = true;
var getNameForEdit = function(row) {
if (ctrl.readOnly) return 'View';
return 'Edit';
};
ctrl.providerAdapter = ReferenceTableDataAdapter('provider');
ctrl.ownerRoleAdapter = ReferenceTableDataAdapter('dss_report_owner_contact');
ctrl.providerIssueStatusAdapter = ReferenceTableDataAdapter('provider_issue_status');
ctrl.providerIssueSiteAdapter = ReferenceTableDataAdapter('provider_issue_site');
ctrl.providerIssuePriorityAdapter = ReferenceTableDataAdapter('provider_issue_priority');
ctrl.providerIssueCategoryAdapter = ReferenceTableDataAdapter('provider_issue_category');
if($routeParams.issue_code) {
isExisting = true;
initPromises.push(IssueService.get({id: $routeParams.issue_code}).$promise.then(function (data) {
ctrl.issue = data;
// placing this on the controller for the Parent Issue grid (if exists). Grid needs list of objects...
ctrl.parentActivities = [{'parent_provider_activity':ctrl.issue.parent_provider_activity,
'parent_provider_activity_activity_date':ctrl.issue.parent_provider_activity_activity_date,
'parent_provider_activity_description': ctrl.issue.parent_provider_activity_description}];
}));
} else {
ctrl.issue = IssueService.wrapData({
description: '',
resolution: '',
status: 'N',
site: '',
priority: 'N',
category: '',
provider: $routeParams.provider_code,
issue_date: new Date(),
owner_role: currentUser
});
ctrl.hide_resolution = true;
//ctrl.issue_date = new Date();
/* An issue may be created from a provider */
if($routeParams.provider_code) {
initPromises.push(ProviderService.get({id: $routeParams.provider_code}).$promise.then(function (data) {
ctrl.issue.provider = data.provider_code;
}));
}
}
/* Issues can have their due dates set based on their priority */
initPromises.push(IssuePriorityService.getPrioritiesMap().then(function (priorities) {
issuePriority = priorities;
if(!isExisting) {
ctrl.issue.due_date = moment(DateService.todayAsUtcDate()).add(issuePriority.N, 'day').toDate();
}
}));
/* Initialization */
$q.all(initPromises).then(function () {
/* Allow editing existing record only by owner & admins */
if(!isExisting || ctrl.isAdmin || ctrl.issue.owner_role === cpm.authentication.user.report_ownership_role_code) {
ctrl.readOnly = false;
}
// TODO: when grid actions are added
// ctrl.activitiesGrid.hideAddButton = ctrl.readOnly;
DirtyFormCheckingService($scope, 'issueForm', ctrl.issue);
});
ctrl.setDueDate = function (newModel, newCode) {
ctrl.issue.due_date = moment(ctrl.issue.issue_date).add(issuePriority[newCode], 'day').toDate();
};
ctrl.setResolutionDate = function (newModel, newCode) {
// ctrl.hide_resolution = (newCode !== 'R');
if (newCode == 'R') {
ctrl.issue.resolution_date = DateService.todayAsUtcDate();
} else {
ctrl.issue.resolution_date = null;
}
};
$scope.$watch('ctrl.issue.status', function (newCode) {
if(ctrl.issue) {
ctrl.hide_resolution = (newCode !== 'R');
}
});
ctrl.setStatus = function (event){
if (ctrl.issue.resolution_date) {
ctrl.issue.status = "R";
} else {
ctrl.issue.status = "N";
}
};
ctrl.submitSuccess = function() {
$location.url('/');
};
ctrl.parentActivityConfig = {
modelSetName: 'parentIssues',
actions: {
editRow: {
label: function(row) {
return ctrl.readOnly ? 'View' : 'Edit';
},
active: true,
callback: function(row){
$location.path('/activity/'+ctrl.issue.parent_provider_activity);
}
},
deleteRow: {
active: false
}
},
rowActions: ['editRow'],
columns: [
{col_description: 'Activity Date',
datatype: 'date',
model_field: 'parent_provider_activity_activity_date',
field_type: 'american_date',
field_name: 'activityDate'
},
{col_description: 'Description',
datatype: 'text',
model_field: 'parent_provider_activity_description',
field_type: 'text',
field_name: 'description'
},
]
};
ctrl.activitiesTableConfig = {
modelSetName: "activities",
serializerChildName: 'activities',
actions: {
addRow: {
label: 'Add Activity',
callback: function(){
$location.path('/provider/'+ctrl.issue.provider+'/add_activity/'+ctrl.issue.provider_issue_code);
},
active: function(row) {return !ctrl.readOnly;}
},
editRow: {
label: function(row) {
return getNameForEdit(row.modelSet);
},
active: true,
callback: function(row){
$location.path('/activity/'+row.modelSet.provider_activity_code);
}
},
deleteRow: {
active: function(row) {return !ctrl.readOnly;},
callback: function(row){
_.pull(ctrl.issue.activities, row.modelSet);
}
}
},
rowActions: ['editRow', 'deleteRow'],
columns: [
{col_description: 'Date',
datatype: 'date',
model_field: 'activity_date',
field_type: 'american-date',
field_name: 'activity_date'
},
{col_description: 'Type',
datatype: 'text',
model_field: 'provider_activity_type_description',
field_type: 'text',
field_name: 'provider_activity_type_description'
},
{col_description: 'Notes',
datatype: 'text',
model_field: 'description',
field_type: 'text',
field_name: 'description'
},
{col_description: 'Owner',
datatype: 'text',
model_field: 'owner_comp_name',
field_type: 'text',
field_name: 'owner_comp_name'
},
]
};
}]);
I'm trying my best to understand your question, but there's a lot of code to look through here. I do understand that you're trying to pass the current date to a function named "same-or-earlier than." However I can't seem to find a function with that name in your controller file.
I'm assuming that you've got a custom directive (attribute directive) that accepts the current date.
Either way, a simple solution for your problem of passing the current date could be to write a small function that when invoked returns the date using Javascript's native "Date" object.
Perhaps like this in your controller:
ctrl.returnCurDate = function() {
return new Date();
};
That way in your markup, you can invoke the method we've defined since it's attached to the scope of your controller.
same-or-earlier-than = "ctrl.returnCurDate()"
I hope I'm answering your problem in a relevant fashion, please feel free to ask further if I've misunderstood your question.
Best of luck.
P.S. Inherently there are some pain-in-the-butt formatting issues that come with using JS's native Date constructor, I would consider using a library such as "Moment.js" to relieve you of parsing the returned value when invoking:
new Date();
I have a model consisting of a project object, a text object and a setting object. The settings object can hold values for both projects and texts.
A text is always coupled to a project. Thus it should inherit the projects settings (if any). However if the text itself has settings, the user should be able to choose between using the project default or the text-specific settings.
The problem I'm having in my project is that updating the text local setting is also updating the project default settings. I guess this has to do with the observables relation, but so far I have been unable to find the connection.
Here's my code:
Text = function (data) {
var self = this;
self.textID = data.textID;
self.title = ko.observable(data.title);
self.projectID = data.projectID;
// settings
self.settings = ko.observable(); // initiated with "each" loop hook-up in viewmodel.
self.project = ko.observable(); // initiated with "each" loop hook-up in viewmodel.
self.projectSettings = ko.computed(function () {
var project = self.project();
if (project) {
var proset = project.settings();
return proset;
}
return null;
});
self.useProjectSettings = ko.observable(data.useProjectSettings || true);
self.hasLocalSettings = ko.observable(false); // initiated with each loop hook-up in viewmodel.
self.activeSettings = ko.computed(function () {
return self.useProjectSettings() ? self.projectSettings() : self.settings();
});
return self;
};
Project = function (data) {
var self = this;
self.projectID = data.projectID;
self.title = ko.observable(data.title);
// settings
self.settings = ko.observable();
return self;
};
Setting = function (data) {
var self = this;
self.settingID = data.settingID;
self.projectID = data.projectID;
self.textID = data.textID;
self.isVisible = ko.observable(data.isVisible);
self.isProjectDefault = ko.observable(data.isProjectDefault || true);
return self;
};
ViewModel = function (myprojects, mysettings, mytexts) {
var self = this,
koEach = ko.utils.arrayForEach, koFirst = ko.utils.arrayFirst, koFilter = ko.utils.arrayFilter;
self.selectedText = ko.observable();
//data - load
self.Projects = ko.observableArray(
ko.utils.arrayMap(myprojects, function (item) {
return new Project(item);
}));
self.Settings = ko.observableArray(
ko.utils.arrayMap(mysettings, function (item) {
return new Setting(item);
}));
self.Texts = ko.observableArray(
ko.utils.arrayMap(mytexts, function (item) {
return new Text(item);
}));
//alert("self.Projects()[0].projectID=" + self.Projects()[0].projectID );
// hook up 'settings' on 'project'
koEach(self.Settings(), function (s) {
if (s.isProjectDefault()) {
var project = koFirst(self.Projects(), function (p) {
return p.projectID === s.projectID;
});
if (project) {
project.settings(s);
}
}
});
//alert("self.Texts()[0].textID=" + self.Texts()[0].textID + ", self.Texts()[0].title()=" + self.Texts()[0].title() );
//hook up 'project' on 'texts'
koEach(self.Texts(), function (t) {
var project = koFirst(self.Projects(), function (p) {
return p.projectID === t.projectID;
});
t.project(project);
// initiate default publishingSettings for textbatch
var initsettings = project.settings();
t.settings(initsettings);
});
// hook up 'settings' on 'text'
koEach(self.Settings(), function (s) {
if (!s.isProjectDefault()) {
var text = koFirst(self.Texts(), function (t) {
return t.textID === s.textID;
});
if (text) {
text.settings(s);
}
}
});
};
// console log
var consoleLine = "<p class=\"console-line\"></p>";
console = {
log: function (text) {
$("#console-log").append($(consoleLine).html(text));
}
};
// input
var projects = [{ projectID: 0, title: 'project 0' }, { projectID: 1, title: 'project 1' }];
var settings = [
{ settingID: 0, projectID: 1, textID: null, isVisible: true, isProjectDefault: true },
{ settingID: 1, projectID: null, textID: 0, isVisible: true, isProjectDefault: false },
{ settingID: 2, projectID: 1, textID: 1, isVisible: false, isProjectDefault: false }
];
var texts = [{ textID: 0, projectID: 0, title: 'first text' }, { textID: 1, projectID: 1, title: 'second text' }];
// binding
ko.applyBindings(new ViewModel(projects, settings, texts));
Here´s my HTML:
<select data-bind="options: $root.Texts, optionsCaption: 'choose...', optionsText: 'title', value: selectedText"></select>
<br/>
Selected: <span data-bind="text: $root.selectedText().title"></span><br>
Selected2: <span data-bind="text: $root.selectedText().title"></span><br>
TextID: <span data-bind="text: selectedText().textID"></span><br>
<hr/>
<div data-bind="with: $root.selectedText">
<h2 data-bind="text:title"></h2>
<div class="form-group">
<div class="checkbox">
<label>
<input type="checkbox" data-bind="checked: useProjectSettings" />Use project settings
</label>
</div>
</div>
Settings for text '<span data-bind="text: title"></span>' (id='<span data-bind="value: textID"></span>').<br/><br/>
<div class="pubset" data-bind="css: {'unabled' : useProjectSettings() }">
<div class="form-group">
<div class="checkbox">
<label>
<input type="checkbox" data-bind="enable: !useProjectSettings(),checked: activeSettings().isVisible"/>
Show text
</label>
</div>
</div>
</div>
</div>
With my example code here, it seems I also have another problem, as it's not progressing to the second <span> node. Fiddle here.
I have another function that calls the `"GetEmployee" function from the server and when I check the call in the Chrome Debugger I can see my details coming through as expected so it's not a server side error.
However, if I place a breakpoint on the third line of quickBookingSource the breakpoint is never reached. I do have quickBooking:quickBookingSource in the viewModel definition and there are no typos.
Also, you'll see I have added a ternary operator with "blah" text as the safety net but to no avail
The error message I'm getting is:
Uncaught ReferenceError: Unable to process binding "text: function (){return project }"
Message: project is not defined
Code is:
HTML
<div data-bind="dxTileView: {listHeight:tileWidgetHeight,itemClickAction:sendProject,baseItemHeight: 80, baseItemWidth: 100,dataSource:quickBooking}">
<div data-options="dxTemplate : { name:'item' }" class="tile">
<h2 data-bind="text: project"></h2>
<p data-bind="text: name"></p>
<p data-bind="text: costCenter"></p>
<p>Jetzt Büchen</p>
</div>
</div>
JS
var quickBookingSource = DevExpress.data.createDataSource({
load: function (loadOptions) {
if (loadOptions.refresh) {
var deferred = new $.Deferred();
callService("GetEmployee",
{
employeeNo: aktivEmployee.id
},
function (result) {
var mapped = $.map(result, function (data) {
return {
name: data.LastNProjects? data.LastNProjects["Name"]:"blah",
project: data.LastNProjects? data.LastNProjects["Address"]:"blah",
costCenter: data.LastNCostCenters? data.LastNCostCenters["Name"]:"blah"
}
});
deferred.resolve(mapped);
});
return deferred.promise();
}
},
});
Thanks in advance
I reproduced your case in the following fiddle http://jsfiddle.net/tabalinas/7aSS7/.
Request to server is mocked with setTimeout.
You can click Refresh button to reload dataSource. The demo shows that your code works correctly. It seems that problem is in client code behind the scene if server code is ok.
<div class="dx-viewport dx-theme-ios dx-version-major-6 dx-theme-ios-typography">
<div data-bind="dxButton: { text: 'Refresh', clickAction: reloadData }"></div>
<span data-bind="visible: loading">Loading ...</span>
<div data-bind="dxTileView: { listHeight: tileWidgetHeight, itemClickAction: sendProject, baseItemHeight: 200, baseItemWidth: 100, dataSource: quickBooking }">
<div data-options="dxTemplate : { name:'item' }" class="tile">
<h2 data-bind="text: project"></h2>
<p data-bind="text: name"></p>
<p data-bind="text: costCenter"></p>
<p>Jetzt Büchen</p>
</div>
</div>
</div>
// stub service call
var callService = function(method, data, success) {
var fakeData = [
{ LastNProjects: { Name: 'test project1' }, LastNCostCenters: { Name: 'cost center1' }},
{ LastNProjects: { Name: 'test project2' }, LastNCostCenters: { Name: 'cost center2' }},
{ LastNProjects: { Name: 'test project3' }, LastNCostCenters: { Name: 'cost center3' }},
{ LastNProjects: { Name: 'test project4' }, LastNCostCenters: { Name: 'cost center4' }}
];
setTimeout(function() {
success(fakeData);
}, 1500);
};
var quickBookingSource = DevExpress.data.createDataSource({
load: function (loadOptions) {
vm.loading(true);
if (loadOptions.refresh) {
var deferred = new $.Deferred();
callService("GetEmployee",
{
employeeNo: 'id'
},
function (result) {
var mapped = $.map(result, function (data) {
return {
name: data.LastNProjects? data.LastNProjects["Name"]:"blah",
project: data.LastNProjects? data.LastNProjects["Address"]:"blah",
costCenter: data.LastNCostCenters? data.LastNCostCenters["Name"]:"blah"
}
});
deferred.resolve(mapped);
vm.loading(false);
});
return deferred.promise();
}
},
});
var vm = {
loading: ko.observable(false),
reloadData: function() {
quickBookingSource.load();
},
tileWidgetHeight: 300,
quickBooking: quickBookingSource,
sendProject: function(args) {
console.log("send " + args.itemData.name);
}
};
ko.applyBindings(vm);
Now Solved - See bottom....
I've got a Backbone list view with a button on it that should show the edit elements.
Neither the jQuery hide() call in the 'showAddEntry' function or the view rendering for 'versionEditView' are doing anything at all. I've stepped right through and I'm not getting any errors. I've even tried manually running methods in the console to see what's going on with hide, but I'm not getting anywhere.
Here's the main view...
define(['ministry', 'jquery', 'models/m-version-info', 'views/about/v-edit-version-info-entry', 'text!templates/version-info/version-info.html'],
function(Ministry, $, VersionInfo, VersionInfoEditView, TemplateSource) {
var versionInfoEntriesView = Ministry.View.extend({
el: '#mainAppArea',
template: Handlebars.compile(TemplateSource),
versionInfoEditView: null,
initialize: function () {
this.$addEntryArea = $('#addVersionInfoEntryArea');
this.$addEntryButton = $('#addVersionInfoEntryButton');
},
events: {
'click #addVersionInfoEntryButton': 'showAddEntry'
},
render: function () {
var that = this;
var entries = new VersionInfo.Collection();
entries.fetch({
success: function (data) {
that.$el.html(that.template({ items: data.toJSON() }));
}
});
return this;
},
showAddEntry: function() {
if (this.versionInfoEditView != null) {
this.versionInfoEditView.trash();
}
this.versionInfoEditView = new VersionInfoEditView({ el: this.$addEntryArea });
this.$addEntryButton.hide();
this.versionInfoEditView.render();
return false;
}
});
return versionInfoEntriesView;
});
And here's the child view...
define(['ministry', 'models/m-version-info', 'text!templates/version-info/edit-version-info- entry.html', 'jquery.custom'],
function (Ministry, VersionInfo, TemplateSource) {
var editVersionInfoView = Ministry.View.extend({
template: Handlebars.compile(TemplateSource),
initialize: function () {
this.$dbVersionInput = this.$('#dbVersion');
this.$tagInput = this.$('#tag');
},
render: function () {
this.$el.html(this.template());
return this;
},
events: {
'submit .edit-version-info-form': 'saveEntry'
},
saveEntry: function() {
var entry = new VersionInfo.Model({ dbVersion: this.$dbVersionInput.val(), tag: this.$tagInput.val() });
entry.save({
success: function() {
alert('Your item has been saved');
}
});
return false;
}
});
return editVersionInfoView;
});
And the main template...
<h2>Version Info</h2>
<div id="info">
<a id="addVersionInfoEntryButton" href="#/versioninfo">Add manual entry</a>
<div id="addVersionInfoEntryArea">
</div>
<ul id="items">
{{#each items}}
<li>{{dbVersion}} | {{tag}}</li>
{{/each}}
</ul>
</div>
And the edit template...
<form class="edit-version-info-form">
<h3>Create a new entry</h3>
<label for="dbVersion">DB Version</label>
<input type="text" id="dbVersion" maxlength="10" />
<label for="tag">Tag</label>
<input type="text" id="tag" />
<button type="submit" id="newEntryButton">Create</button>
</form>
I'm fairly new to backbone so I may well be doing something totally wrong, but I can't see anything wrong with the approach so far and it's not throwing any errors.
OK - Fix as follows after some facepalming...
define(['ministry', 'jquery', 'models/m-version-info', 'views/about/v-edit-version-info-entry', 'text!templates/version-info/version-info.html'],
function(Ministry, $, VersionInfo, VersionInfoEditView, TemplateSource) {
var versionInfoEntriesView = Ministry.View.extend({
el: '#mainAppArea',
template: Handlebars.compile(TemplateSource),
versionInfoEditView: null,
$addEntryArea: undefined,
$addEntryButton: undefined,
initialize: function () {
},
events: {
'click #addVersionInfoEntryButton': 'showAddEntry'
},
render: function () {
var that = this;
var entries = new VersionInfo.Collection();
entries.fetch({
success: function (data) {
that.$el.html(that.template({ items: data.toJSON() }));
that.$addEntryArea = that.$('#addVersionInfoEntryArea');
that.$addEntryButton = that.$('#addVersionInfoEntryButton');
}
});
return this;
},
showAddEntry: function (e) {
e.preventDefault();
if (this.versionInfoEditView != null) {
this.versionInfoEditView.trash();
}
this.versionInfoEditView = new VersionInfoEditView({ el: this.$addEntryArea });
this.$addEntryButton.hide();
this.$addEntryArea.append('Do I want to put it here?');
this.versionInfoEditView.render();
}
});
return versionInfoEntriesView;
});
The issue was due to the fact that I was setting the internal element variables within the view before the completion of the render, so the elements were linked up to nothing. I resolved this by extracting the element initiation to the end of the render success callback.
Here's the fix again...
define(['ministry', 'jquery', 'models/m-version-info', 'views/about/v-edit-version-info-entry', 'text!templates/version-info/version-info.html'],
function(Ministry, $, VersionInfo, VersionInfoEditView, TemplateSource) {
var versionInfoEntriesView = Ministry.View.extend({
el: '#mainAppArea',
template: Handlebars.compile(TemplateSource),
versionInfoEditView: null,
$addEntryArea: undefined,
$addEntryButton: undefined,
initialize: function () {
},
events: {
'click #addVersionInfoEntryButton': 'showAddEntry'
},
render: function () {
var that = this;
var entries = new VersionInfo.Collection();
entries.fetch({
success: function (data) {
that.$el.html(that.template({ items: data.toJSON() }));
that.$addEntryArea = that.$('#addVersionInfoEntryArea');
that.$addEntryButton = that.$('#addVersionInfoEntryButton');
}
});
return this;
},
showAddEntry: function (e) {
e.preventDefault();
if (this.versionInfoEditView != null) {
this.versionInfoEditView.trash();
}
this.versionInfoEditView = new VersionInfoEditView({ el: this.$addEntryArea });
this.$addEntryButton.hide();
this.$addEntryArea.append('Do I want to put it here?');
this.versionInfoEditView.render();
}
});
return versionInfoEntriesView;
});
The issue was due to the fact that I was setting the internal element variables within the view before the completion of the render, so the elements were linked up to nothing. I resolved this by extracting the element initiation to the end of the render success callback.
Let's start with the code..
Javascript to bind viewmodel and display dialog
$("#add-song").click(function () {
$("<div>")
.attr("data-bind", "template: { name: 'manage-song-template' }")
.dialog({
resizable: false,
modal: true,
title: "Add Song",
width: 960,
height: 490,
buttons: [
{
text: "Create Song",
'data-bind': 'click: savechanges',
click: function () {
}
},
{
text: "Close",
click: function () {
$(this).dialog("close");
}
}
],
close: function () {
$(this).dialog('destroy').remove();
},
open: function () {
ko.applyBindings(new my.managesongviewmodel());
jquerybindings();
}
});
});
Now let's take a look at the view model
my.managesongviewmodel = function () {
var
//Scalar Properties
song = null,
//Functions
loadfromserver = function (Id) {
$.post("/song/getsong", "id=1", function (response) {
if (response.Success) {
var data = response.Data;
var song = response.Data.Song;
var songdata = {
Song: {
Id: song.Id,
Title: song.Title,
Description: song.Description,
Lyrics: song.Lyrics,
isMaster: song.isMaster,
AudioFilePath: song.AudioFilePath,
CoverImageFilePath: song.CoverImageFilePath,
CreatedDate: song.CreatedDate
},
Genres: data.Genres,
SongAlternateTitles: data.SongAlternateTitles,
Exploitations: data.Exploitations,
SongWriterSongs: data.SongWriterSongs
};
song = new my.song(songdata);
alert(song.title());
}
});
},
savechanges = function () {
};
loadfromserver();
return {
song: song,
loadfromserver: loadfromserver
};
};
my.song = function (data) {
var
//Scalar Properties
id = ko.observable(data.Song.Id),
title = ko.observable(data.Song.Title),
description = ko.observable(data.Song.Description),
lyrics = ko.observable(data.Song.Lyrics),
ismaster = ko.observable(data.Song.isMaster),
audiofilepath = ko.observable(data.Song.AudioFilePath),
coverimagefilepath = ko.observable(data.Song.CoverImageFilePath),
createddate = ko.observable(data.Song.CreatedDate),
//Arrays
genres = ko.observableArray(data.Genres),
alttitles = ko.observableArray(data.SongAlternateTitles),
exploitations = ko.observableArray(data.Exploitations),
songwritersongs = ko.observableArray(data.SongWriterSongs);
return {
id: id,
title: title,
description: description,
lyrics: lyrics,
ismaster: ismaster,
audiofilepath: audiofilepath,
coverimagefilepath: coverimagefilepath,
createddate: createddate,
genres: genres,
alttitles: alttitles,
exploitations: exploitations,
songwritersongs: songwritersongs
};
};
Here is my template
<script type="text/html" id="manage-song-template">
<div class="modal-form">
<span data-bind="text: song.title"></span>
</div>
</script>
Chrome is throwing the error "Cannot read property 'title' of null;" when I launch the dialog. That said, the "alert(song.title());" actually shows a the song title.
Any thoughts on where I've gone wrong?
Update
I have created a jsfiddle that replicates the issue. http://jsfiddle.net/mcottingham/H7jqa/28/
It's easy. In the moment when you shows modal window your song var still equals null. So you have to wait while info about song will be loaded from server:
$("<div>").attr("data-bind", "template: { name: 'manage-song-template' }").dialog({
// another options
open: function (event, ui) {
var vm = new my.managesongviewmodel(), domNode = this;
vm.loadfromserver().done(function() {
ko.applyBindings(vm, domNode);
jquerybindings();
});
}
})
// another code
And add return statement at the begining of loadfromserver function:
loadfromserver = function (Id) {
return $.post("/song/getsong", "id=1", function (response) {
// code
});
}