TinyMCE4 will be disappeared when it has been bund to OData property - sapui5

I have a smart form and it has a container for fetching a RichTextEditor in:
<smartForm:GroupElement label="{/#Report/Detail/#sap:label}" visible="{= (${appView>/appMode} === 'edit') }">
<VBox id="RichTextEditorContainer" visible="{= (${appView>/appMode} !== 'review') }" app:objectId="{ path:'Id', events: { change: '.onBindingObjectChange'}}" width="100%">
<!-- Insert RichTextEditor by JS-->
</VBox>
</smartForm:GroupElement>
As it has been said here that:
Make sure you destroy the RichTextEditor instance instead of hiding it and create a new one when you show it again.
I don't make the editor in xml and by an event inject it:
onBindingObjectChange: function (oEvent) {
if (this._oRTXE) {
this._oRTXE.destroy();
}
var oBox = this.getView().byId("RichTextEditorContainer");
oBox.removeAllItems();
this._oRTXE = new RichTextEditor({
value: "{Detail}",
editable: true,
height: "120px",
width: "100%",
wrapping: false,
editorType: "TinyMCE4",
showGroupClipboard: false,
showGroupFontStyle: false,
showGroupStructure: false,
showGroupTextAlign: false
});
oBox.insertItem(this._oRTXE);
}
The problem is, when user tries to type fast, it shows <p>xyz</p> for a second and then the text editor will be disappeared. Please look at the following picture:
As a work around if I remove value: "{Detail}" (that makes binding) then this problem will not happen. Also, if I change its binding to a JSON model also this error won't happen.

The problem is the change event of the RichTextEditor. While it has been written in the documentation that change event happen after leaving focus or press enter but it will also happen when user starts for typing after first enter focus. Here is my work around. Bind the RichTextEditor to JSON model, and update oData by a customized event.
<smartForm:GroupElement label="{/#Report/Detail/#sap:label}" visible="{= (${appView>/appMode} === 'edit') }">
<VBox id="RichTextEditorContainer" visible="{= (${appView>/appMode} !== 'review') }" app:objectId="{ path:'Id', events: { change: '.onBindingObjectChange'}}"
app:detail="{ path:'Detail', events: { change: '.onBindingTextChange'} }"
width="100%">
<!-- Insert RichTextEditor by JS-->
</VBox>
</smartForm:GroupElement>
/**
* Event is fired when the data binding happen on RichTextEditor for object id
* #public
* #param {sap.ui.base.Event} oEvent pattern match event of data changed
*/
onBindingObjectChange: function (oEvent) {
var oBox = this.getView().byId("RichTextEditorContainer");
if (this._oRTXE && this._oRTXE.data("objectId") === oBox.data("objectId")) {
return;
} else if(this._oRTXE) {
this._oRTXE.destroy();
}
oBox.removeAllItems();
this._oRTXE = new RichTextEditor({
value: "{viewModel>/Detail}",
editable: true,
height: "120px",
width: "100%",
wrapping: false,
editorType: "TinyMCE4",
showGroupClipboard: false,
showGroupFontStyle: false,
showGroupStructure: false,
showGroupTextAlign: false,
change: this.onTextChanged.bind(this)
}).attachBrowserEvent("focusin", () => {this._oRTXE.bHasFocus = true;})
.attachBrowserEvent("focusout", () => {this._oRTXE.bHasFocus = false; this._checkWaitingChanges();});
this._oRTXE.data("objectId", oBox.data("objectId"));
oBox.insertItem(this._oRTXE);
},
/**
* Event is fired when the data binding happen on RichTextEditor for text
* #public
* #param {sap.ui.base.Event} oEvent pattern match event of data changed
*/
onBindingTextChange: function (oEvent) {
var oBox = this.getView().byId("RichTextEditorContainer");
this.getModel("viewModel").setProperty("/Detail", oBox.data("detail"));
},
/**
* Event is fired when the text changed on RichTextEditor by user
* #public
* #param {sap.ui.base.Event} oEvent pattern match event of text changed
*/
onTextChanged: function (oEvent) {
this.getModel("viewModel").setProperty("/LastDetail", oEvent.getParameter("newValue"));
this._oRTXE.bWaitingChanges = true;
if(this._oRTXE.bHasFocus === false){
this._oRTXE.bWaitingChanges = false;
var sNewValue = oEvent.getParameter("newValue"),
oBox = this.getView().byId("RichTextEditorContainer"),
oContext = oBox.getBindingContext(),
oModel = oContext.getModel(),
sBindingPath = oContext.getPath() + "/Detail";
oModel.setProperty(sBindingPath, sNewValue);
}
},
// Just checks if there is any changes that has not yet been written in the odata model
_checkWaitingChanges: function(){
if(this._oRTXE.bWaitingChanges === true){
this._oRTXE.bWaitingChanges = false;
var sNewValue = this.getModel("viewModel").getProperty("/LastDetail"),
oBox = this.getView().byId("RichTextEditorContainer"),
oContext = oBox.getBindingContext(),
oModel = oContext.getModel(),
sBindingPath = oContext.getPath() + "/Detail";
oModel.setProperty(sBindingPath, sNewValue);
}
}

Related

Suggestion Items not appearing after Value State Change in sap.m Table input

I have a SuggestionItems on sap.m table input which is not appearing after value state change.
Please note that SAPUI5 Version used is 1.38.37
I have a sap.m table with columns with Suggestion List enabled. User has to select customer number from that available suggestion list. Also he can enter manually.
Now there are list of validations depending on user input.
XML.view
<items>
<ColumnListItem>
<cells>
<Input id="idCustNumber" type="Text" value="{path:'createJSON>CUSTNO'}" change="onCustValChange" liveChange="onLiveChangeCustNo"
startSuggestion="3" maxSuggestionWidth="400px" showSuggestion="true" suggestionItemSelected="OnSuggestionSelectedCustNumber"
suggest="handleSuggestCustNo" suggestionItems="{path:'ITEMVALJSON>/items', templateShareable:false}">
<suggestionItems>
<core:Item key="{ITEMVALJSON>CustNoAndName}" text="{ITEMVALJSON>CustNoAndName}"/>
</suggestionItems>
</Input>
Controller.js
handleSuggestCustNo: function (oEvent) {
var sTerm = oEvent.getParameter("suggestValue");
oEvent.getSource().setFilterFunction(function (sTerm, oItem) {
// A case-insensitive 'string contains' style filter
return oItem.getText().match(new RegExp(sTerm, "i"));
});
},
onCustValChange: function (oEvent) {
var oInput = oEvent.getSource();
this._validateCustValInput(oInput);
},
_validateCustValInput: function (oInput) {
var oBinding = oInput.getBinding("suggestionItems");
var value = oInput.getValue();
if (value !== "") {
var filters = [new Filter("CustNo", sap.ui.model.FilterOperator.EQ, value)];
var found = oBinding.filter(filters).getLength();
if (found === 0) {
oInput.setValueState(sap.ui.core.ValueState.Error);
this.sSelectedTableRow.ERRORFLAG = "X";
oInput.setValueStateText("Customer " + "'" + value + "' is invalid for GCC: " + "'" + this.Header.Cntry + "'");
} else {
oInput.setValueState(sap.ui.core.ValueState.None);
this.sSelectedTableRow.ERRORFLAG = "";
}
} else {
oInput.setValueState(sap.ui.core.ValueState.None);
this.sSelectedTableRow.ERRORFLAG = "";
}
},
Now issue is, if validation is failing, I am setting the value state to error. then after value change, suggestion list is not appearing.
Suggestion List Error
The problem is not the validation itself (or the SAPUI5 version) but in this line of code:
var found = oBinding.filter(filters).getLength();
Here you are filtering out your suggestionItems aggregation.... That's why they stop working: the ITEMVALJSON model is only filled with the chosen and validated value. Try to cancel a letter of the bound value in the input and you will see what I mean.
By the way I noticed two more things: you are filtering the wrong property (but maybe is only a matter of cut and paste) and your handleSuggestCustNo function is probably unnecessary. You can set the filter function (setFilterFunction) only once for your input in the initialization phase.
Hope this helps.
My (minimal) working code,I tried it with 1.38.xx
<Table id="table" items="{ path: '/items'}">
<columns>
<Column>
<Text text="ItemNo"/>
</Column>
<Column>
<Text text="Name"/>
</Column>
</columns>
<ColumnListItem>
<cells>
<Text text="{ItemNo}"/>
<Input id="idCustNumber" placeholder="Suggestion test 2" type="Text" _value="{path:'localModel>CUSTNO'}" change="onCustValChange"
maxSuggestionWidth="400px" showSuggestion="true" suggest="handleSuggestCustNo"
suggestionItems="{path:'localModel>/items', templateShareable:true}">
<suggestionItems>
<core:Item text="{localModel>CustNoAndName}"/>
</suggestionItems>
</Input>
</cells>
</ColumnListItem>
</Table>
Controller code:
onInit: function () {
var oLocalModel = new sap.ui.model.json.JSONModel({
items: [{
CustoNo: 1,
CustNoAndName: "Mickey Mouse"
}, {
CustoNo: 2,
CustNoAndName: "Donald Duck"
}]
});
this.getView().setModel(oLocalModel, "localModel");
var oData = {
items: [{
ItemNo: 1
}, {
ItemNo: 2
}, {
ItemNo: 3
}]
};
var oModel = new sap.ui.model.json.JSONModel(oData);
this.getView().setModel(oModel);
},
handleSuggestCustNo: function (oEvent) {
console.log("handleSuggestCustNo");
var sTerm = oEvent.getParameter("suggestValue");
console.log({
sTerm
});
},
onCustValChange: function (oEvent) {
console.log("onCustValChange");
var oInput = oEvent.getSource();
this._validateCustValInput(oInput);
},
_validateCustValInput: function (oInput) {
console.log("_validateCustValInput");
var oBinding = oInput.getBinding("suggestionItems");
var value = oInput.getValue();
if (value !== "") {
var filters = [new sap.ui.model.Filter("CustNoAndName", sap.ui.model.FilterOperator.EQ, value)];
var found = oBinding.filter(filters).getLength();
if (found === 0) {
oInput.setValueState(sap.ui.core.ValueState.Error);
//this.sSelectedTableRow.ERRORFLAG = "X";
oInput.setValueStateText("Customer " + "'" + value + "' is invalid");
} else {
oInput.setValueState(sap.ui.core.ValueState.None);
//this.sSelectedTableRow.ERRORFLAG = "";
}
} else {
oInput.setValueState(sap.ui.core.ValueState.None);
//this.sSelectedTableRow.ERRORFLAG = "";
}
oBinding.filter([]);
}
The "value" property of each input is commented. Can you try this code and tell me if it works for you? Maybe I got something wrong but my _validateCustValInput function is always triggered.

onAfterRendering hook for smartform in UI5

In my app i have an XML view that consists of a smartform. I have a need to access an input element(via sap.ui.getCore().byId()) that becomes available after the smartform is parsed and rendered.
The onAfterRendering in the controller for my view triggers as soon as the view is rendered(i get all my non-smartform elements like title etc.), but before the smartform is parsed and rendered. A rudimentary test via an alert also proved this visually.
Is there any event that is triggered after the smartform has rendered which i can hook into to access my input element?
The developer guide walkthrough is extending the smartform and thus has its init method, but in my case i am extending the basecontroller and my init is for the page view.
Thanks for any pointers.
My View:
<mvc:View
controllerName="myns.controller.Add"
xmlns:mvc="sap.ui.core.mvc"
xmlns:semantic="sap.m.semantic"
xmlns:smartfield="sap.ui.comp.smartfield"
xmlns:smartform="sap.ui.comp.smartform"
xmlns="sap.m">
<semantic:FullscreenPage
id="page"
title="{i18n>addPageTitle}"
showNavButton="true"
navButtonPress="onNavBack">
<semantic:content>
<smartform:SmartForm
id="form"
editable="true"
title="{i18n>formTitle}"
class="sapUiResponsiveMargin" >
<smartform:Group
id="formGroup"
label="{i18n>formGroupLabel}">
<smartform:GroupElement>
<smartfield:SmartField
id="nameField"
value="{Name}" />
</smartform:GroupElement>
</smartform:Group>
</smartform:SmartForm>
</semantic:content>
<semantic:saveAction>
<semantic:SaveAction id="save" press="onSave"/>
</semantic:saveAction>
<semantic:cancelAction>
<semantic:CancelAction id="cancel" press="onCancel"/>
</semantic:cancelAction>
</semantic:FullscreenPage>
My Controller:
sap.ui.define([
"myns/controller/BaseController",
"sap/ui/core/routing/History",
"sap/m/MessageToast"
],function(BaseController, History, MessageToast){
"use strict";
return BaseController.extend("myns.controller.Add",{
onInit: function(){
this.getRouter().getRoute("add").attachPatternMatched(this._onRouteMatched, this);
},
onAfterRendering: function(){
//I tried my sap.ui.getCore().byId() here but does not work
//An alert shows me this triggers before the smartform is rendered but
//after all the other non-smartform elements have rendered
},
_onRouteMatched: function(){
// register for metadata loaded events
var oModel = this.getModel();
oModel.metadataLoaded().then(this._onMetadataLoaded.bind(this));
},
_onMetadataLoaded:function(){
//code here....
},
onNavBack: function(){
//code here....
}
});
});
You can look for when SmartForm is added to the DOM with DOMNodeInserted event of jQuery.
For this you can use it's id to identify the SmartForm has been added to the DOM.
Every UI5 element gets some prefix after it has been added to the DOM.
for e.g. __xmlview0--form.
So to make sure required form is added you can split the id of added element, then compare it with id which you have given.
Although it's not optimal solution, but you can try.
onAfterRendering: function() {
$(document).bind('DOMNodeInserted', function(event) {
var aId = $(event.target).attr("id").split("--");
var id = aId[aId.length - 1];
if (id) {
if (id == "form") {
// smart form fields are accessible here
$(document).unbind("DOMNodeInserted");
}
}
})
}
My final solution (for now and uses the accepted answer provided by #Dopedev):
(in controller for the nested view containing the smartform)
onAfterRendering: function() {
$(document).bind('DOMNodeInserted', function(event) {
var elem = $(event.target);
var aId = elem.attr("id").split("--");
var id = aId[aId.length - 1];
if (id) {
if (id == "nameField") {
elem.find("input").on({
focus: function(oEvent) {
//code here;
},
blur: function(oEvent) {
//code here;
}
});
/*
elem.find("input").get(0).attachBrowserEvent("focus", function(evt) {
//code here
}).attachBrowserEvent("blur", function(ev) {
//code here
});
*/
$(document).unbind("DOMNodeInserted");
}
}
});
}

How add mandatory dropdown field in Touch UI

I added "required" as "true" but it is not working. "required" as "true" only works for text field.
As per below document, I do not see any option to add mandatory field from dropdown.
http://docs.adobe.com/docs/en/aem/6-0/author/assets/managing-assets-touch-ui/managing-asset-schema-forms.html
How is it possible to achieve this?
Use $.validator.register to register custom validators.
I have written a detailed blog post on writing custom validators: http://www.nateyolles.com/blog/2016/02/aem-touch-ui-custom-validation.
I have made a comprehensive Touch UI validation library available on GitHub that fixes the issue you described where the "required" property doesn't work for several Granite UI fields as well as other functionality. See https://github.com/nateyolles/aem-touch-ui-validation.
Essentially, you need to modify the field's HTML to include an HTML input that can be validated by either overlaying the foundation component or using JavaScript to modify the DOM when the dialog opens. A hidden input is not eligible for validation, so you need to add a text input hidden by CSS. Use JavaScript to update the "hidden" field when the component action is updated. For example, a color is chosen in the color picker.
Then you register the custom validator against the non-visible text input. Pass in the selector of the non-visible text field and the function that does the actual validation. Also pass in functions for show and clear that show and hide the error message/icon.
The following example is for the color picker taken from the library I linked to above:
/**
* Validation for Granite Touch UI colorpicker.
*
* Additional properties for granite/ui/components/foundation/form/colorpicker
* are:
*
* {Boolean}required
* Is field required
* defaults to false
*
* <myColorPicker
* jcr:primaryType="nt:unstructured"
* sling:resourceType="granite/ui/components/foundation/form/colorpicker"
* fieldLabel="My colorpicker"
* name="./myColorPicker"
* required="{Boolean}true"/>
*/
var COLORPICKER_SELECTOR = '.coral-ColorPicker',
$.validator.register({
selector: '.marker-colorpicker',
validate: function(el) {
var field,
value,
required;
field = el.closest(".coral-Form-field");
value = el.val();
required = field.data('required');
if (required && !value) {
return Granite.I18n.get('Please fill out this field.');
} else {
el.setCustomValidity(null);
el.updateErrorUI();
}
},
show: function (el, message) {
var fieldErrorEl,
field,
error,
arrow;
fieldErrorEl = $("<span class='coral-Form-fielderror coral-Icon coral-Icon--alert coral-Icon--sizeS' data-init='quicktip' data-quicktip-type='error' />");
field = el.closest('.coral-Form-field');
el.add(field)
.attr('aria-invalid', 'true')
.toggleClass('is-invalid', true);
field.nextAll('.coral-Form-fieldinfo')
.addClass('u-coral-screenReaderOnly');
error = field.nextAll('.coral-Form-fielderror');
if (error.length === 0) {
arrow = field.closest('form').hasClass('coral-Form--vertical') ? 'right' : 'top';
fieldErrorEl.clone()
.attr('data-quicktip-arrow', arrow)
.attr('data-quicktip-content', message)
.insertAfter(field);
} else {
error.data('quicktipContent', message);
}
},
clear: function(el) {
var field = el.closest('.coral-Form-field');
el.add(field)
.removeAttr('aria-invalid')
.removeClass('is-invalid');
field.nextAll('.coral-Form-fielderror').tooltip('hide').remove();
field.nextAll('.coral-Form-fieldinfo').removeClass('u-coral-screenReaderOnly');
}
});
/**
* Create hidden field to validate against and click event handler when a
* Granite UI dialog loads.
*/
$(document).on('foundation-contentloaded', function(e) {
var $dialog,
$radioGroups;
$dialog = $(e.target);
$radioGroups = $dialog.find(COLORPICKER_SELECTOR);
$radioGroups.each(function() {
var $radioGroup,
required,
$marker,
$button;
$radioGroup = $(this);
required = $radioGroup.data('required');
if (required) {
$marker = $radioGroup.find('input[type="hidden"]');
$button = $radioGroup.find('.coral-ColorPicker-button')
/* Change to text as hidden is not validated */
$marker.attr('type', 'text');
$marker.addClass('marker-colorpicker');
$marker.attr('aria-required', 'true');
/* revalidate once the button color has changed */
$button.on('stylechange', function(){
$marker.trigger('change');
});
}
});
});
AFAIK, In touch ui dialogs you can apply such validation via jquery. One thing you can try. Create a clientlib folder under component with categories cq.authoring.dialog . Then add the below js snippet as per normal process :
(function (document, $, ns) {
"use strict";
$(document).on("click", ".cq-dialog-submit", function (e) {
e.stopPropagation();
e.preventDefault();
var $form = $(this).closest("form.foundation-form"),
title = $form.find("[name='authoringMode']").val(),
message, clazz = "coral-Button ";
if(!title){
ns.ui.helpers.prompt({
title: Granite.I18n.get("Invalid Input"),
message: "Please Check Values",
actions: [{
id: "CANCEL",
text: "CANCEL",
className: "coral-Button"
}
],
callback: function (actionId) {
if (actionId === "CANCEL") {
}
}
});
}else{
$form.submit();
}
});
})(document, Granite.$, Granite.author);
One thing here you need to change is $form.find("[name='authoringMode']") here name is the property and authoringMode is the value of select box in my dialog. as shown.
Here it will check at dialog submit time whether there is value in drop down and will not let author to submit the dialog till drop-down is blank.
Here is the reference.
http://experience-aem.blogspot.in/2015/02/aem-6-sp2-touch-ui-dialog-before-submit.html

How to remove certain elements before taking screenshot?

I am able to take screenshot of the page using the example code below:
html2canvas(document.body, {
onrendered: function(canvas) {
document.body.appendChild(canvas);
}
});
Now there are certain div's i dont want to be part of the page when I take the screenshot?
How can i prevent them from being part of the screenshot.
One way I thought was to clone the element and then remove the elements, but taking a screenshot of the clone gives a white screen. Here is the code I used:
html2canvas($(document.body).clone()[0], {
onrendered: function(canvas) {
document.body.appendChild(canvas);
}
});
Add this attribute: data-html2canvas-ignore to any element you don't want to be taken when the screenshot is processed.
Hopefully this will help the next guy.
When I used this library I faced a problem that the lib download all the images in my application, that cause the application to run slowly. I resolved the problem using the ignoreElements option.
This is my code:
var DropAreaElement= document.getElementById("123");
var config= {
useCORS: true,
ignoreElements: function (element) {
if (element.contains(DropAreaElement) || element.parentElement.nodeName =="HTML" || element == DropAreaElement || element.parentNode == DropAreaElement) {
console.log("elements that should be taken: ", element)
return false;
}else {
return true;
}
}
};
html2canvas(DropAreaElement, config).then(function (canvas){
var imgBase64 = canvas.toDataURL('image/jpeg', 0.1);
console.log("imgBase64:", imgBase64);
var imgURL = "data:image/" + imgBase64;
var triggerDownload = $("<a>").attr("href", imgURL).attr("download", "layout_" + new Date().getTime() + ".jpeg").appendTo("body");
triggerDownload[0].click();
triggerDownload.remove();
}).catch(Delegate.create(this, function (e){
console.error("getLayoutImageBase64 Exception:", e);
});
If you don't want to use an attribute, html2canvas does provide a method to remove elements. For example:
html2canvas( document.body, {
ignoreElements: function( element ) {
/* Remove element with id="MyElementIdHere" */
if( 'MyElementIdHere' == element.id ) {
return true;
}
/* Remove all elements with class="MyClassNameHere" */
if( element.classList.contains( 'MyClassNameHere' ) ) {
return true;
}
}
} ).then( function( canvas ) {
document.body.appendChild( canvas );
} );
For more information, see html2canvas options.
You can create HOC for <Printable/> and <NonPrintable/> , you can wrap your component with <NonPrintable><YourCoolComponent/></NonPrintable>
those children components would be excluded.
import React from "react"
interface INonPrintable {
children: React.ReactChildren
}
/*
HOC - Printable which injects the printId to the React component
which gets us Printable Context to html2canvas => jsPDF
eg:
<Printable printId="about-you-print">
<PersonalInfo badEmail={badEmail} />
<IdentityInfo />
<AdditonalInfo />
<AddressInfo
serviceAddress={serviceAddress}
billingAddress={this.state.billingAddress}
setBillingAddress={this.setBillingAddress}
/>
</Printable>
*/
export default function Printable({ printId = "", children, ...restProps }) {
return <div print-id={printId} {...restProps}>{children}</div>
}
/*
HOC - NONPrintable which injects the data-html2canvas-ignore to the React component
which gets us Printable Context to html2canvas => jsPDF
eg:
<NonPrintable style={{display:"flex",justifyContent:'space-around'}}>
<Button
text="Print PDF using Own utility"
onClick={this.handlePrintPdf}
/>
<Button
text="Print PDF using html2canvas + jsPDF"
onClick={this.handlePrintwithPDFjs}
/>
</NonPrintable>
*/
export const NonPrintable = ({ children, ...restProps }) => {
return <div data-html2canvas-ignore {...restProps}>{children}</div>
}

View doesn't update on observablearray.remove(item) in knockout without call to applyBindings

I am learning to use MVC 4/MVVM/Knockout for a web-managed data project. I have been running into a problem updating the View when using the remove function on an observable array. The updates happen when using push or unshift, but not remove. Using the debugger in chrome I can see that the data is being removed from the array, the update event just isn't working.
Snippet from the html is the table below, there is a form I did not include for adding or editing data.
<div id="MessageDiv" data-bind="message: Message"></div>
<div class="tableContainer hiddenHead">
<div class="headerBackground"></div>
<div class="tableContainerInner">
<table id="adapter-table" class="grid" data-bind="sortTable: true">
<thead>
<tr>
<th class="first">
<span class="th-inner">Name</span>
</th>
<th>
<span class="th-inner">DeviceID</span>
</th>
<th>
<span class="th-inner"></span>
</th>
<th>
<span class="th-inner"></span>
</th>
</tr>
</thead>
<tbody data-bind="template: { name: 'AdaptersTemplate', foreach: Adapters }">
</tbody>
</table>
<script id="AdaptersTemplate" type="text/html">
<tr>
<td data-bind="text: Name"></td>
<td data-bind="text: DeviceID"></td>
<td>Edit
<td>Delete
</tr>
</script>
</div>
<input type="button" data-bind='click: addAdapter' value="Add New Adapter" />
<input type="button" data-bind='click: saveAll' value="Save Changes" id="SaveChangesButton" />
</div>
My javascript has been set up to manage the VM as restful and caches the changes. Add, Edit, and Saving/Deleting data all seems to work without throwing errors that I am seeing in the debugger in Chrome. Confirming changes seems to work fine and makes the changes to the database as expected.
$(function () {
var viewModel = new AdaptersModel();
getData(viewModel);
});
function getData(viewModel) {
$.getJSON("/api/AdapterList",
function (data) {
if (data && data.length > 0) {
viewModel.SetAdaptersFromJSON(data);
}
ko.applyBindings(viewModel);
});
}
//#region AdapterVM
function Adapter(name, siFamily, deviceIDs) {
var self = this;
self.Name = ko.observable(name);
self.DeviceID = ko.observable(deviceIDs);
self.ID = 0;
}
function AdaptersModel() {
var self = this;
self.Adapters = ko.observableArray([]);
self.DeleteAdapters = ko.observableArray([]);
self.NewAdapter = ko.observable(new Adapter("", "", "", ""));
self.Message = ko.observable("");
self.SetAdaptersFromJSON = function (jsData) {
self.Adapters = ko.mapping.fromJS(jsData);
};
//#region Edit List Options: confirmChanges
self.confirmChanges = function () {
if (self.NewAdapter().ID == 0) {
self.Adapters.push(self.NewAdapter());
}
};
//#endregion
//#region Adapter List Options: addAdapter, selectItem, deleteItem, saveAll
self.addAdapter = function () {
self.NewAdapter(new Adapter("", "", "", ""));
};
self.selectItem = function (item) {
self.NewAdapter(item);
};
self.deleteItem = function(item) {
self.DeleteAdapters.push(item.ID());
self.Adapters.remove(item);
};
self.saveAll = function () {
if (self.Adapters && self.Adapters().length > 0) {
var filtered = ko.utils.arrayFilter(self.Adapters(),
function(adapter) {
return ((!isEmpty(adapter.Manufacturer())) &&
(!isEmpty(adapter.Name())) &&
(!isEmpty(adapter.DeviceIDs()))
);
}
);
var updateSuccess = true;
if (self.DeleteAdapters().length > 0) {
jsonData = ko.toJSON(self.DeleteAdapters());
$.ajax({
url: "/api/AdapterList",
cache: false,
type: "DELETE",
data: jsonData,
dataType: "json",
contentType: "application/json; charset=utf-8",
success: function () { updateSuccess = true; },
error: function () { updateSuccess = false; }
});
}
var jsonData = ko.toJSON(filtered);
$.ajax({
url: "/api/AdapterList",
type: "POST",
data: jsonData,
dataType: "json",
contentType: "application/json; charset=utf-8",
success: function(data) {
self.SetAdaptersFromJSON(data);
updateSuccess = true && updateSuccess;
},
error: function () { updateSuccess = false; }
});
if (updateSuccess == true) { self.Message("Update Successfull"); }
else { self.Message("Update Failed"); }
}
};
//#endregion
}
//#endregion
ko.bindingHandlers.message = {
update: function(element, valueAccessor) {
$(element).hide();
ko.bindingHandlers.text.update(element, valueAccessor);
$(element).fadeIn();
$(element).fadeOut(4000);
}
};
ko.bindingHandlers.sortTable = {
init: function (element, valueAccessor) {
setTimeout(function () {
$(element).addClass('tablesorter');
$(element).tablesorter({ widgets: ['zebra'] });
}, 0);
}
};
function isEmpty(obj) {
if (typeof obj == 'undefined' || obj === null || obj === '') return true;
if (typeof obj == 'number' && isNaN(obj)) return true;
if (obj instanceof Date && isNaN(Number(obj))) return true;
return false;
}
The specific script portion that is failing to update my html table is:
self.deleteItem = function(item) {
self.DeleteAdapters.push(item.ID());
self.Adapters.remove(item);
};
Everything seems to work except for the remove, so I seem to be at a loss for what to look at next, and I am too new to javascript or knockout to know if this is a clue: If I run ko.applyBindings() command in the self.deleteItem function, I get the update to happen but it does give me an unhandled error:
Uncaught Error: Unable to parse bindings.
Message: ReferenceError: Message is not defined;
Bindings value: message: Message
Message was defined in the VM before binding... was there something I missed in all this?
In the beginning of your Js file you are defining var viewModel = new AdaptersModel(); but lower you are stating that function Adapter() is the view model in your region declaration. It is making your code difficult to read. I am going to take another stab at what you can do to troubleshoot, but I would suggest that your viewmodel contains the adapters and your model contains a class-like instance of what each adapter should be.
The specific error you are getting is because you are binding Message() to something and then deleting Message(). One thing you could do to trouble shoot this is to change your div to something like :
<div id="MessageDiv" data-bind="with: Message">
<h5 data-bind="message: $data"><h5>
</div>
If you could create a fiddle I could give a more definite example of why, but basically if Message() is blank the with binding should not show the header which is undefined after deletion.
What you probably need to do though is look at what is being sent as 'item' and make sure it is not your viewmodel.
self.deleteItem = function(item) {
console.log(item); // << Check console and see what is being returned
self.DeleteAdapters.push(item.ID());
self.Adapters.remove(item);
};
You are probably deleting more than just a single adapter.
This will lead you the right direction, but I would seriously consider either renaming your code.
There was a lot of help solving surrounding issues but nothing actually solved the "why" of the problem. The updates worked perfectly sometimes but not other times. When I was troubleshooting it and started to get it dumbed down and working in JSFiddle I didn't include the data-bind="sortTable: true" in all my working versions. Apparently, if you sort a table or using the code as I did it will not work. The example code I have seen floating around is here at http://jsfiddle.net/gregmason/UChLF/16/, pertinent code:
ko.bindingHandlers.tableSorter = {
init: function (element) {
setTimeout(function () { $(element).tablesorter(); }, 0);
},
update: function (element, valueAccessor) {
ko.utils.unwrapObservable(valueAccessor()); //just to get a dependency
$(element).trigger("update");
}
};
The errant behavior can be obvious by clicking the delete link on the row.
If you click on the row without sorting, you will see the row disappear correctly.
If you first click on a column to re-sort in a different order, THEN delete the row, it remains in the table and appears to have cached.
This can be handled by binding each of the table headers instead of the table itself and replacing the tableSorter code with a custom sort behavior as discussed in this thread:
knockout js - Table sorting using column headers. The sort replacement is here:
ko.bindingHandlers.sort = {
init: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
var asc = false;
element.style.cursor = 'pointer';
element.onclick = function(){
var value = valueAccessor();
var prop = value.prop;
var data = value.arr;
asc = !asc;
if(asc){
data.sort(function(left, right){
return left[prop]() == right[prop]() ? 0 : left[prop]() < right[prop]() ? -1 : 1;
});
} else {
data.sort(function(left, right){
return left[prop]() == right[prop]() ? 0 : left[prop]() > right[prop]() ? -1 : 1;
});
}
}
}
};
This has fixed my sorting/editing/deleting issues and a working jsFiddle is here: http://jsfiddle.net/gregmason/UChLF/18/