Using ckeditor in Knockout as part of an observableArray - javascript

I am trying to add the Rich Text Editor to my Survey system using CKeditor and knockout. I have my ViewModel, which has an observerable array of quesitons. I want to make the Name in each question use the ckeditor. I have look at the post Knockout.js: array parameter in custom binding. And have immplemented that but my OnBlur is not working. The ValueAccessor() is not returning an observable object. So I get an error that string is not a function() on this line of code..
var observable = valueAccessor();
observable($(element).val());
Here is my Html, I am just using a static Id for now on question, and was going to change that after I got this to work for just one question in the array.
<tbody data-bind="foreach: questionModel">
<tr>
<td>
<button data-bind='click: $root.addQuestion' class="btn btn-success" title="Add Question"><i class="icon-plus-sign fontColorWhite"></i></button>
<button data-bind='click: $root.removeQuestion' class="btn btn-danger" title="Remove Question"><i class="icon-minus-sign fontColorWhite"></i></button>
</td>
<td><textarea id="question123" class="RichText" data-bind="richText: Name"></textarea></td>
<td><input type="checkbox" data-bind="checked: AllowComment" /></td>
<td><button data-bind="click: $root.addAnswer" class="btn btn-success" title="Add Answer"><i class="icon-plus-sign fontColorWhite"></i></button></td>
<td>
<div data-bind="foreach: possibleAnswerModel">
<input style="width: 278px" style="margin-bottom: 5px;" data-bind='value: Name' />
<button data-bind='click: $root.removeAnswer' class="btn btn-danger" title="Remove Answer"><i class="icon-minus-sign fontColorWhite"></i></button>
</div>
</td>
</tr>
<tr>
</tbody>
Below is my ViewModel as well as my custom binding....
ko.bindingHandlers.richText = {
init: function (element, valueAccessor, allBindingsAccessor, ViewModel) {
var txtBoxID = $(element).attr("id");
console.log("TextBoxId: " + txtBoxID);
var options = allBindingsAccessor().richTextOptions || {};
options.toolbar_Full = [
['Bold', 'Italic'],
['NumberedList', 'BulletedList', '-', 'Outdent', 'Indent'],
['Link', 'Unlink']
];
//handle disposal (if KO removes by the template binding)
ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
if (CKEDITOR.instances[txtBoxID]) {
CKEDITOR.remove(CKEDITOR.instances[txtBoxID]);
};
});
$(element).ckeditor(options);
//wire up the blur event to ensure our observable is properly updated
CKEDITOR.instances[txtBoxID].focusManager.blur = function () {
console.log("blur");
console.log("Value: " + valueAccessor());
console.log("Value: " + $(element).val());
var observable = valueAccessor();
observable($(element).val());
};
},
update: function (element, valueAccessor, allBindingsAccessor, ViewModel) {
var value = valueAccessor();
console.log("Value Accessor: " + value);
var valueUnwrapped = ko.utils.unwrapObservable(value);
//var val = ko.utils.unwrapObservable(valueAccessor());
console.log("Value: " + valueUnwrapped);
$(element).val(valueUnwrapped);
}
};
function ViewModel(survey) {
// Data
var self = this;
self.StartDate = ko.observable(survey.StartDate).extend({ required: { message: 'Start Date is required' } });
self.EndDate = ko.observable(survey.EndDate).extend({ required: { message: 'End Date is required' } });
self.Name = ko.observable(survey.Name).extend({ required: { message: 'Name is required' } });
self.ButtonLock = ko.observable(true);
self.questionModel = ko.observableArray(ko.utils.arrayMap(survey.questionModel, function(question) {
return { Id: question.QuestionId, Name: ko.observable(question.Name), Sort: question.Sort, IsActive: question.IsActive, AllowComment: question.AllowComment, possibleAnswerModel: ko.observableArray(question.possibleAnswerModel) };
}));
// Operations
self.addQuestion = function () {
self.questionModel.push({
Id: "0",
Name: "",
AllowComment: true,
Sort: self.questionModel().length + 1,
possibleAnswerModel: ko.observableArray(),
IsActive:true
});
};
self.addAnswer = function (question) {
question.possibleAnswerModel.push({
Id: "0",
Name: "",
Sort: question.possibleAnswerModel().length + 1,
IsActive:true
});
};
self.GetBallotById = function (id) {
for (var c = 0; c < self.BallotProjectStandardList().length; c++) {
if (self.BallotProjectStandardList()[c].BallotId === id) {
return self.BallotProjectStandardList()[c];
}
}
return null;
};
self.removeQuestion = function(question) { self.questionModel.remove(question); };
self.removeAnswer = function(possibleAnswer) { $.each(self.questionModel(), function() { this.possibleAnswerModel.remove(possibleAnswer) }) };
self.save = function() {
if (self.errors().length == 0) {
self.ButtonLock(true);
$.ajax("#Url.Content("~/Survey/Create/")", {
data: ko.toJSON(self),
type: "post",
contentType: 'application/json',
dataType: 'json',
success: function(data) { self.successHandler(data, data.success); },
error: function() {
self.ButtonLock(true);
self.errorHandler();
}
});
} else {
self.errors.showAllMessages();
}
};
}
ViewModel.prototype = new ErrorHandlingViewModel();
var mainViewModel = new ViewModel(#Html.Raw(jsonData));
mainViewModel.errors = ko.validation.group(mainViewModel);
ko.applyBindings(mainViewModel);

I figured what I was doing wrong. When I define the observableArray() I was defining the object as ko.observable, however, when I add a question to the array, I was initializing it as a string. So I change that to match and it worked like a champ. Here is the change push.
self.questionModel = ko.observableArray(ko.utils.arrayMap(survey.questionModel, function(question) {
return { Id: question.QuestionId, Name: ko.observable(question.Name), Sort: question.Sort, IsActive: question.IsActive, AllowComment: question.AllowComment, possibleAnswerModel: ko.observableArray(question.possibleAnswerModel) };
}));
// Operations
self.addQuestion = function () {
self.questionModel.push({
Id: "0",
Name: ko.observable(),
AllowComment: true,
Sort: self.questionModel().length + 1,
possibleAnswerModel: ko.observableArray(),
IsActive:true
});
};

Related

KnockoutJS foreach doesn't iterate through collection

For some reason, foreach in Knockout.js doesn't iterate through my observable array.
In my HTML I have this which works perfectly fine with the observable model:
<div class="field-group">
<label class="popup-label" for="email">Email</label>
<span class="email" data-bind="text: masterVM.employeeVM.Email"></span>
</div>
But in the same model, this code doesn't work:
<ul data-bind="foreach: { data: masterVM.employeeVM.Tags, as: 'tag' }">
<li>
<span class="popup-tag" data-bind="text: tag.tagName"><i class="zmdi zmdi-delete"></i></span>
</li>
</ul>
There are two models:
Employee
var observableEmployee = function(id, email, tags) {
var self = this;
self.Id = ko.observable(id);
self.Email = ko.observable(email);
self.Tags = ko.observableArray(ko.utils.arrayMap(tags, function(item) {
return new observableTag(item.Id, item.EmployeeId, item.TagId, item.tagName)
}));
self.errors = ko.validation.group(this, {
deep: true
});
self.isValid = ko.computed(function() {
return self.errors().length > 0 ? false : true;
});
}
and Tag
var observableTag = function(id, employeeId, tagId, tagName) {
var self = this;
self.Id = ko.observable(id);
self.employeeId = ko.observable(employeeId);
self.tagId = ko.observable(tagId);
self.TagName = ko.observable(tagName);
self.errors = ko.validation.group(this, {
live: true
});
self.isValid = ko.computed(function() {
return self.errors().length > 0 ? false : true;
});
}
and handler function:
var employeeHandler = function () {
var self = this;
self.getEmployeeDetails = function (header) {
$.ajax({
url: masterVM.controller.renderEmployeeDetails,
dataType: 'json',
contentType: 'application/json',
type: 'POST',
data: JSON.stringify({ id: header.data("employeeid") }),
success: function (result) {
masterVM.employeeVM = new observableEmployee(
result.model.Id,
result.model.Email,
result.model.Tags
);
ko.applyBindings(masterVM, $("#employee-planning-selected")[0]);
//header.parent().addClass('open');
//header.next().slideDown('normal');
//hideLoader(header);
console.log('get employee details');
$(document).on('click', "div.employee", onNameCardClick);
},
error: function (xhr, ajaxOptions, thrownError) {
alert('Error!');
}
});
}}
In my HTML file
<script>
masterVM = {
controller: {
renderEmployeeDetails: '#(Html.GetActionUrl<EmployeesController>(c => c.RenderEmployeeDetails(0)))'
},
employeeHandler: new employeeHandler(),
employeeVM: new observableEmployee(0, '', '', '', '')
}
ko.applyBindings(masterVM);
</script>
Tried something like this, and still nothing
<!--ko foreach: employeeVM.Tags -->
<span data-bind="text: $data.Tags"></span>
<!-- /ko -->
And no, there are no errors in the console, I have used KnockouJS context debugger which shows me that there are elements in this collection, even when I try to display them as an object it shows me a list of 4 elements.
Knockout version: 2.3.0
1). If you are binding masterVM object in ko.applyBindings(masterVM), you don't need to specify that object again in your data-bindings.
So, it should be
foreach: { data: employeeVM.Tags, as: 'tag' }
And not
foreach: { data: masterVM.employeeVM.Tags, as: 'tag' }
(I'm not sure how the first data-bind="text: masterVM.employeeVM.Email" is working)
2). You don't need to call applyBindings more than once. If you want to update the employee object, you can turn your employeeVM into an observable and keep updating it inside getEmployeeDetails method.
3) Your containerless control flow syntax won't work. (<!--ko foreach: employeeVM.Tags -->). Inside this foreach, $data is the current Tag object in context. So, it should be <span data-bind="text: $data.TagName"></span>
Here's a minimal version of the code. Click on "Run code snippet" to test it. When you click on Update employee button, I'm updating the employeeVM observable and the data gets rendered again. Without calling applyBindings again
var employeeHandler = function() {
var self = this;
self.getEmployeeDetails = function(header) {
var newEmployee = new observableEmployee(0, 'newEmployee#xyz.com', [{
Id: 3,
EmployeeId: 3,
TagId: 3,
tagName: 'Tag Name 3'
}]);
// You need to use employeeVM(newEmployee) instead of employeeVM = newEmployee
// Because employeeVM is an observable.
masterVM.employeeVM(newEmployee);
}
}
var observableEmployee = function(id, email, tags) {
var self = this;
self.Id = ko.observable(id);
self.Email = ko.observable(email);
self.Tags = ko.observableArray(ko.utils.arrayMap(tags, function(item) {
return new observableTag(item.Id, item.EmployeeId, item.TagId, item.tagName)
}));
}
var observableTag = function(id, employeeId, tagId, tagName) {
var self = this;
self.Id = ko.observable(id);
self.employeeId = ko.observable(employeeId);
self.tagId = ko.observable(tagId);
self.TagName = ko.observable(tagName);
}
var masterVM = {
controller: {
renderEmployeeDetails: ''
},
employeeHandler: new employeeHandler(),
// change this to an observable
employeeVM: ko.observable(new observableEmployee(0, 'abc#xyz.com', [{
Id: 1,
EmployeeId: 1,
TagId: 1,
tagName: 'Tag name 1'
}]))
}
ko.applyBindings(masterVM);
document.getElementById("button").addEventListener("click", function(e) {
masterVM.employeeHandler.getEmployeeDetails()
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<div class="field-group">
<label class="popup-label" for="email">Email:</label>
<span class="email" data-bind="text: employeeVM().Email"></span>
</div>
<ul data-bind="foreach: { data: employeeVM().Tags, as: 'tag' }">
<li>
<span class="popup-tag" data-bind="text: tag.employeeId"></span> <br>
<span class="popup-tag" data-bind="text: tag.tagId"></span><br>
<span class="popup-tag" data-bind="text: tag.TagName"></span>
</li>
</ul>
<button id="button">Update employee</button>

Knockout and Semantic UI Multi Select Dropdown with pre-selected values with a collection inside a model

Using Knockout and Semantic UI.
I'm trying to figure out how to get the values selected for my multi select dropdown. The first dropdown works with just single values, but the multi select one dosent. I have an observable array inside another collection:
<tbody id="tbodyelement" data-bind="foreach: groupUserCollection">
<tr>
<td>
<div class="ui selection dropdown fluid">
<input type="hidden" name="groupDD" data-bind="value: group.name">
<i class="dropdown icon"></i>
<div class="default text">Select Group</div>
<div class="menu" data-bind="foreach: $parent.groupCollection">
<div class="item" data-bind="text: $data.name(), attr: {'data-value': $data.id()}"></div>
</div>
</div>
</td>
<td>
<div class="ui multiple selection dropdown long-width" id="multi-select">
<div data-bind="foreach: user">
<input type="hidden" name="userDD" data-bind="value: firstLastName">
</div>
<div class="default text">Select User</div>
<div class="menu" data-bind="foreach: $parent.userCollection">
<div class="item" data-bind="text: $data.firstLastName(), attr: {'data-value': $data.id()}"></div>
</div>
<i class="dropdown icon"></i>
</div>
</td>
</tr>
</tbody>
I have one model groupuser that has a group model in it and a collection of roles.
var groupUser = function (data) {
var self = this;
self.group = ko.mapping.fromJS(data.group),
self.user = ko.observableArray([]),
self.id = ko.observable(data.id),
self.group.subscribe = function () {
showButtons();
},
self.user.subscribe = function () {
// self.user.push(data.user);
showButtons();
}
};
var group = function (data) {
var self = this;
self.id = ko.observable(data.id),
self.name = ko.observable(data.name),
self.project = ko.observable(data.project),
self.projectId = ko.observable(data.projectId),
self.role = ko.observable(data.role),
self.roleId = ko.observable(data.roleId)
};
var user = function (data) {
var self = this;
self.id = ko.observable(data.id),
self.accountId = ko.observable(data.accountId),
self.email = ko.observable(data.email),
self.firstName = ko.observable(data.firstName),
self.lastName = ko.observable(data.lastName),
self.firstLastName = ko.pureComputed({
read: function()
{
return self.firstName() + " " + self.lastName();
}
,
write: function(value)
{
var lastSpacePos = value.lastIndexOf(" ");
if (lastSpacePos > 0) {
self.firstName(value.substring(0, lastSpacePos));
self.lastName(value.substring(lastSpacePos + 1));
}
console.log("firstname: " + self.firstName());
}
}),
};
groupViewModel = {
groupUserCollection: ko.observableArray(),
userCollection: ko.observableArray(),
groupCollection: ko.observableArray()
}
I add the data using this function:
$(data).each(function (index, element) {
var newGroup = new group({
id: element.group.id,
name: element.group.name,
project: element.group.project,
projectId: element.group.projectId,
role: element.group.role,
roleId: element.group.roleId
});
newGroup.id.subscribe(
function () {
newGroupUser.showButtons();
}
);
newGroup.name.subscribe(
function () {
newGroupUser.showButtons();
}
);
var newGroupUser = new groupUser({
group: newGroup,
id: element.id,
});
ko.utils.arrayForEach(element.user, function (data) {
var newUser = new user({
id: data.id,
accountId: data.accountId,
email: data.email,
firstName: data.firstName,
lastName: data.lastName,
});
newUser.id.subscribe(
function () {
newGroupUser.showButtons();
}
);
newUser.firstName.subscribe(
function () {
newGroupUser.showButtons();
}
);
newUser.lastName.subscribe(
function () {
newGroupUser.showButtons();
}
);
newGroupUser.user.push(newUser);
});
groupViewModel.groupUserCollection.push(newGroupUser);
});
I ended up adding in a custom bind to the data-bind on the hidden input and it worked. But now my subscription dosent work when I add values or remove them.
Code that worked:
ko.bindingHandlers.customMultiBind = {
init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
ko.utils.arrayForEach(bindingContext.$data.user(), function (data) {
if (element.value === "")
{
element.value = ko.utils.unwrapObservable(data.id)
}
else {
element.value = element.value + "," + ko.utils.unwrapObservable(data.id)
}
});
}
};

jquery datatable not removing rows when using knockout custom binding

I am attempting to create a custom binding for a knockout jquery datatable. (I was working on an existing SO question someone had posted) everything seems to be working well except when adding a new row. the datatable also adds the rows that were already there. my gues is that the .draw() function on the datatable is not firing. here is the fiddle.
http://jsfiddle.net/LkqTU/35814/
if you fill in the form and click add you will notice the original row is duplicated.
here is my binding.
ko.bindingHandlers.dataTable = {
init: function(element, valueAccessor, allBindingsAccessor) {
var value = valueAccessor(),
rows = ko.toJS(value);
allBindings = ko.utils.unwrapObservable(allBindingsAccessor()),
$element = $(element);
var table = $element.DataTable( {
data: rows,
columns: [
{ data: "id" },
{ data: "firstName" },
{ data: "lastName" },
{ data: "phone" },
{
data: "ssn",
"render": function ( data, type, full, meta ) {
return '<a href="/Medlem/'+data+'">' + data + '<a>';
}
}
]
} );
alert('added');
ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
$element.dataTable().fnDestroy();
});
value.subscribe(function(newValue) {
rows = ko.toJS(newValue)
console.log(rows);
$element.find("tbody tr").remove();
table.rows().remove().draw();
$.each(rows, function( index, row ) {
table.row.add(row).draw()
});
}, null);
}
}
I just noticed if I add set timeout to the part where it adds the rows. then everything works. very odd.
setTimeout(function(){ $.each(rows, function( index, row ) {
table.row.add(row).draw()
});
}, 0);
here is the updated fiddle.
http://jsfiddle.net/LkqTU/35820/
It has specifically to do with the timing of the draw() call. This is because you have a Knockout binding inside your table, so you have both jQuery and Knockout trying to manage this DOM, and they're stepping on each other.
The answer: You don't need to have the table body at all; DataTables will put that in for you.
I've reorganized your code a bit to use the update portion of the bindingHandler for data updates.
ko.bindingHandlers.dataTable = {
init(element, valueAccessor, allBindingsAccessor) {
const $element = $(element);
$element.DataTable({
columns: [{
data: "id"
}, {
data: "firstName"
}, {
data: "lastName"
}, {
data: "phone"
}, {
data: "ssn",
"render": function(data, type, full, meta) {
return '<a href="/Medlem/' + data + '">' + data + '<a>';
}
}]
});
ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
$element.dataTable().fnDestroy();
});
},
update(element, valueAccessor, allBindingsAccessor) {
const rows = ko.toJS(valueAccessor());
const table = $(element).DataTable();
table.clear().rows.add(rows).draw();
}
}
function employee(id, firstName, lastName, phone, ssn) {
var self = this;
this.id = ko.observable(id);
this.firstName = ko.observable(firstName);
this.lastName = ko.observable(lastName);
this.phone = ko.observable(phone);
this.ssn = ko.observable(ssn);
}
function model() {
var self = this;
this.employees = ko.observableArray([
new employee('1', 'Joe', 'Smith', '333-657-4366', '111-11-1111')
]);
this.id = ko.observable('');
this.firstName = ko.observable('');
this.lastName = ko.observable('');
this.phone = ko.observable('');
this.ssn = ko.observable('');
this.add = function() {
self.employees.push(new employee(
this.id(), this.firstName(), this.lastName(), this.phone(), this.ssn()
));
}
}
var mymodel = new model();
$(document).ready(function() {
ko.applyBindings(mymodel);
});
<link href="//cdn.datatables.net/1.10.13/css/jquery.dataTables.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="//cdn.datatables.net/1.10.13/js/jquery.dataTables.min.js"></script>
<table data-bind="dataTable: employees">
<thead>
<tr>
<th>Id</th>
<th>First</th>
<th>Last</th>
<th>Phone</th>
<th>ssn</th>
</tr>
</thead>
</table>
<p style="padding-top: 20px;">
Id:
<input data-bind="textInput: id" />
</p>
<p>
First:
<input data-bind="textInput: firstName" />
</p>
<p>
Last:
<input data-bind="textInput: lastName" />
</p>
<p>
phone:
<input data-bind="textInput: phone" />
</p>
<p>
ssn:
<input data-bind="textInput: ssn" />
</p>
<p>
<input type="button" value="add employee" data-bind="click: add" />
</p>

Pass html input value to knockout view

please help me with this issue that I have.
Embedded in CRM on Demand I have a view that needs to take values from CRM input fields to perform a search against CRM through web service and to show a view if duplicate records are found.
Here I have the code into some libraries against CRM {crm.context.ts}:
/*
* Context Helpers
*/
declare var epmcrm: any;
class context {
private getParameterByName(name) {
name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
results = regex.exec(location.search);
return results == null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
}
config: { objects: { [key: string]: any } } = {
objects: {
"Homepage": {
name: "Homepage"
},
"Task": {
name: "Task",
idParam: "TaskDetailForm.Id",
screens: {
"/OnDemand/user/TaskDetail": "Detail",
"/OnDemand/user/TaskEdit": "Edit"
}
},
"Account": {
name: "Account",
idParam: "AccountDetailForm.Id",
screens: {
"/OnDemand/user/TaskDetail": "Detail",
"/OnDemand/user/TaskEdit": "Edit"
}
},
"User": {
name: "User",
idParam: "UserDetailForm.Id",
screens: {
"/OnDemand/user/UserDetail": "Detail",
"/OnDemand/user/UserEdit": "Edit"
}
},
"Opportunity": {
name: "Opportunity",
idParam: "OpportunityDetailForm.Id",
screens: {
"/OnDemand/user/OpportunityDetail": "Detail",
"/OnDemand/user/OpportunityEdit": "Edit"
}
},
"Contact": {
name: "Contact",
idParam: "ContactDetailForm.Id",
screens: {
"/OnDemand/user/ContactDetail": "Detail",
"/OnDemand/user/ContactEdit": "Edit",
// "/OnDemand/user/ContactInsert": "Create"
}
}
}
};
private knownPaths: { [key: string]: any } = {
"/OnDemand/user/Homepage": this.config.objects["Homepage"],
"/OnDemand/user/TaskDetail": this.config.objects["Task"],
"/OnDemand/user/TaskEdit": this.config.objects["Task"],
"/OnDemand/user/AccountDetail": this.config.objects["Account"],
"/OnDemand/user/AccountEdit": this.config.objects["Account"],
"/OnDemand/user/ContactDetail": this.config.objects["Contact"],
"/OnDemand/user/ContactEdit": this.config.objects["Contact"],
// "/OnDemand/user/ContactInsert": this.config.objects["Contact"],
"/OnDemand/user/UserDetail": this.config.objects["User"],
"/OnDemand/user/UserEdit": this.config.objects["User"],
"/OnDemand/user/OpportunityEdit": this.config.objects["Opportunity"]
//"/OnDemand/user/CustomObj2": mapping to custom objects here is important!
};
webServiceUrl: string = null;
screen: string = null;
objectType: string = null;
objectId: string = null;
ssoToken: string = null;
moduleRoot: string = null;
rowId: string = null;
commentsAction: string = null;
status: string = null;
//crm parameters to built the task link;
account: string = null;
accountId: string = null;
contact: string = null;
contactId: string = null;
quote: string = null;
quoteId: string = null;
order: string = null;
orderId: string = null;
oppty: string = null;
opptyId: string = null;
lead: string = null;
leadId: string = null;
step: string = null;
//crm contact
lastName: string = null;
firstName: string = null;
email: string = null;
constructor() {
// pick out the info from the url
this.webServiceUrl = "https://" + window.location.hostname + "/Services/Integration";
// get the SSO token from the global variable defined in the web applet
this.ssoToken = epmcrm.ssoToken;
// get the module root from the global variable defined in the web applet
this.moduleRoot = epmcrm.moduleRoot;
this.rowId = epmcrm.rowId;
this.commentsAction = epmcrm.commentsAction;
this.status = epmcrm.status;
this.step = epmcrm.step;
//crm parameters to built the task link;
this.account = epmcrm.account;
this.accountId = epmcrm.accountId;
this.contact = epmcrm.contact;
this.contactId = epmcrm.contactId;
this.quote = epmcrm.quote;
this.quoteId = epmcrm.quoteId;
this.order = epmcrm.order;
this.orderId = epmcrm.orderId;
this.oppty = epmcrm.oppty;
this.opptyId = epmcrm.opptyId;
this.lead = epmcrm.lead;
this.leadId = epmcrm.leadId;
//crm Contact
$("#ContactEditForm\\.First\\ Name").on("change", function () {
this.firstName = (<HTMLInputElement>document.getElementById("ContactEditForm.First Name")).value;
});
$("#ContactEditForm\\.Email\\ Address").on("change", function () {
this.email = (<HTMLInputElement>document.getElementById("ContactEditForm.Email Address")).value;
});
$("#ContactEditForm\\.Last\\ Name").on("change", function () {
this.lastName = (<HTMLInputElement>document.getElementById("ContactEditForm.Last Name")).value;
});
// attempt to discover contextual information
var pathname = window.location.pathname;
if (this.knownPaths[pathname]) {
var obj = this.knownPaths[pathname];
this.objectType = obj.name;
if (obj.idParam) {
this.objectId = this.getParameterByName(obj.idParam);
}
if (obj.screens) {
this.screen = obj.screens[pathname];
}
}
}
}
export = context;
In the view models I have what should give me the results into knockout observables which should than mirror CRM field and with this results I would perform a search and return or not some results:
`contactSearch.ts`
import ko = require("knockout");
import context = require("libs/crm.context");
import contacts = require("libs/crm.contacts");
$("#BTN_TB_ContactNewForm_Save").hide();
$("#BTN_TB_ContactNewForm_Save_idx_1").hide();
//$("#ContactEditForm\\.First\\ Name").on("change", assignFName);
//$("#ContactEditForm\\.Last\\ Name").on("change", assignLName);
//function assignFName() {
// var firstName = (<HTMLInputElement>document.getElementById("ContactEditForm.First Name")).value;
// alert(firstName);
//}
//function assignLName() {
// var lastName = (<HTMLInputElement>document.getElementById("ContactEditForm.Last Name")).value;
// alert(lastName);
//}
//function assignEmail() {
// var Email = (<HTMLInputElement>document.getElementById("ContactEditForm.Email Address")).value
// alert(Email);
//}
//var contactViewModel = function () {
// var self = this;
// self.validContacts = ko.observableArray([]);
// self.addContact = function (validContact) {
// self.validContacts.puch(validContact);
// $.ajax({
// data: ko.toJSON(this),
// contentType: 'application/json',
// success: function (result) {
// validContact.fName(result.
// }
// });
// }
//}
class contactSearch {
private _context: context = new context();
private _contacts: contacts = new contacts(this._context.webServiceUrl);
private _firstName = this._context.firstName;
private _lastName = this._context.lastName;
private _email = this._context.email;
vFName = ko.observable(this._firstName);
vLName = ko.observable(this._lastName);
vEmail = ko.observable(this._email);
//email = ko.computed({
// read: () => $("#ContactEditForm\\.Email\\ Address").on("change", function () {
// })
//})
////})
//lName = ko.observable("");
////email = ko.computed(function () {
//// assignEmail();
//})
isSearching: KnockoutObservable<boolean> = ko.observable(false);
searchValue = ko.computed({
read: () => ("[ContactEmail]~=" + "'" + "" + "'" + " AND [ContactFirstName]~=" + "'" + this.vFName() + "'" + " AND [ContactLastName]~=" + "'" + this.vLName() + "'")
});
contactSearchResults: KnockoutObservableArray<IContact> = ko.observableArray([]);
doValidation() {
$("#ContactEditForm\\.Email\\ Address").on("change", function () {
})
}
doContactSearch() {
this.isSearching(true);
this.contactSearchResults([]);
this._contacts
.find(this.searchValue(), ["Id", "ContactFirstName", "ContactLastName", "ContactEmail", "AccountId", "AccountName"])
.done((results: IContact[]) => {
if (results.length > 0) {
this.contactSearchResults(results);
this.isSearching(false);
}
else {
$("#BTN_TB_ContactNewForm_Save").show();
$("#BTN_TB_ContactNewForm_Save_idx_1").show();
alert("# of matching results= " + results.length);
}
});
}
bindTR(element): void {
/*
* Replicate the CRMOD method of hover styles
*/
var $element = $(element);
$element.hover(
() => {
$element.attr("_savedBGColor", $element.css("background-color"));
$element.css("background-color", "#d3dde6");
},
() => {
$element.css("background-color", $element.attr("_savedBGColor"));
$element.attr("_savedBGColor", "");
}
);
}
bindLink(element): void {
var $element = $(element);
$element.click(
() => {
window["doNavigate"]('ContactDetail?ocTitle=' + encodeURIComponent(this.vLName()) + '&ContactDetailForm.Id=' + this.contactSearchResults["Id"] + '&OCTYPE=', true, this, null)
},
() => {
$element.css("text-decoration", "underline");
}
);
}
}
export = contactSearch;
David,
I have created the observables and I'm binding those here in the HTML view model, but the source for my data are those HTMLInputElement and I don't know how pass the values to the observables.
<p>Email <input data-bind="value: vEmail" />
<span data-bind="text: vEmail"></span>
<span data-bind="text: vFName"></span>
<span data-bind="text: vLName"></span>
<p>Enter the search spec: <textarea data-bind="value: searchValue" />
<button type="button" data-bind="click: validation, click: doContactSearch">Go</button></p>
<table class="list clist" cellspacing="0">
<thead>
<tr>
<th class="m">
<th class="m">
<th>Id</th>
<th>First Name</th>
<th>Last Name</th>
<th>Email</th>
<th>Account Id</th>
<th>Account</th>
</tr>
</thead>
<tbody data-bind="visible: isSearching">
<tr>
<td colspan="99" style="text-align:center;">Searching, please wait...</td>
</tr>
</tbody>
<tbody data-bind="foreach: contactSearchResults, afterRender: bindTR, click: bindLink">
<tr>
<td class="lrml"> </td>
<td> </td>
<td data-bind="text: Id"></td>
<td data-bind="text: ContactFirstName"></td>
<td data-bind="text: ContactLastName"></td>
<td data-bind="text: ContactEmail"></td>
<td data-bind="text: AccountId"></td>
<td data-bind="text: AccountName"></td>
</tr>
</tbody>
</table>
I have also this file that create the dependency with CRM:
var epmcrm;
((epmcrm) => {
if (!epmcrm["moduleRoot"])
throw new Error("epmcrm global variable not configured");
require.config({
baseUrl: epmcrm.moduleRoot + "/scripts/app",
paths: {
// define the libs here
// 1. External
"jquery": "//cdnjs.cloudflare.com/ajax/libs/jquery/1.11.1/jquery.min",
"jquery-ui.theme": "//cdnjs.cloudflare.com/ajax/libs/jqueryui/1.11.2/jquery-ui.theme.css",// recently added
"knockout": "//cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min",
"text": "//cdnjs.cloudflare.com/ajax/libs/require-text/2.0.10/text",
"json2": "//cdnjs.cloudflare.com/ajax/libs/json2/20130526/json2.min",
"knockout.mapping": "//cdnjs.cloudflare.com/ajax/libs/knockout.mapping/2.4.1/knockout.mapping", // added by daniel
// 2. Internal
"koExtensions": "../libs/knockout-extensions",
"libs/crm.tasks": "../libs/crm.tasks",
"libs/crm.accounts": "../libs/crm.accounts",
"libs/crm.contacts": "../libs/crm.contacts",
"libs/crm.users": "../libs/crm.users",
"libs/crm.session": "../libs/crm.session",
"libs/crm.context": "../libs/crm.context",
"libs/crm.objects": "../libs/crm.objects",
"libs/crm.utilities": "../libs/crm.utilities",
"libs/crm.viewEngine": "../libs/crm.viewEngine",
"libs/crm.viewmodelEngine": "../libs/crm.viewmodelEngine"
},
shim: {
"knockout": {
deps: ["json2"]
}
}
});
require(["knockout", "knockout.mapping", "koExtensions"],
(ko: KnockoutStatic, komap: KnockoutMapping) => {
ko.mapping = komap;
ko.applyBindings({}, document.getElementById("QuoteWebAppletContainer"));
});
})(epmcrm || (epmcrm = {}));
I'm coming with the solution eventually, because I got what I need.
This were changed like this:
$("#ContactEditForm\\.First\\ Name").on("change", () => {
this.firstName((<HTMLInputElement>document.getElementById("ContactEditForm.First Name")).value);
});
$("#ContactEditForm\\.Email\\ Address").on("change", () => {
this.email((<HTMLInputElement>document.getElementById("ContactEditForm.Email Address")).value);
});
$("#ContactEditForm\\.Last\\ Name").on("change", () => {
this.lastName((<HTMLInputElement>document.getElementById("ContactEditForm.Last Name")).value);
});
this three variables I changed to be observable properties:
lastName: KnockoutObservable<string> = ko.observable("");
firstName: KnockoutObservable<string> = ko.observable("");
email: KnockoutObservable<string> = ko.observable("");
And here I've made some other changes:
vFName = this._context.firstName;
vLName = this._context.lastName;
vEmail = this._context.email;
isSearching: KnockoutObservable<boolean> = ko.observable(false);
searchValue = ko.computed(() => {
return ("[ContactEmail]~=" + "'" + ko.unwrap(this._context.email) + "'" + " AND [ContactFirstName]~=" + "'" + ko.unwrap(this._context.firstName) + "'" + " AND [ContactLastName]~=" + "'" + ko.unwrap(this._context.lastName) + "'")
});

Knockout.js + Bootstrap - strange thing happening

I'm using Twitter Bootstrap together with knockout.js.
I have an orders page where the cashier can choose a customer and products that the customer wants to buy. However, I get some very strange behavior. When I add one product to the cart the correct function addToCart is called, but also the function removeFromCart is called without me telling the program to call it. I guess something is happening because I use Bootstrap modals.
Please help. Here is the fiddle http://jsfiddle.net/rY59d/4/ .
HTML Code:
<div id="create-order-main">
<h2>Create new order</h2>
<a class="btn btn-primary" data-toggle="modal" data-target="#select-products2"><b>+</b> Add products</a>
<div>
<div id="create-order-select-products" data-bind="with: productVM">
<div class="modal fade" id="select-products2" tabindex="-1" role="dialog" aria-labelledby="selectProducts2Label" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">
×
</button>
<h4 class="modal-title">Add products</h4>
</div>
<div class="modal-body">
<table class="table table-bordered table-with-records" data-bind="triggerUpdate: Products, visible: Products().length > 0">
<thead>
<tr>
<th>Id</th>
<th>Name</th>
<th>Price</th>
<th>Quantity</th>
<th></th>
</tr>
</thead>
<tbody data-bind="foreach: filteredProducts2">
<tr>
<td data-bind="text: id"></td>
<td data-bind="text: name"></td>
<td data-bind="text: price"></td>
<td><input type="number" min="0" step="1" value="1"></td>
<td data-bind="attr: { value: $index }, click: $parent.selectedProduct2"><a class="btn btn-primary" data-bind="click: $parent.addToCart">Add to cart</a></td>
</tr>
</tbody>
</table>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">
Cancel
</button>
<button type="submit" class="btn btn-primary" data-dismiss="modal">
Choose
</button>
</form>
</div>
</div>
<!-- /.modal-content -->
</div>
<!-- /.modal-dialog -->
</div>
<!-- /.modal -->
</div>
<div data-bind="with: productVM">
<table class="table table-bordered table-with-records" data-bind="triggerUpdate: cart, visible: cart().length > 0">
<thead>
<tr>
<th>Id</th>
<th>Name</th>
<th>Price</th>
<th></th>
</tr>
</thead>
<tbody data-bind="foreach: cart">
<tr>
<td data-bind="text: id"></td>
<td data-bind="text: name"></td>
<td data-bind="text: price"></td>
<td data-bind="attr: { value: $index }, click: $parent.selectedProductInOrder"><a class="btn btn-primary" data-bind="click: $parent.removeFromCart($data)">Remove</a></td>
</tr>
</tbody>
</table>
</div>
</div>
JavaScript Code:
/**
* -----------
* Viewmodels.js
* -----------
*
* Contains Knockout.js Viewmodels
*
*/
// CustomerViewModel starts here
var CustomerViewModel = function () {
var self = this;
var stringStartsWith = function (string, startsWith) {
string = string || "";
if (startsWith.length > string.length)
return false;
return string.substring(0, startsWith.length) === startsWith;
};
self.name = ko.observable("");
self.surname = ko.observable("");
self.email = ko.observable("");
self.query = ko.observable();
self.Customers = ko.observableArray();
self.filterId = ko.observable("");
self.filterName = ko.observable("");
self.filterSurname = ko.observable("");
// Used for search in "Create Order" view
self.filterId2 = ko.observable("");
self.filterName2 = ko.observable("");
self.filterSurname2 = ko.observable("");
function Customer(id, name, surname, email) {
this.id = id;
this.name = name;
this.surname = surname;
this.email = email;
}
self.selectedCustomer = ko.observable(null);
// Used for search in "Create Order" view
self.selectedCustomer2 = ko.observable(null);
self.getId = function () {
var idCounter;
if (self.Customers().length === 0) {
idCounter = 0;
} else {
idCounter = self.Customers()[self.Customers().length - 1]['id'];
}
return (++idCounter);
};
$.getJSON("api/customers", function (data) {
self.Customers(data);
});
self.Customers.push(new Customer(1,"John","Smith","john#smith.com"));
self.Customers.push(new Customer(2,"Maria","Jones","maria#jones.com"));
self.Customers.push(new Customer(3,"Alexander","Stevenson","alexander#stevenson.com"));
self.clearSearchCustomers = function () {
self.filterId("");
self.filterName("");
self.filterSurname("");
};
// Used in the "Create new Order" view
self.clearSearchCustomers2 = function () {
self.filterId2("");
self.filterName2("");
self.filterSurname2("");
self.selectedCustomer2("");
};
self.selectCustomer = function () {
self.selectedCustomer(this);
};
self.chooseCustomerInSearch = function () {
$('#select-customer2').modal('toggle');
};
self.createNewCustomer = function () {
var customer = new Customer(self.getId(), self.name(), self.surname(), self.email());
$.ajax({
type: "POST",
url: 'api/customers',
data: ko.toJSON({
data: customer
}),
success: function (result) {
self.Customers.push(customer);
self.name("");
self.surname("");
self.email("");
},
error: function (err) {
alert(err.status + " - " + err.statusText);
}
});
$('#create-customer').modal('toggle');
};
self.deleteItem = function ($this) {
$.ajax({
type: "DELETE",
url: 'api/customers/' + this.id,
success: function (result) {
self.Customers.remove($this);
$('#delete-customer').modal('toggle');
},
error: function (err) {
alert(err.status + " - " + err.statusText);
}
});
};
self.callEditCustomerFromViewCustomer = function () {
$('#display-customer').modal('toggle');
$('#edit-customer').modal('toggle');
};
self.editCustomer = function ($this) {
var customer = self.selectedCustomer();
$.ajax({
type: "PUT",
url: 'api/customers/' + this.id,
contentType: 'application/json',
data: ko.toJSON({
data: customer
}),
success: function (result) {
self.Customers.remove($this);
self.Customers.push($this);
$('#edit-customer').modal('toggle');
},
error: function (err) {
alert(err.status + " - " + err.statusText);
}
});
};
self.filteredCustomer = ko.computed(function () {
var filterTextId = self.filterId().toLowerCase(),
filterTextName = self.filterName().toLowerCase(),
filterTextSurname = self.filterSurname().toLowerCase();
if (!filterTextId && !filterTextName && !filterTextSurname) {
return self.Customers();
} else {
if (self.Customers() != 'undefined' && self.Customers() !== null && self.Customers().length > 0) {
return ko.utils.arrayFilter(self.Customers(), function (item) {
return (stringStartsWith(item.id.toLowerCase(), filterTextId) && stringStartsWith(item.name.toLowerCase(), filterTextName) && stringStartsWith(item.surname.toLowerCase(), filterTextSurname));
});
}
}
});
// Used for the "Create New Order" view
self.filteredCustomer2 = ko.computed(function () {
var filterTextId2 = self.filterId2().toLowerCase();
var filterTextName2 = self.filterName2().toLowerCase();
var filterTextSurname2 = self.filterSurname2().toLowerCase();
if (!filterTextId2 && !filterTextName2 && !filterTextSurname2) {
return self.Customers();
} else {
if (self.Customers() != 'undefined' && self.Customers() !== null && self.Customers().length > 0) {
return ko.utils.arrayFilter(self.Customers(), function (item) {
return (stringStartsWith(item.id.toLowerCase(), filterTextId2) && stringStartsWith(item.name.toLowerCase(), filterTextName2) && stringStartsWith(item.surname.toLowerCase(), filterTextSurname2));
});
}
}
});
};
// Product View Model starts here
var ProductViewModel = function () {
var stringStartsWith = function (string, startsWith) {
string = string || "";
if (startsWith.length > string.length)
return false;
return string.substring(0, startsWith.length) === startsWith;
};
function Product(id, name, price) {
this.id = id;
this.name = name;
this.price = price;
}
var self = this;
self.name = ko.observable("");
self.price = ko.observable("");
self.filterId = ko.observable("");
self.filterName = ko.observable("");
self.filterPrice = ko.observable("");
self.selectedProduct = ko.observable(null);
self.Products = ko.observableArray();
// used for "create new order - add items" view
self.filterProductId2 = ko.observable("");
self.filterProductName2 = ko.observable("");
self.filterProductPrice2 = ko.observable("");
self.selectedProduct2 = ko.observable(null);
self.selectedProductInOrder = ko.observable("");
self.cart = ko.observableArray("");
self.addToCart = function () {
alert("Item added to cart");
self.cart.push(this);
};
self.removeFromCart = function ($this) {
alert("this is a test");
// self.cart.remove($this);
};
self.getId = function () {
var idCounter;
if (self.Products().length === 0) {
idCounter = 0;
} else {
idCounter = self.Products()[self.Products().length - 1]['id'];
}
return (++idCounter);
};
self.clearSearchProducts = function () {
self.filterId("");
self.filterName("");
self.filterPrice("");
};
self.clearSearchProducts2 = function () {
self.filterProductId2("");
self.filterProductName2("");
self.filterProductPrice2("");
};
$.getJSON("api/products", function (data) {
self.Products(data);
});
self.Products.push(new Product(1,"product 1", "300"));
self.Products.push(new Product(2,"product 2", "400"));
self.Products.push(new Product(3,"product 3", "500"));
self.Products.push(new Product(4,"product 4", "600"));
self.createNewProduct = function () {
var product = new Product(self.getId(), self.name(), self.price());
$.ajax({
type: "POST",
url: 'api/products',
data: ko.toJSON({
data: product
}),
success: function (result) {
self.Products.push(product);
self.name("");
self.price("");
},
error: function (err) {
alert(err.status + " - " + err.statusText);
}
});
$('#create-product').modal('toggle');
};
self.deleteItem = function ($this) {
$.ajax({
type: "DELETE",
url: 'api/products/' + this.id,
success: function (result) {
self.Products.remove($this);
$('#delete-product').modal('toggle');
},
error: function (err) {
alert(err.status + " - " + err.statusText);
}
});
};
self.callEditProductFromViewProduct = function () {
$('#display-product').modal('toggle');
$('#edit-product').modal('toggle');
};
self.editProduct = function ($this) {
var product = self.selectedProduct();
$.ajax({
type: "PUT",
url: 'api/products/' + this.id,
contentType: 'application/json',
data: ko.toJSON({
data: product
}),
success: function (result) {
self.Products.remove($this);
self.Products.push($this);
$('#edit-product').modal('toggle');
},
error: function (err) {
alert(err.status + " - " + err.statusText);
}
});
};
self.filteredProducts = ko.computed(function () {
var filterTextId = self.filterId().toLowerCase(),
filterTextName = self.filterName().toLowerCase(),
filterTextPrice = self.filterPrice().toLowerCase();
if (!filterTextId && !filterTextName && !filterTextPrice) {
return self.Products();
} else {
if (self.Products() !== 'undefined' && self.Products() !== null && self.Products().length > 0) {
return ko.utils.arrayFilter(self.Products(), function (item) {
return (stringStartsWith(item.id.toLowerCase(), filterTextId) && stringStartsWith(item.name.toLowerCase(), filterTextName) && stringStartsWith(item.price.toLowerCase(), filterTextPrice));
});
}
}
});
// used for "create new order - add item" view
self.filteredProducts2 = ko.computed(function () {
var filterProductTextId2 = self.filterProductId2().toLowerCase(),
filterProductTextName2 = self.filterProductName2().toLowerCase(),
filterProductTextPrice2 = self.filterProductPrice2().toLowerCase();
if (!filterProductTextId2 && !filterProductTextName2 && !filterProductTextPrice2) {
return self.Products();
} else {
if (self.Products() !== 'undefined' && self.Products() !== null && self.Products().length > 0) {
return ko.utils.arrayFilter(self.Products(), function (item) {
return (stringStartsWith(item.id.toLowerCase(), filterProductTextId2) && stringStartsWith(item.name.toLowerCase(), filterProductTextName2) && stringStartsWith(item.price.toLowerCase(), filterProductTextPrice2));
});
}
}
});
};
// CustomerOrderViewModel starts here
var CustomerOrderViewModel = function () {
function CustomerOrder(id, date, customer, details) {
this.id = id;
this.date = name;
this.customer = customer;
this.details = details;
}
var self = this;
self.id = ko.observable("");
self.date = ko.observable();
self.customer = ko.observable("");
self.details = ko.observable("");
self.selectedOrder = ko.observable(null);
self.CustomerOrders = ko.observableArray("");
var newOrder = {
id: 1,
date: "10/10/20",
customer: "ThecUstomeRhere",
details: "sajdasdj"
};
self.createOrder = function () {
alert("Order is created!")
};
self.CustomerOrders.push(newOrder);
self.callEditOrderFromViewOrder = function () {
$('#display-order').modal('toggle');
$('#edit-order').modal('toggle');
};
self.deleteItem = function ($this) {
$.ajax({
type: "DELETE",
url: 'api/orders/' + this.id,
success: function (result) {
self.CustomerOrders.remove($this);
$('#delete-order').modal('toggle');
},
error: function (err) {
$('#delete-order').modal('toggle');
alert(err.status + " - " + err.statusText);
}
});
};
self.editOrderItem = function ($this) {
var selectedCustomerOrder = self.selectedOrder();
$.ajax({
type: "PUT",
url: 'api/orders/' + this.id,
contentType: 'application/json',
data: ko.toJSON({
data: selectedCustomerOrder
}),
success: function (result) {
self.CustomerOrders.remove($this);
self.CustomerOrders.push($this);
$('#edit-order').modal('toggle');
},
error: function (err) {
alert(err.status + " - " + err.statusText);
}
});
};
};
var masterVM = {
customerVM: new CustomerViewModel(),
productVM: new ProductViewModel(),
customerOrderVM: new CustomerOrderViewModel()
};
ko.applyBindings(masterVM);
Your binding for the removeFromCart is wrong. It calls the function when you bind, which happens when the cart observable array changes as it is in a foreach binding.
Replace click: $parent.removeFromCart($data)
With click: $parent.removeFromCart
Demo

Categories