Refresh custom control in sapui5 when model change - sapui5

I've a custom control which have multiple properties inserted in Detail View page. I've binded data with these properties. Scenario is I've two pages one is list view and then detail view. I've to navBack from detail page and select diff product from main page.Detail view page show diff products detail according to selected product. everything works fine. but problem is that my custom control doesn't update values and other page have updated values.
<custom:product topic="" subTopic="{product>name}" id="productDetial"></custom:product>
I've used one methond this.getView().byId("productDetail").rerender(); but it doesn't update my Inner HTML of control.
the control code. might be some typos error.as I've changed some variables name and remove unwanted code. the purpose is to show the methods which I've used and how I did
sap.ui.define([
"sap/m/Label",
"sap/m/Button",
"sap/m/CustomTile"
], function(Label, Button, CustomTile) {
"use strict";
jQuery.sap.declare("testProduct.control.product");
return CustomTile.extend("testProduct.control.product", {
metadata: { // the Control API
properties: {
name: {
type: "string",
defaultValue: "--"
},
subTopic: {
type: "string",
defaultValue: "--"
}
}
},
init: function() {
},
rerender: function(oRM, oControl) {
},
renderer: function(oRM, oControl) {
oRM.write('<div class=" sapMTile customTileCourseDetail">');
oRM.write('<div class="leftTileYourScore">');
if (oControl.getSubTopic() !== "" && oControl.getSubTopic() !== undefined) {
oRM.writeEscaped(oControl.getSubTopic());
} else {
oRM.write(" ");
}
oRM.write('</div>');
oRM.write('</div>
}
});
});

Yo just need to add a setter function in you control. When the binding is refreshed/changes, UI5 will trigger a setter method specific to the property. So in you case for the property subTopic it expects a method setSubTopic. This method should define you own logic to update said property in the UI layer according to your needs.
Here is part of the code you need to add, you will also have to tweak the initial rendering logic a bit.
renderer: function (oRM, oControl) {
//oRM.write('<div class=" sapMTile customTileCourseDetail">');
oRM.write("<div")
oRM.writeControlData(oControl);
oRM.addClass("sapMTile customTileCourseDetail");
oRM.writeClasses();
oRM.write(">");
oRM.write('<div class="leftTileYourScore">');
if (oControl.getSubTopic() !== "" && oControl.getSubTopic() !== undefined) {
oRM.writeEscaped(oControl.getSubTopic());
} else {
oRM.write(" ");
}
oRM.write('</div>');
oRM.write('</div>');
},
setSubTopic: function(sText){
this.setProperty("subTopic", sText, true);
$("#"+this.sId+" .leftTileYourScore").html(sText);
}

Related

Custom Control: "Lifecycle-Method" when aggregation is updated

I am creating a simple custom control:
sap.ui.define([
"sap/ui/core/Control"
], function(Control) {
"use strict";
return Control.extend("my.control.SvgVisualizer", {
metadata: {
properties: {
width: {
type: "sap.ui.core.CSSSize",
defaultValue: "100%"
},
height: {
type: "sap.ui.core.CSSSize",
defaultValue: "100%"
}
},
aggregations: {
elements: {
type: "my.control.Element",
multiple: true,
singularName: "element"
}
}
},
renderer: {
apiVersion: 2,
render: function(oRm, oControl) {
oRm.openStart("svg", oControl.getId())
.attr("viewBox", oControl._sViewBox)
.attr("width", oControl.getWidth())
.attr("height", oControl.getHeight())
.openEnd();
oRm.close("svg");
}
}
});
});
Basically, it can be used to implement svg stuff with aggregations into SAPUI5.
When creating the svg (html)-element in the renderer function I need a viewBox.
I would like to calculate the value for the viewBox based on the properties of the controls in the aggregation elements. The calculation could be pretty heavy if there are loads of elements.
My question is where I should calculate the viewBox property. It needs to be recalculated when the aggregation elements changes (so init isn't enough) but it does not need to be recalculated every time the renderer function is called.
Is attaching a handler to the aggregation-binding in the constructor the best way: this.getBinding("elements").attachChange(...)
It is possible to override all the aggregation modifiers like addElement, removeElement, ...etc and recalculate there.
But I would suggest to implement some kind of change detection for the elements aggregation in the onBeforeRendering hook and perform calculations only if the aggregation has changed. This way you don't need to worry if you have overridden all the modifiers correctly and you have the implementation on a single place. For example:
onBeforeRendering: function () {
var currElements = this.getElements();
var recalculate = false;
if (!this._oldElements) {
recalculate = true;
} else if (this._oldElements.length !== currElements.length) {
recalculate = true;
} else if (... another condition that should trigger recalculation) {
recalculate = true;
}
if (recalculate) {
this._sViewBox = ...;
this._oldElements = currElements;
}
}

Bind CSS class of a UI5 control programatically to a model value

Is there a way to bind the class attribute of a ui5-input-template inside a sap.ui.table.Table to a model value?
What I tried so far is:
[
{
label: 'arow',
disabled: true,
class: 'myClass1',
data: [
{
value: 'rowVal1'
}
]
},
// ...
]
and
myTable.bindColumns("/columns", function (index: string, context: any) {
let indParts: string[] = index.split("-");
let ind = +indParts[indParts.length - 1];
var colLabel = context.getProperty().label;
let template = new sap.m.Input({
value: `{data/${ind}/value}`,
class: '{= ${class} }',
enabled: '{= !${disabled} && !${data/' + ind + '/disabled} }',
});
// template.addStyleClass('{class}');
// template.setClass('{class}');
let column = new sap.ui.table.Column({
label: colLabel,
width: `{width}`,
template: template,
});
return column;
});
myTable.bindRows("/rows");
It seems as if I cannot use the model binding here, only add static class values when I create the template. Is this right?
As suggested in the comment, one of the solutions is to enhance the control's set of properties with your own property to allow binding the style class.
Here is a working sample: https://embed.plnkr.co/ik9PIdHKvK8udpQt
And here a snippet from the control extension:
sap.ui.define([
"sap/m/Input",
"sap/m/InputRenderer",
], function(Input, InputRenderer) {
"use strict";
return Input.extend("demo.control.Input", {
metadata: {
properties: {
"styleClass": {
type: "string",
defaultValue: null,
bindable: true,
}
}
},
renderer: { // will be merged with the parent renderer (InputRenderer)
apiVersion: 2, // enabling semantic rendering (aka. DOM-patching)
// Implement the hook method from the parent renderer
addOuterClasses: function (oRenderManager, oInput) {
InputRenderer.addOuterClasses.apply(this, arguments);
oRenderManager
.class("demoControlInput") // Standard CSS class of demo.control.Input
.class(oInput.getStyleClass()); // Custom CSS class defined by application
},
},
});
});
As documented in the topic Extending Input Rendering, some base controls allow overwriting existing methods from the renderer. If you look at the sap.m.InputRenderer, for example, you can see that the renderer provides multiple hooks to be overwritten by subclasses such as the addOuterClasses.
And since styleClass in our customer control is a valid ManagedObject property, binding in JavaScript ("programmatically") also works:
new Input({ // required from "demo/control/Input"
// ...,
styleClass: "{= ${class}}"
});

How to pass a function to custom control

I have a question about custom controls in UI5. Say I want to use a formatter function in the custom control (see the snippet below). A colleague of mine insists that custom control should be as generic as possible (e.g. to be able to specify texts with commas, spaces and newlines in whichever way you need it to be). Thus my idea was to pass formatter function to the custom control. Is it possible and if yes how to do it?
sap.ui.define([
"pr/formatter/Formatter",
"sap/m/Popover",
"sap/m/Text"
], function(Formatter, Popover, Text) {
"use strict";
return Text.extend("pr.control.TextWithPopover", {
metadata: {
aggregations: {
_popover: {
type: "sap.m.Popover",
multiple: false,
visibility: "hidden"
}
}
},
init: function() {
const popover = new Popover({});
this.setAggregation("_popover", popover);
},
setText: function(text) {
if (this.getProperty("text") !== text) {
// How to make it generic?
const formattedText = Formatter.formatCommaListToNewLine(text);
const contentToAdd = new Text({ text: formattedText });
contentToAdd.addStyleClass("popoverContent");
// ...
}
},
renderer: "sap.m.TextRenderer",
});
});
UI5 introduced the standard type "function" to sap/ui/base/DataType in 1.46(Commit) which allows ManagedObject properties to receive functions as their values.
Control
return ControlToExtend.extend("MyControl", {
metadata: {
properties: {
/**
* This function will contain foo and bar as parameters.
* Applications should return xyz.
*/
doSomethingWith: {
type: "function",
},
},
},
// ...
getXYZ: function(/*...*/) {
const doSomethingWith = this.getDoSomethingWith(); // function from the application
if (typeof doSomethingWith == "function") {
const [foo, bar] = [/*...*/];
return doSomethingWith(foo, bar);
} else {
/*default behavior*/;
}
},
});
Application
<MyControl doSomethingWith=".myControllerMethod" /> <!-- or -->
<MyControl doSomethingWith="some.globally.available.function" /> <!-- or -->
<!-- Since 1.69: -->
<MyControl
xmlns:core="sap.ui.core"
core:require="{
'myRequiredFunction': 'mynamespace/myApplicationFunction'
}"
doSomethingWith="myRequiredFunction"
/>
Note: XMLTemplateProcessor (XML-view / -fragment) supports function properties only as of 1.56. (Commit)
myApplicationFunction: function(foo, bar) {
// create and return xyz however the application wants;
},
This way, the control has no hard dependency to the application while keeping the flexibility to allow changing the default output or behavior.
The above option is one of the many solutions to reduce tight couplings in UI5. Another solution would be to add a control property which can be then manipulated by applications via binding and formatter.
Generally, controls (or control libraries) and control consumers (e.g. applications) should be always developed independently; with an interface in between (e.g. MenagedObjectMetadata) and the controls being still open for extensions without disclosing how they're implemented internally.

Vuetify TreeView + Drag and drop

I am trying to implement drag and drop on Vuetify Treeview and data table. It seems like it is not supported fully but a workaround is described in this thread. The workaround is however not complete. Perhaps the community would benefit if someone created a codepen or similar on this?
What confuses me is that the component DragDropSlot.vue is created but "drag-drop-slot" is used in the code. Also there is a "_.cloneDeep(this.tree)" call where _ is not defined. I assume it should be replaced by something. When I comment that out drag and drop does still not work. Probably missed something more like defining data. Not sure of correct data types. It seems to be based on react which I have not worked with. Have just started to learn vue and vuetify.
I'm open for any suggestion for how to solve this.
All the best
I use V-Treeview with Vue.Draggable (https://github.com/SortableJS/Vue.Draggable).
I use direct link.
<script src="//cdn.jsdelivr.net/npm/sortablejs#1.8.4/Sortable.min.js"/>
<script src="//cdnjs.cloudflare.com/ajax/libs/Vue.Draggable/2.20.0 vuedraggable.umd.min.js"/>
<v-treeview
:active.sync="active"
:items="users"
:search="search"
item-key="Id"
item-text="UserName"
item-children="Children"
:open.sync="open"
activatable
color="warning"
dense
transition
return-object
>
<template v-slot:label="{ item }">
<draggable :list="users" group="node" :id="item.Id" :data-parent="item.ParentId" #start="checkStart" #end="checkEnd" >
<label>
<i class="fas fa-user mr-3" />
<span id="item.id" >{{item.UserName}}</span>
</label>
</draggable>
Also I add ParentId property to item tree model:
{
Id:1,
UserName: "John Doe",
ParentId: null,
Children:[{Id:2, ParentId: 1,...}]
}
Then I use start and end events where I search parent start node from I drag the item and parent end node where I drop the item. When parent is null the item is a root.
new Vue({
el: '#app',
vuetify: new Vuetify(),
components: {
vuedraggable
},
data() {
return {
active: [],
open: [],
users: [],
selectedItems: [],
}
},
mounted: function () {
this.fetchUsers();
},
methods: {
findTreeItem: function (items, id) {
if (!items) {
return;
}
for (var i = 0; i < items.length; i++) {
var item = items[i];
// Test current object
if (item.Id === id) {
return item;
}
// Test children recursively
const child = this.findTreeItem(item.Children, id);
if (child) {
return child;
}
}
},
checkStart: function (evt) {
var self = this;
self.active = [];
self.active.push(self.findTreeItem(self.users, evt.from.id))
},
checkEnd: function (evt) {
var self = this;
var itemSelected = self.active[0];
var fromParent = itemSelected.ParentId ? self.findTreeItem(self.users, itemSelected.ParentId) : null;
var toParent = self.findTreeItem(self.users, evt.to.id);
var objFrom = fromParent ? fromParent.Children : self.users;
objFrom.splice(objFrom.indexOf(itemSelected), 1);
if (toParent.Id === itemSelected.Id) {
itemSelected.ParentId = null;
self.users.push(itemSelected);
}
else {
itemSelected.ParentId = toParent.Id;
toParent.Children.push(itemSelected);
}
self.saveUser(itemSelected);
// self.active = [];
return false;
},
fetchUsers: function () {
//load from api
},
saveUser: function (user) {
//save
},
},
computed: {
selected() {
if (!this.active.length) return undefined
return this.active[0];
},
}
})
Hope I help you.
IngD.
After some additional work I ended up with implementing Drag and Drop on top of vuetify tree view and data table using this library:
https://www.vuetoolbox.com/projects/vue-drag-drop
At first I looked at draggable and similar but realized it was always based on that you move an element from position A to position B. I needed more control. For example I wanted the element to disappear when dropping on some drop zones.
found this component.
https://vuejsexamples.com/vuetify-draggable-v-treeview-component/
I didn't try it myself (because it has too few options), but it looks working well in demo.
Anyways, just to try

How to display NotFound when invalid hash is matched in SAPUI5

I did the steps to catch and handle invalid hashes with SAPUI5 but my application is not working.
When i try to navigate to NotFound view changing the Hash i only gets an Info message:
But the view isn't displayed.
[EDIT]:
Adding source code files:
Here i added the bypassed section
I've created the target in Targets section of the manifest:
This is the NotFound.controller.js
sap.ui.define([
"my/path/controller/BaseController"
], function (BaseController) {
"use strict";
return BaseController.extend("my.path.controller.NotFound", {
onInit: function () {
var oRouter, oTarget;
oRouter = this.getRouter();
oTarget = oRouter.getTarget("NotFound");
oTarget.attachDisplay(function (oEvent) {
this._oData = oEvent.getParameter("data"); // store the data
}, this);
},
// override the parent's onNavBack (inherited from BaseController)
onNavBack : function (oEvent){
// in some cases we could display a certain target when the back button is pressed
if (this._oData && this._oData.fromTarget) {
this.getRouter().getTargets().display(this._oData.fromTarget);
delete this._oData.fromTarget;
return;
}
// call the parent's onNavBack
BaseController.prototype.onNavBack.apply(this, arguments);
}
});
});
Here the NotFound.view.xml:
<mvc:View
controllerName="my.path.controller.NotFound"
xmlns="sap.m"
xmlns:mvc="sap.ui.core.mvc">
<MessagePage
title="{i18n>NotFound}"
text="{i18n>NotFound.text}"
description="{i18n>NotFound.description}"
showNavButton="true"
navButtonPress="onNavBack"/>
</mvc:View>
And here the onInit method at the App controller:
onInit: function(){
jQuery.sap.log.setLevel(jQuery.sap.log.Level.INFO);
var oRouter = this.getRouter();
oRouter.attachBypassed(function (oEvent) {
var sHash = oEvent.getParameter("hash");
// do something here, i.e. send logging data to the backend for analysis
// telling what resource the user tried to access...
jQuery.sap.log.info("Sorry, but the hash '" + sHash + "' is invalid.", "The resource was not found.");
});
oRouter.attachRouteMatched(function (oEvent){
var sRouteName = oEvent.getParameter("name");
// do something, i.e. send usage statistics to backend
// in order to improve our app and the user experience (Build-Measure-Learn cycle)
jQuery.sap.log.info("User accessed route " + sRouteName + ", timestamp = " + new Date().getTime());
});
}
and
Any can help me?
Regards,
Check this plunker:
https://plnkr.co/edit/pxOkRSM8c97hXO6gkbpV
The key config is this on manifest.json:
"targets": {
"tgHome": {
"viewPath": "sapui5Example",
"viewName": "home"
},
"notFound": {
"viewPath": "sapui5Example",
"viewName": "NotFound",
"transition": "show"
}
}
To fire the 'not found' route, download the plunker and in the URL, after the hash just type anything and you will the the not Found Page (if you do it directly on plunker it won't work). Here is a pic: