How to reset bound control values efficiently? - sapui5

I have an "Add New…" screen with multiple sap.m.Input fields. Everything is working. I submit the form and the values are stored in the DB. But once I re-open this "Add New…" screen, I get the form with previously entered values.
Currently, I can solve the issue iterating over all sap.m.Input fields with sap.ui.core.Element, resetting the values:
Element.registry.forEach(el => {
if (el.isA("sap.m.Input") && el.sId.includes(inputFieldsMask)) {
sap.ui.getCore().byId(el.sId).setValue("");
}
});
Where inputFieldsMask is a mask for all input fields of the relevant screen.
As far as I understand, Element.registry.forEach iterates over all controls in the app, therefore I'm not sure that, from a performance point of view, it's an optimal approach to clean up the fields.
Is there a better way to reset input fields from the previously entered values?

There are several ways to reset the control values depending on what kind of approach you took to create the new entry. Generally, we can make use of the following APIs:
Pass the context to the target container.
In case of working with client-side models: targetContainer.bindElement("newItemPath").
Otherwise, call myV2ODataModel.createEntry("/ThatSet", {...}), which returns a new context, and then pass it to targetContainer.setBindingContext(context, "modelName").
This resolves all the relative bindings in the target container.
<user enters some values and submits ...>
targetContainer.unbindElement("modelName") after the edit was successfully stored.
By unbinding element, relatively bound control values are reset automatically.
Example (using client-side model):
sap.ui.getCore().attachInit(() => sap.ui.require([
"sap/ui/core/mvc/XMLView",
"sap/ui/model/json/JSONModel",
"sap/base/util/uid",
], (XMLView, JSONModel, createPseudoUniqueID) => XMLView.create({
definition: `<mvc:View xmlns:mvc="sap.ui.core.mvc" height="100%">
<App xmlns="sap.m">
<Page backgroundDesign="List" title="Resetting inputs via client-side Model and Context">
<headerContent>
<Button id="addBtn" text="Add Item" type="Emphasized" />
</headerContent>
<List id="myList" growing="true" items="{
path: '/myItems',
key: 'key',
templateShareable: false
}">
<StandardListItem title="{value}" info="Key: {key}"/>
</List>
</Page>
<dependents>
<Dialog id="myDialog"
icon="sap-icon://ui-notifications"
title="New Item"
draggable="true"
class="sapUiResponsiveContentPadding"
>
<Input id="myInput"
placeholder="<New value>"
valueLiveUpdate="true"
value="{
path: 'value',
type: 'sap.ui.model.type.String',
constraints: {
minLength: 1
}
}"
/>
<beginButton>
<Button
text="Submit"
enabled="{= !!%{value} && !%{messages>/}.length}"
/>
</beginButton>
</Dialog>
</dependents>
</App>
</mvc:View>`,
models: {
undefined: new JSONModel({
"myItems": [],
}),
"messages": sap.ui.getCore().getMessageManager().getMessageModel()
},
afterInit: function() {
sap.ui.getCore().getMessageManager().registerObject(this, true);
this.byId("addBtn").attachPress(handleAddPress.bind(this));
this.byId("myInput").attachSubmit(handleSubmit.bind(this));
this.byId("myDialog").setEscapeHandler(onESCPress.bind(this))
.attachAfterClose(onAfterClose.bind(this))
.getBeginButton().attachPress(handleSubmit.bind(this));
function handleAddPress(event) {
const dialog = this.byId("myDialog");
const listBinding = this.byId("myList").getBinding("items");
listBinding.suspend(); // Do not update the list yet
this._currentItems = this.getModel().getProperty("/myItems"); // temp in case user cancels
dialog.getModel().setProperty("/myItems", this._currentItems.concat({})); // new empty item
dialog.bindElement("/myItems/" + listBinding.getLength()); // enable data synchronization via TwoWay binding
dialog.open();
}
function onESCPress(promise) {
const model = this.getModel();
model.setProperty("/myItems", this._currentItems, /*context*/null, /*async*/true);
return promise.resolve(); // continue closing dialog
}
function onAfterClose(event) {
handleAfterClose(event.getSource(), this.byId("myList").getBinding("items"));
}
function handleAfterClose(dialog, listBinding) {
dialog.unbindElement(); // reset data
dialog.setBusy(false);
listBinding.resume();
}
function handleSubmit() {
const dialog = this.byId("myDialog");
if (!dialog.getBeginButton().getEnabled()) return; // something is wrong
dialog.setBusy(true);
if (!this._isStillRequesting) {
this._isStillRequesting = true;
/* send request */setTimeout(mySuccessHandler.bind(this), 3000)
};
}
function mySuccessHandler(newKeyFromServer = createPseudoUniqueID()) {
const dialog = this.byId("myDialog");
this._isStillRequesting = false;
if (!dialog.isOpen()/* request was aborted e.g. by pressing ESC */) {
return; // exit early
}
const context = dialog.getBindingContext();
const value = context.getProperty("value");
dialog.getModel().setProperty(context.getPath("key"), newKeyFromServer);
dialog.close();
sap.ui.require([
"sap/m/MessageToast"
], MT => window.requestAnimationFrame(() => MT.show(`${value} created`)));
}
},
}).then(view => view.placeAt("content"))));
<script id="sap-ui-bootstrap" src="https://ui5.sap.com/resources/sap-ui-core.js"
data-sap-ui-libs="sap.ui.core,sap.m"
data-sap-ui-theme="sap_fiori_3"
data-sap-ui-async="true"
data-sap-ui-compatversion="edge"
data-sap-ui-excludejquerycompat="true"
data-sap-ui-xx-waitForTheme="init"
></script>
<body id="content" class="sapUiBody sapUiSizeCompact"></body>
As explained above, binding and unbinding element also applies to server-side models such as v2.ODataModel.
Benefits
✅ Reduced overload: no need to iterate over all existing controls. Reset only those automatically that need to be reset.
✅ Control agnostic: does not rely on control specific APIs such as myInput.setValue, mySwitch.setState, etc..
✅ Reduced maintenance costs: no need to maintain list of model properties in controller that application needs to reset manually.

Best practice is to use a model to store your application data and to bind any input field to that model. I added an example here. For the sake of simplicity the model data is cleared when the button is pressed.
In a real world application you would place any setup of the model to the onRouteMatched handler to ensure that the data is in an initial state.
onRouteMatched : function(event) {
this.getView().getModel().setData({
"firstName": "",
"lastName": ""
});
}

Bind all your control values to a model. Then reset this model after you've successfully saved the data.
Example:
control1.bindProperty("value", "/controlValues/control1Value"); // Binding
// control1.bindProperty("value", "/controlValues/name");
// <Input value="{/controlValues/name}" /> // <-- ideal binding in xml view
this.getView().getModel().setProperty("/controlValues", this.resetFormData()); // Clear Model
resetFormData: function () {
var emptyControlValues = {
"control1Value": "", // "name": "", <-- bind to control
"control2Value": 0, // "age": 0,
"control3Value": "", // "address": "",
"control4Value": "" // "tel": ""
};
return emptyControlValues;
};

Related

Bootstrap-vue modal manipulate ok-disabled state in function

I've set the default OK Button in a Bootstrap-Vue Modal to disabled true and want to change it when inputing something in ab-form-input. Calling the function works but disabling ok-disabled not. Can't get access to the property. Seems to be a very basic question but in the component docs in bootstrap-vue there is only the infor that state can be changed (true-false) but not how to manipulate via script.
`
<template>
<b-modal
id="component-modal"
title="Add Component"
#ok="handleOk"
:ok-disabled="true"
>
<div class="container">
<b-row>
<b-col>Component: </b-col>
<b-col>
<b-form-input
v-model="component"
id="new-component"
required
#input="enableOK"
></b-form-input>
</b-col>
</b-row>
</div>
</b-modal>
</template>
<script>
import axios from 'axios';
import store from '../../store';
export default {
data() {
return {
count: 0,
};
},
methods: {
handleOk() {
this.handleSubmit();
},
handleSubmit() {
this.insertComponentClass(this.component, store.state.project);
delete this.component;
},
insertComponentClass(componentClass, pid) {
const path = `${store.state.apiURL}/componentclass/add`;
const payload = {
name: componentClass,
project_id: pid,
};
axios
.put(path, payload)
.then(() => {
this.$parent.getComponents();
})
.catch((error) => {
console.error(error);
});
},
enableOK() {
console.info('enable ok fired');
this.ok-disable = false; // doesnt wor, linter says "Invalid left-hand side in assignment expression"
},
},
};
</script>
`
There's a few things going on here that are incorrect.
You're binding the ok-disabled prop to a hardcoded value of true in your template. If you want that value to change, you'll need to bind it to a variable that you can update in your components <script>
For example, you can update the modal's :ok-disabled prop to:
:ok-disabled="okDisabled"
Then in your <script> data function, add it to the return object (defaulted to true):
data() {
return {
count: 0,
okDisabled: true,
}
}
Now the modal's :ok-disabled property is bound to that variable and we can change the value in the enableOk method like so:
this.okDisabled = false;
Note regarding the lint error, the name of the variable you're trying to assign to this.ok-disable is not a valid variable name. You can't use a dash (-) character for a Javascript variable name. You can rename it to the property we created earlier this.okDisabled

Adding a SimplePlanningCalendar to a UI5 app fails with `getKey` error

When I add a SimplePlanningCalendar to my app, I got the following Error.
I added the calendar with a minimal configuration to my app.
<IconTabBar xmlns="sap.m"
id="idCategoryMenu"
selectedKey="Home"
headerMode="Inline"
stretchContentHeight="true"
applyContentPadding="false"
select=".onSelectCategory"
items="{
path: 'backend>/CategorySet',
parameters: {
expand: 'Reports'
},
sorter: {
path: 'Sort'
},
templateShareable: true
}">
<items>
<IconTabFilter id="myIconTabFilter" key="{backend>Uuid}" text="{backend>Name}">
<!-- ... -->
<SinglePlanningCalendar>
<appointments>
<unified:CalendarAppointment xmlns:unified="sap.ui.unified"
title="{title}"
startDate="{startDate}"
endDate="{endDate}"
/>
</appointments>
</SinglePlanningCalendar>
<!-- ... -->
</IconTabFilter>
</items>
</IconTabBar>
When I debug the app, I come to the following line inside the SinglePlanningCalendar.js where a key from the given vView parameter is requested, but the parameter only holds a string.
Anyone else had this problem before and knows why or how to solve this?
The problem is caused by the control implementation itself in the current version (1.71.21) I use for my development.
The fix in the commit 45696fe is available as of UI5 1.75.x.
Since I cannot change my version, I implemented the given solution in my package:
{ // Controller
onInit: function () {
// ...
this.calendarTemporaryFix(); // Not needed since UI5 1.75
},
calendarTemporaryFix: function () {
// SinglePlanningCalendar required from "sap/m/SinglePlanningCalendar"
const fnSetSelectedView = SinglePlanningCalendar.prototype.setSelectedView;
SinglePlanningCalendar.prototype.setSelectedView = function (vView) {
if (typeof vView === "string") {
vView = sap.ui.getCore().byId(vView);
}
return fnSetSelectedView.call(this, vView);
};
},
// ...
}

Formatter function not working properly inside Input Control

I need to highlight a few of the table's cell border colors on the basis of a condition and pre-fill it with some negative value. Then I need to SAVE/POST this value to proceed further.
View.xml
<t:Column width="100px">
<Label text="ActualQty"/>
<t:template>
<Input id="idInput" value="{ parts: [ {path: 'viewData>ACT_QTY'}, {path: 'viewData>MTART'} ], formatter: '._formatter.defaultInput' }">
<customData>
<core:CustomData key="colorclass" value="{path: 'viewData>MTART', formatter: '._formatter.formatCell'}" writeToDom="true"/>
</customData>
</Input>
</t:template>
</t:Column>
Formatter.js
formatCell: function (iValue) {
try {
iValue.toString();
} catch (err) {
iValue = "foo";
}
return iValue.toString();
},
defaultInput: function (iValue, iValue1) {
if (iValue !== 0 && iValue1 === "HALB") {
iValue = "-1";
return iValue;
} else {
return iValue;
}
}
style.css
div[data-colorclass="HALB"] {
border: 4px solid #fdf6b1 !important;
}
Highlighting and the default value is appearing. But inside the controller, the input value is not coming.
If I remove parts and pass single input param to formatter function, it's working. But I need both the values to built my logic.
Update
Now I am using Composite Binding to make the binding as Two-way.
View.xml
<Input id="idInput" value="{ parts: [ {path: 'viewData>ACT_QTY'}, {path: 'viewData>MTART'} ], type: '._Compound', formatter: '._formatter.defaultInput' }">
Compound.js
sap.ui.define([
"sap/ui/model/CompositeType",
"dismantling/bom/integration/model/type/Compound"
], CompositeType => CompositeType.extend('Compound', {
constructor: function () {
CompositeType.apply(this, arguments);
this.bParseWithValues = true; // make 'parts' available in parseValue
},
formatValue: iValue => iValue,
parseValue: bValue => bValue,
validateValue: vValue => { /*validate...*/ },
}));
In the controller file, I am passing Compound type as _Compound. I am not getting any errors in the console.
Still, I am not able to get the formatter passed value inside the controller.
Your formatter is correct. Make sure sure that the values are actually accessible under your binding path and that the formatter is accessible to the view. If both these things are ensured, your function will work just fine.

sapui5 Not able to display data in detail page

I have a problem to display data in my detail page. I've tried almost everything but its dosnt work. On main page everything looks fine. Routing work (display proper ID on network address).
Details.controller.js :
return Controller.extend("sapProject.controller.Details", {
onInit: function () {
var oTable = this.getView().byId("details");
var oModel = new sap.ui.model.json.JSONModel();
oModel.loadData("model/Object.json");
oTable.setModel(oModel);
var oRouter = sap.ui.core.UIComponent.getRouterFor(this);
oRouter.getRoute("Details").attachMatched(this._onRouteMatched, this);
},
_onRouteMatched : function (oEvent) {
var oArgs, oView;
oArgs = oEvent.getParameter("arguments");
oView = this.getView();
oView.bindElement({
path : "/Objects(" + oArgs.employeeId + ")",
events : {
dataRequested: function () {
oView.setBusy(true);
},
dataReceived: function () {
oView.setBusy(false);
}
}
});
},
and this is my Details.view.xml:
<Page
id="details"
title="{i18n>EmployeeDetailsOf} {FirstName} {LastName}"
showNavButton="true"
navButtonPress="onBack"
class="sapUiResponsiveContentPadding">
<content>
<Panel
width="auto"
class="sapUiResponsiveMargin sapUiNoContentPadding">
<headerToolbar >
<Toolbar>
<Title text="{i18n>EmployeeIDColon} {EmployeeID}" level="H2"/>
<ToolbarSpacer />
</Toolbar>
</headerToolbar>
<content>
<f:SimpleForm>
<f:content>
<Label text="{i18n>FirstName}" />
<Text text="{FirstName}" />
<Label text="{i18n>LastName}" />
</f:content>
</f:SimpleForm>
</content>
</Panel>
</content>
</Page>
I think you are binding an empty model to your detail view because probably the loadData function is not completed when you set the model on the Table.
Try to load your json file in the manifest (best option) or differ the setModel on the _onRouteMatched function (although I don't see any table in your detail view).
EDIT:
You can also use this code after oModel.loadData("model/Object.json");
oModel.attachEventOnce("requestCompleted", function(oEvent) {
// Here your file is fully loaded
});
Firstly I recommend you to bind like this:
var sObjectPath = this.getModel().createKey("Objects", {
ID: oArgs.employeeId
});
this._bindView("/" + sObjectPath);
...
}
_bindView: function (sObjectPath) {
//Todo: Set busy indicator during view binding
this.getView().bindElement({
path: sObjectPath,
parameters: {
},
events: {
change: this._onBindingChange.bind(this),
dataRequested: function () {
}.bind(this),
dataReceived: function () {
}.bind(this)
}
});
},
Secondly check if oArgs.employeeId has a valid value and also if the model is loaded with data, easily set a brekapoint or write console.log(this.getView().getModel().oData).

UI5: Check if a control is currently rendered and visible

In a SAPUI5 application I would like to update the content of a control (e. g. a tile) only when this is currently visible to the user.
I created a function like this:
updatePage: function() {
for (var i = 0; i < this.myTiles.length; i++) {
if (this.myTiles[i].updater) {
this.myTiles[i].updater();
}
}
setTimeout(this.updatePage.bind(this), 10000);
},
.. where the updater is a custom function I added to the tiles that is in charge to update their content.
The problem is: I want to check if the tile is currently visible to the user (i. e. is not in a page or in a tab that is not currently selected, but was rendered previously).
Is there a way to achieve this using object properties? Do I need to manage it manually?
You can utilize jQuery do achieve that.
// determine whether your control is still part of the active DOM
jQuery.contains(document, yourControl.$());
// determine whether your control is visible (in terms of CSS)
yourControl.$().is(':visible');
// combined
MyControl.prototype.isVisible = function() {
return this.$().length && // has rendered at all
jQuery.contains(document, this.$()) && // is part of active DOM
this.$().is(':visible'); // is visible
};
Elements could still be invisible to the user by being:
out of the viewport
visibility: hidden
opacity: 0
...
Also check this answer
BR
Chris
http://api.jquery.com/jQuery.contains/
http://api.jquery.com/visible-selector/
The web API IntersectionObserver is now supported by all major browsers, including Safari.
Basic syntax
const myObserver = new IntersectionObserver(callback/*, settings?*/);
myObserver.observe(domElement);
Demo
Below is a demo with UI5. Run the snippet and try to scroll there. The page title changes depending on the visibility of the target element:
globalThis.onUI5Init = () => sap.ui.require([
"sap/ui/core/mvc/XMLView",
"sap/ui/model/json/JSONModel",
"sap/ui/core/mvc/Controller",
], async (XMLView, JSONModel, Controller) => {
"use strict";
const control = await XMLView.create({
definition: document.getElementById("myxmlview").textContent,
models: new JSONModel({ "ratio": 0 }),
controller: new (Controller.extend("demo.MyController", {
onInit: function () {
this.intersectionObserver = new IntersectionObserver(entries => {
const targetEl = this.byId("myBox").getDomRef();
const entry = entries.find(entry => entry.target === targetEl);
const model = this.getView().getModel();
model.setProperty("/ratio", entry && entry.intersectionRatio);
});
},
onBeforeRendering: function () {
this.intersectionObserver.disconnect();
},
onAfterRendering: function () {
const targetEl = this.byId("myBox").getDomRef();
this.intersectionObserver.observe(targetEl);
},
onExit: function () {
this.intersectionObserver.disconnect();
this.intersectionObserver = null;
},
}))(),
});
control.placeAt("content");
});
<script id="sap-ui-bootstrap"
src="https://openui5.hana.ondemand.com/resources/sap-ui-core.js"
data-sap-ui-libs="sap.ui.core,sap.m"
data-sap-ui-async="true"
data-sap-ui-theme="sap_horizon"
data-sap-ui-oninit="onUI5Init"
data-sap-ui-resourceroots='{ "demo": "./" }'
data-sap-ui-compatVersion="edge"
data-sap-ui-excludejquerycompat="true"
data-sap-ui-xx-waitForTheme="init"
></script>
<script id="myxmlview" type="text/xml">
<mvc:View controllerName="demo.MyController"
xmlns:mvc="sap.ui.core.mvc"
xmlns:core="sap.ui.core"
xmlns="sap.m"
displayBlock="true">
<App>
<Page title="Tile visible: {= ${/ratio} > 0}">
<FlexBox renderType="Bare"
height="360vh"
justifyContent="Center"
alignItems="Center">
<core:Icon id="myBox"
src="sap-icon://color-fill"
size="5rem"
color="Critical"/>
</FlexBox>
</Page>
</App>
</mvc:View>
</script>
<body id="content" class="sapUiBody sapUiSizeCompact"></body>