Formatter called multiple times or too soon with `undefined` - data-binding

I'm using OpenUI5. Using the formatter.js, I have formatted some text in my view.
But my formatter is called 3 times:
When I bind the model to panel control: oPanel.setModel(oModel, "data");
both sBirthday and sFormat are undefined.
After onInit() is finished and the view is rendered:
sBirthday is valorized correctly and sFormat is undefined
Again: both sBirthday and sFormat ara valorized correctly.
Why does this happen? Is it correct?
The app get an error, because the ageDescription() in the formatter, can't manage undefined values.
formatter.js
sap.ui.define([], function () {
"use strict";
return {
ageDescription : function (sBirthday, sFormat) {
do.something();
var sFromMyBd = moment(sBirthday, sFormat).fromNow();
do.something();
return sAge;
}
}
});
main.view.xml
<mvc:View
controllerName="controller.main"
xmlns="sap.m"
xmlns:mvc="sap.ui.core.mvc">
<Panel id="user-panel-id">
<Input id="name-input-id" enabled="false" value="{data>/user/name}" />
<Label text="{i18n>age}: " class="sapUiSmallMargin"/>
<Label text="{
parts: [
{path: 'data>/user/birthday'},
{path: 'data>/user/dateFormat'}
],
formatter: '.formatter.ageDescription' }"/>
</Panel>
</mvc:View>
Main.controller.js
sap.ui.define([
"sap/ui/core/mvc/Controller",
"sap/ui/model/json/JSONModel",
"model/formatter"
], function (Controller, JSONModel, formatter) {
"use strict";
return Controller.extend("controller.main", {
formatter: formatter,
onInit: function () {
var oModel = new JSONModel();
var oView = this.getView();
oModel.loadData("model/data.json");
var oPanel = oView.byId("user-panel-id");
oPanel.setModel(oModel,"data");
do.something();
},
});
});
data.json
{
"user": {
"name": "Frank",
"surname": "Jhonson",
"birthday": "23/03/1988",
"dateFormat": "DD/MM/YYYY",
"enabled": true,
"address": {
"street": "Minnesota street",
"city": "San Francisco",
"zip": "94112",
"country": "California"
}
}
}

Set the model to the view only when the data request is completed:
onInit: function() {
const dataUri = sap.ui.require.toUri("<myNamespace>/model/data.json");
const model = new JSONModel(dataUri);
model.attachEventOnce("requestCompleted", function() {
this.getView().setModel(model);
}, this);
// ...
},
This ensures that the formatter is called only once (invoked by checkUpdate(true) which happens on binding initialization; see below), and no further changes are detected afterwards.
Additionally (or alternatively), make the formatter more defensive. Something like:
function(value1, value2) {
let result = "";
if (value1 && value2) {
// format accordingly ...
}
return result;
}
Why does this happen?
View gets instantiated.
onInit of the Controller gets invoked. Here, the file model/data.json is requested (model is empty).
Upon adding the view to the UI, UI5 propagates existing parent
models to the view.
Bindings within the view are initialized, triggering checkUpdate(/*forceUpdate*/true)src in each one of them.
Due to the forceUpdate flag activated, change event is fired, which forcefully triggers the formatters even if there were no changes at all:
[undefined, undefined] → [undefined, undefined]. - 1st formatter call
Fetching model/data.json is now completed. Now the model needs to checkUpdate again.
[undefined, undefined] → [value1, undefined] → change detected → 2nd formatter call
[value1, undefined] → [value1, value2] → change detected → 3rd formatter call

Now, given that you are trying to load a static JSON file into your project, it's better to maximize the usage of the manifest.json.
That way, you are sure that the data is already loaded and available in the model prior to any binding.
You achieve this by adding the JSON file as a data source under sap.app
manifest.json
"sap.app": {
"id": "com.sample.app",
"type": "application",
"dataSources": {
"data": {
"type": "JSON",
"uri": "model/data.json"
}
}
}
Now, simply add this dataSource called data as one of the models under sap.ui5.
"sap.ui5": {
"rootView": {
"viewName": "com.sample.app.view.App",
"type": "XML"
},
"models": {
"i18n": {
"type": "sap.ui.model.resource.ResourceModel",
"settings": {
"bundleName": "com.app.sample.i18n.i18n"
}
},
"data": {
"type": "sap.ui.model.json.JSONModel",
"dataSource": "data"
}
}
}
With this, you DON'T need to call this anymore:
var oModel = new JSONModel();
var oView = this.getView();
oModel.loadData("model/data.json");
var oPanel = oView.byId("user-panel-id");
oPanel.setModel(oModel,"data");
..as the data model we added in the manifest.json, is already visible to oView and oPanel right from the get go.
This way, it doesn't matter if the formatter gets called multiple times, as it would already have the data available to it right from the beginning.

Related

Is there a way to show related model ids without sideloading or embedding data

My understanding is that using serializeIds: 'always' will give me this data, but it does not.
Here's what I'm expecting:
{
id="1"
title="some title"
customerId="2"
}
Instead the output I'm receiving is:
{
id="1"
title="some title"
}
My code looks something like this:
import {
Server,
Serializer,
Model,
belongsTo,
hasMany,
Factory
} from "miragejs";
import faker from "faker";
const ApplicationSerializer = Serializer.extend({
// don't want a root prop
root: false,
// true required to have root:false
embed: true,
// will always serialize the ids of all relationships for the model or collection in the response
serializeIds: "always"
});
export function makeServer() {
let server = newServer({
models: {
invoice: Model.extend({
customer: belongsTo()
}),
customer: Model.extend({
invoices: hasMany()
})
},
factories: {
invoice: Factory.extend({
title(i) {
return `Invoice ${i}`;
},
afterCreate(invoice, server) {
if (!invoice.customer) {
invoice.update({
customer: server.create("customer")
});
}
}
}),
customer: Factory.extend({
name() {
let fullName = () =>
`${faker.name.firstName()} ${faker.name.lastName()}`;
return fullName;
}
})
},
seeds(server) {
server.createList("invoice", 10);
},
serializers: {
application: ApplicationSerializer,
invoice: ApplicationSerializer.extend({
include: ["customer"]
})
},
routes() {
this.namespace = "api";
this.get("/auth");
}
});
}
Changing the config to root: true, embed: false, provides the correct output in the invoice models, but adds the root and sideloads the customer, which I don't want.
You've run into some strange behavior with how how serializeIds interacts with embed.
First, it's confusing why you need to set embed: true when you're just trying to disable the root. The reason is because embed defaults to false, so if you remove the root and try to include related resources, Mirage doesn't know where to put them. This is a confusing mix of options and Mirage should really have different "modes" that take this into account.
Second, it seems that when embed is true, Mirage basically ignores the serializeIds option, since it thinks your resources will always be embedded. (The idea here is that a foreign key is used to fetch related resources separately, but when they're embedded they always come over together.) This is also confusing and doesn't need to be the case. I've opened a tracking issue in Mirage to help address these points.
As for you today, the best way to solve this is to leave root to true and embed false, which are both the defaults, so that serializeIds works properly, and then just write your own serialize() function to remove the key for you:
const ApplicationSerializer = Serializer.extend({
// will always serialize the ids of all relationships for the model or collection in the response
serializeIds: "always",
serialize(resource, request) {
let json = Serializer.prototype.serialize.apply(this, arguments);
let root = resource.models ? this.keyForCollection(resource.modelName) : this.keyForModel(resource.modelName)
return json[root];
}
});
You should be able to test this out on both /invoices and /invoices/1.
Check out this REPL example and try making a request to each URL.
Here's the config from the example:
import {
Server,
Serializer,
Model,
belongsTo,
hasMany,
Factory,
} from "miragejs";
import faker from "faker";
const ApplicationSerializer = Serializer.extend({
// will always serialize the ids of all relationships for the model or collection in the response
serializeIds: "always",
serialize(resource, request) {
let json = Serializer.prototype.serialize.apply(this, arguments);
let root = resource.models ? this.keyForCollection(resource.modelName) : this.keyForModel(resource.modelName)
return json[root];
}
});
export default new Server({
models: {
invoice: Model.extend({
customer: belongsTo(),
}),
customer: Model.extend({
invoices: hasMany(),
}),
},
factories: {
invoice: Factory.extend({
title(i) {
return "Invoice " + i;
},
afterCreate(invoice, server) {
if (!invoice.customer) {
invoice.update({
customer: server.create("customer"),
});
}
},
}),
customer: Factory.extend({
name() {
return faker.name.firstName() + " " + faker.name.lastName();
},
}),
},
seeds(server) {
server.createList("invoice", 10);
},
serializers: {
application: ApplicationSerializer,
},
routes() {
this.resource("invoice");
},
});
Hopefully that clears things up + sorry for the confusing APIs!

botkit middleware - How to use sendToWatson to update context?

I use https://github.com/watson-developer-cloud/botkit-middleware#implementing-app-actions as my reference.
The context in my conversation does not update.
Here is my bot-facebook.js.
function checkBalance(context, callback) {
var contextDelta = {
user_name: 'Henrietta',
fname: 'Pewdiepie'
};
callback(null, context);
}
var checkBalanceAsync = Promise.promisify(checkBalance);
var processWatsonResponse = function (bot, message) {
if (message.watsonError) {
console.log(message.watsonError);
return bot.reply(message, "I'm sorry, but for technical reasons I can't respond to your message");
}
if (typeof message.watsonData.output !== 'undefined') {
//send "Please wait" to users
bot.reply(message, message.watsonData.output.text.join('\n'));
if (message.watsonData.output.action === 'check_balance') {
var newMessage = clone(message);
newMessage.text = 'check new name';
checkBalanceAsync(message.watsonData.context).then(function (contextDelta) {
console.log("contextDelta: " + JSON.stringify(contextDelta));
return watsonMiddleware.sendToWatsonAsync(bot, newMessage, contextDelta);
}).catch(function (error) {
newMessage.watsonError = error;
}).then(function () {
return processWatsonResponse(bot, newMessage);
});
}
}
};
controller.on('message_received', processWatsonResponse);
The JSON editor of welcome node in my watson conversation.
{
"context": {
"fname": "",
"user_name": ""
},
"output": {
"text": {
"values": [
"Good day :) My name is Doug and I am a chatbot."
],
"selection_policy": "random"
},
"action": "check_balance"
}
}
I have tried multiple ways I could imagine.
Do I need to do something like fname: <?contextDelta.fname?> in the json editor?
You aren't checking context in your dialog.
Context object in JSON editor is used to store captured data in context,
so your node actually empties context variable.
Probably you need to remove that context initialization from your dialog,
To see value of context variable, you have to use it in the output
"Good day, $fname :) My name is Doug and I am a chatbot."

Binding KendoGrid to a local object

I want to bind a KendoGrid to an object array so that it reflects what ever the user enters. The object will have two fields ExceptionName and ExceptionType. ExceptionType needs to be a dropdown of 5 items (this is working). The ExceptionName will be free text.
If I double click on the kendo grid, I can edit, but it does not reflects in the object. Same thing for Delete & new row. (So I think I am doing something wrong in the binding or in the declaration of the object)
Below, find a snippet of my code:
Object array:
var authorizationInformation = [{
id:1,
exemptionName: "",
exemptionType: "Unknown"
}];
KendoGrid:
$("#AuthorizationGrid").kendoGrid({
columns: [{
field: "exemptionName", title: "Exemption Name"
},
{
field: "exemptionType",
title: "Exemption Type",
template: function (value) {
for (var i = 0; i < exemptionTypeList.length; i++) {
if (exemptionTypeList[i].exemptionType == value.exemptionType) {
return exemptionTypeList[i].description;
}
}
},
editor: function (container) {
var input = $('<input id="exemptionType" name="exemptionType">');
input.appendTo(container);
// initialize a dropdownlist
input.kendoDropDownList({
dataTextField: "description",
dataValueField: "exemptionType",
dataSource: exemptionTypeList
}).appendTo(container);
}
},
{
command: "destroy"
}],
dataSource: authorizationInformation,
editable: true,
scrollable: false,
});
Any suggestion would be appreciated.
Thanks, M

Bind constraints in Input control of SAPUI5

My rest service expose me a group of fields: each filed has a value and a list of attributes: enabled, maxLength (in case of string), minLength (in case of string), decimals (number of decimal digits - in case of float).
In OpenUi5 I have:
value and enabled are properties of Input control Link (Good!! I can bind properties with model contains the attributes)
maxLength and decimals are optionsof String type and Float type (Link) but I can't bind options with a model :-/
minLength I can't find a property/option
I would like map (bind) each attribute with component so that automatically the library control for me without writing more code.
there is a property called maxLength for Input Control.
So the only problem I see is binding minLength and decimals for which there is little bit effort is needed.
Solution
Create your own input control by extending the existing Input
Control.How to achieve it?
Sample Code Structure:
jQuery.sap.require("sap.m.Input");
jQuery.sap.declare("sap.m.ComplexInput");
sap.m.Input.extend("sap.m.ComplexInput", {
metadata: {
properties: {
minLength: {
type: "int"
},
decimals: {
type: "int"
},
events: {
//define your own events like checkMinLength,checkDecimals
}
},
onInit: function () {
//on init do something
},
onAfterRendering: function () {
//called after instance has been rendered (it's in the DOM)
},
_somePrivateMethod: function () {
/*do someting...*/
},
somePublicMethod: function () {
/*do someting...*/
},
}
});
sap.m.ComplexInput.prototype.exit = function () {
/* release resources that are not released by the SAPUI5 framework */
//do something
};
Adding CustomData and using wherever you want to.
Then you can access custom data in validation process or on liveChange or so..
Bind the other properties to the value of customData
var input = new sap.m.Input({
value: '{value}',
enabled: '{enabled}',
maxLength: '{maxLength}',
customData: [
new sap.ui.core.CustomData({
key: 'minLength',
value: '{minLength}'
}),
new sap.ui.core.CustomData({
key: 'decimals ',
value: '{decimals}'
})
],
change: function(oEvent) {
var src = oEvent.getSource();
var minLen = src.getCustomData()[0].getValue();
var decimals = src.getCustomData()[1].getValue();
if (src.getValue() && src.getValue().length > minLen) {
src.setValueState('Success');
} else {
src.setValueState('Error');
}
}
});

ExtJS4 - Reconfiguring a grid in ASP.NET - JSON structure issue

One of ASP.NET's security features is proving to be a mountain to scale here - the "d" property addition when returning a JSON response appears to be confusing ExtJS when I attempt to reconfigure a gridpanel dynamically, causing it to fail when attempting to generate new column structure.
I followed this solution by nicholasnet:
http://www.sencha.com/forum/showthread.php?179861-Dynamic-grid-columns-store-fields
and it works beautifully, until the JSON payload is wrapped around the "d" property, e.g.
{"d":{
"metaData": {
"root": "data",
"fields": [{
"type": "int",
"name": "id",
"hidden": true,
"header": "id",
"groupable": false,
"dataIndex": "id"
}, ...omitted for brevity...]
},
"success": true,
"data": [{
"id": "1",
"controller": "Permissions",
"description": "Allow to see permission by roles",
"administrator": true,
"marketing": false
}]
}
}
I can't work out how to tell ExtJS to skirt around this problem. I've tried setting the "root" property of the AJAX reader to "d.data" but that results in the grid showing the correct number of rows but no data at all.
I've all the property descriptors required for column metadata ("name", "header", "dataIndex") in the JSON so I don't believe the JSON structure to be the cause. My main lead at the moment is that on the event handler:
store.on
({
'load' :
{
fn: function(store, records, success, operation, eOpts)
{
grid.reconfigure(store,store.proxy.reader.fields);
},
scope: this
}
}, this);
The fields in historyStore.proxy.reader.fields part is undefined when I pass the "d"-wrapped JSON. Anyone have any ideas on why this is or how to solve this issue?
edit: my Store/proxy
Ext.define('pr.store.Store-History', {
extend: 'Ext.data.Store',
model: 'pr.model.Model-History',
proxy: {
type: 'ajax',
url: '/data/history.json',
reader: {
type: 'json',
root: 'd'
}
}
});
Ext.define('pr.store.Store-History', {
extend: 'Ext.data.Store',
model: 'pr.model.Model-History',
proxy: {
type: 'ajax',
url: '/data/history.json',
reader: {
type: 'json',
root: 'data',
readRecords: function(data) {
//this has to be before the call to super because we use the meta data in the superclass readRecords
var rootNode = this.getRoot(data);
if (rootNode.metaData) {
this.onMetaChange(rootNode.metaData); // data used to update fields
}
/**
* #deprecated will be removed in Ext JS 5.0. This is just a copy of this.rawData - use that instead
* #property {Object} jsonData
*/
this.jsonData = rootNode;
return this.callParent([rootNode]); // data used to get root element and then get data from it
},
}
}
});
Update:
you are not getting fields in reader because the default code for getting fields from data doesn't handle your wrapped data, so you need to change 'readRecords' function to handle your custom data

Resources