Multiple properties from cq-dialog to dom template AEM - aem

I am dependent on getting a json-structure (or something similar) from an AEM cq-dialog to the DOM of the rendered page, where i pick it up by the rendered page's JS.
The sightly page template looks something like the below, here the data-labels are a json-containing the fields of the dialog. As you see I have manually typed all fields/properties:
<div id="myApp"
data-service="${properties.applicationService}"
data-labels="{"title":"${properties.title}","sub1":"${properties.sub1}","number":"${properties.number}"}"></div>
I rather like to be able to pick up all labels more dynamically: data-labels = ${properties.labels}
Can I get all the "label" properties from the cq-dialog to the template as one property?
My dialog has a couple of fields like below, all the properties on tab1 are considered "label" properties (and hence should be added to the #myApp element's data-labels attribute).
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0"
jcr:primaryType="cq:Dialog"
title="my Application"
xtype="dialog">
<items jcr:primaryType="cq:WidgetCollection">
<tabs jcr:primaryType="cq:TabPanel">
<items jcr:primaryType="cq:WidgetCollection">
<tab1
jcr:primaryType="cq:Widget"
title="Texts and Labels"
xtype="panel">
<items jcr:primaryType="cq:WidgetCollection">
<title
jcr:primaryType="cq:Widget"
fieldDescription="The title of the page."
fieldLabel="blablabla"
name="./title"
defaultValue="default value..."
xtype="textfield"/>
<sub1
jcr:primaryType="cq:Widget"
fieldDescription="First subtitle"
fieldLabel="blablba"
name="./subtitle1"
defaultValue="default value..."
xtype="textfield"/>
<number
jcr:primaryType="cq:Widget"
fieldDescription="The textfield label for number."
fieldLabel="number"
name="./number"
defaultValue="number"
xtype="textfield"/>
</items>
</tab1>
...

You could either write a custom ExtJs widget to store the data in the JCR as a JSON string or write a piece of backend (Java or JavaScript) code to read the properties and put them in a JSON object. Personally, I favour the latter approach.
Here's an example using Sling Models:
package com.mycompany.myproject.blah;
//imports, whatever
#Model(adaptables = Resource.class)
public class ItemsModel {
// Properties will be injected by Sling Models from the current resource
#Inject
private String title;
#Inject
private String subtitle1;
#Inject
private String number;
public String getJson() {
// use String concatenation to build a JSON document
// or create a JSON object using Gson or a similar library
// and serailize it to String
}
}
Then in your Sightly file, you can call the model
<div id="myApp" data-sly-use.model="com.mycompany.myproject.blah.ItemsModel"
data-service="${properties.applicationService}"
data-labels="${model.json}"></div>
If you don't want to or cannot use Sling Models, you could write a use class or use the JavaScript Use-API to achieve a similar result.
In your component folder, create a JS file, let's call it items.js, it could look like this:
"use strict";
use(function () {
var items= {};
items.title = "" + properties.get("title");
items.sub1 = "" + properties.get("sub1");
items.number = "" + properties.get("number");
return JSON.stringify(items);
});
To use it in your Sightly script, call it via data-sly-use:
<div id="myApp" data-sly-use.items="items.js"
data-service="${properties.applicationService}"
data-labels="${items}"></div>
If you want to retrieve a number of properties in a more dynamic manner (without specifying each key in your Java/JS code), you can just iterate over all properties and filter them while building the JSON object.
Here's a somewhat crude example in JavaScript that reads all properties of the current resource and puts them in a JSON string:
"use strict";
use(function () {
var result = {},
i,
keys,
key;
keys = properties.keySet().toArray();
for (i = 0 ; i < keys.length ; i ++) {
key = keys[i];
result["" + key] = "" + properties.get(key);
}
return JSON.stringify(result);
});
I'm afraid there's no explicit documentation for the JavaScript API because you're effectively using the same APIs as you would in the Java code. Sorry about the weird type conversions but for some reason, stringify complained about the objects retrieved unless I did the trick with prepending an empty string to enforce the type "" +
I tend not to use JS in my back-end code so I'm not very familiar with this particular style.
If you want to figure out what you can do with the properties object, take a look at the ValueMap javadoc

Related

How to synchronize control values within different views

I would like to know how to get the content of TextArea, assign the value to a variable, set it to a model, and then set the variable to another TextArea in another view. I have coded some examples and it works, but not on TextArea.
Here is the example code:
// In init of the Component.js
this.setModel(new JSONModel(), "TransportModel"); // JSONModel required from "sap/ui/model/json/JSONModel"
// In *.controller.js
this.getView().getModel("TransportModel").setProperty("/", {
"Serial": this.byId("mat_serial").getValue() // "mat_serial" == id of the Input box in XML view
});
In the last step, I set the Text from a different View (also XML and Input Box) with the Value of the Model Element.
<Text text="{TransportModel>/Serial}" />
That worked pretty well.
But how to do the same with the TextArea? How can I do it based on this model? The value that I want to use from the first TextArea should also be on a TextArea in another view.
UI5 supports two-way data binding. I.e. if the user changes something in the UI (e.g. user types something in the text area), that change will be reflected automatically in other bindings that listen to the change.
<!-- In view 1 -->
<TextArea value="{TransportModel>/Serial}" />
<!-- In view 2 -->
<Text text="{TransportModel>/Serial}" />
No need to get input values by hand. Simply let the framework synchronize the value.
How to use a local json model:
Create
initItemViewModel: function () {
return new JSONModel({
Serial: ""
});
}
this._oViewModel = this.initItemViewModel();
this.setModel(this._oViewModel, "TransportModel");
Using
this.getView().getModel("TransportModel").setProperty("/Serial", serial);
<Text text="{TransportModel>/Serial}" width="auto" maxLines="1"/>

How to show hide elements based on select inside a multifield in Touch UI AEM 6?

I have a dropdown that has two options "image" and "icon".
When the user selects "image" I want to show the pathbrowser and when he selects "icon" I will show a text field.
This is famous problem, now I want to do this when these above mentioned fields are inside a multifield in Touch UI.
So say I have two items under this multifield, when I select "image", in the select present in the first item(of the multfield) the OOTB showhide hides my "icon" textfield of the first and the second item entry in the multifield as well.
How do I resolve this ?
Long story short See Blog. I want to do this. Just that my fields are inside a multifield.
Note:
I was able to implement the Classic UI code using ExtJs field.nextSibling() so I don't affect the entries in the other multifield item entries.
Find the code below and for more details check this gitlink here
.content.xml
<enable
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/checkbox"
text="Enable"
id="enable"
value="true"
name="./enable"
class="cq-dialog-checkbox-showhide"
cq-dialog-checkbox-showhide-target=".button-option-enable-showhide-target"/>
<deleteEnable
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/hidden"
name="./enable#Delete"
value="true"/>
<showHideContainer
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/container"
class="hidden button-option-enable-showhide-target"
showhidetargetvalue="true">
<items jcr:primaryType="nt:unstructured">
<!-- some components to show/hide -->
</items>
</showHideContainer>
checkboxshowhide.js
(function(document, $) {
"use strict";
// when dialog gets injected
$(document).on("foundation-contentloaded", function(e) {
// if there is already an inital value make sure the according target element becomes visible
$(".cq-dialog-checkbox-showhide").each( function() {
showHide($(this));
});
});
$(document).on("change", ".cq-dialog-checkbox-showhide", function(e) {
showHide($(this));
});
function showHide(el){
// get the selector to find the target elements. its stored as data-.. attribute
var target = el.data("cqDialogCheckboxShowhideTarget");
// is checkbox checked?
var checked = el.prop('checked');
// get the selected value
// if checkbox is not checked, we set the value to empty string
var value = checked ? el.val() : '';
// make sure all unselected target elements are hidden.
$(target).not(".hide").addClass("hide");
// unhide the target element that contains the selected value as data-showhidetargetvalue attribute
$(target).filter("[data-showhidetargetvalue='" + value + "']").removeClass("hide");
}
})(document,Granite.$);

Control with bound property doesn't display model data in the UI

I'm migrating my app to new version of OpenUI5 (1.48) and have some problems with model bindings. I am using sap.ui.getCore().setModel(oModel, "myModel") for model declaration and when I'm trying to bind some controls to values from this model like this ...
<Text text="{local>/count}" />
... the value isn't displayed.
But if I get this model, set it to view in controller ...
var oModel = sap.ui.getCore().getModel("local");
this.getView().setModel(oModel);
<Text text="{/count}" />
... everything would work fine.
Maybe somebody faced a similar problem or has an idea what is wrong with my code?
You must be using a Component in your app. In that case, core models are not automatically propagated to the children of the ComponentContainer which is why your Text control doesn't know the model "local".
The reason why "{/count}" works is because you set the model explicitly on the view without any model name. If the model doesn't have a name, it's a default model and > has to be omitted in the binding path.
To learn more about where to set models, take a look at my answer to a similar question: https://stackoverflow.com/a/42251431/5846045
I think problem may be how you creating the JSON model!,
try this one.
Controller
sap.ui.define(["sap/ui/core/mvc/Controller",
"sap/ui/model/json/JSONModel",],
function(Controller,JSONModel) {
"use strict";
return Controller.extend("com.stackoverflow.testUI5", {
onInit:function(){
var oData = {
count:"1"
};
var oModel = new JSONModel(oData);
sap.ui.getCore().setModel(oModel , "local")
//this.getView().setModel(oModel ,"local");
}
});
});
XML View
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xml>
<mvc:View controllerName="com.stackoverflow.testUI5"
xmlns:mvc="sap.ui.core.mvc"
xmlns:core="sap.ui.core" xmlns="sap.m" >
<Text text="{local>/count}"/>
</mvc:View>
this snippet will work.

How to Access Elements from XML Fragment by ID

I am working on a SAPUI5 application. I have an XML view which contains an XML Fragment and a Button to save.
The fragment contains a few controls like drop-down, text field and a table.
When I press on the save button, I need to get all the rows in the table and call an OData update service.
The problem is in the onSave method in view controller. I get an error while accessing the table using its ID. Can anyone help me and advice how can I access controls used in fragments by their ID in the controller?
Here is the code snippet:
View:
<mvc:View xmlns:mvc="sap.ui.core.mvc" xmlns:core="sap.ui.core" xmlns:form="sap.ui.layout.form" xmlns="sap.m">
<Page>
...
<form:SimpleForm>
<core:Fragment id ="fr1" fragmentName="first" type="XML" />
<Button id="id1" press="onSave" />
</form:SimpleForm>
</Page>
</mvc:View>
Fragment definition:
<core:FragmentDefinition xmlns="sap.m" xmlns:core="sap.ui.core">
<Table id="tab1" mode="MultiSelect">
...
</Table>
</core:FragmentDefinition>
Controller:
sap.ui.controller("view", {
onSave: function() {
//var tab = this.getView().byId("tab1"); // Not working
var tab = sap.ui.getCore().byId("tab1"); // Not working
},
// ...
});
Accessing controls inside a fragment depends on how your fragment was created in the first place. Here is a list of cases with respective API to use to get the control reference.
Given:
this as a reference to the current controller instance
Fragment required from the module sap/ui/core/Fragment
<MyControl id="controlId"/> in the fragment definition
API to choose
👉this.byId("controlId");
... if the fragment was created with the view ID (either indirectly or directly):
this.loadFragment({ name: "..." }); // id: view ID given by default, API since 1.93
<!-- In the view embedding the fragment declaratively: -->
<core:Fragment fragmentName="..." type="XML"/><!-- id = view ID given by default -->
Fragment.load({ // API since 1.58
id: this.getView().getId(),
name: "...",
controller: this,
});
sap.ui.xmlfragment(this.getView().getId(), "...", this); // Deprecated
Resulting global ID: "componentId---viewId--controlId" *
👉this.byId(Fragment.createId("fragmentId", "controlId"));
... if a fragment ID was given with the view ID combined:
this.loadFragment({ id: this.createId("fragmentId"), name: "..." });
<core:Fragment id="fragmentId" fragmentName="..." type="XML"/>
Fragment.load({
id: this.createId("fragmentId"),
name: "...",
controller: this,
});
sap.ui.xmlfragment(this.createId("fragmentId"), "...", this); // Deprecated
Resulting global ID: "componentId---viewId--fragmentId--controlId" *
👉Fragment.byId("fragmentId", "controlId");
... if only the fragment ID was given without combining with the view ID:
this.loadFragment({
id: "fragmentId",
name: "...",
autoPrefixId: false, // Explicitly disabled view ID as prefix
});
Fragment.load({
id: "fragmentId",
name: "...",
controller: this,
});
sap.ui.xmlfragment("fragmentId", "...", this); // Deprecated
Resulting global ID: "fragmentId--controlId" *
👉sap.ui.getCore().byId("controlId");
... if no ID to prefix was given. The below settings are not recommended as all control IDs within the fragment definition will be registered globally without any prefix. The uniqueness of the IDs is not guaranteed!
this.loadFragment({ name: "...", autoPrefixId: false }); // Not recommended if no id
Fragment.load({ name: "...", controller: this }); // Not recommended
sap.ui.xmlfragment("demo.view.MyFragment", this); // Deprecated
Resulting global ID: "controlId"
* Do not rely on the resulting global ID, for example, concatenating ID parts manually in your application. Always use the dedicated APIs mentioned above such as byId and createId. See Stable IDs: All You Need to Know.
Favor model-first approach over byId
Instead of accessing the fragment controls directly, consider manipulating the UI via data binding. Changes in the model will be reflected in the UI automatically, and, if two-way binding is enabled, user inputs from the UI will be stored in the model directly.
SAP Fiori elements guidelines
When developing Fiori elements extensions, make sure to adhere to the documented compatibility guidelines, especially regarding byId:
[...] Don't access or manipulate SAP Fiori elements' internal coding.
[...] Must not access any UI elements that are not defined within your view extensions.
âš  Caution
If you do not adhere to this guideline, your app may not work with future SAPUI5 versions because SAP Fiori elements might exchange controls for new ones that have a different API.
Looking at the OpenUI5 code at GitHub, it seems that the Fragment delegates the local ID generation to the containing view if the <Fragment/> itself does not have an explicit ID.
So your code this.getView().byId("tab1") should work as soon as you remove the id="fr1" attribute from your <Fragment/> element.
When using explicit IDs there is a static Fragment.byId method to retrieve the control. I guess you have to use it like this:
// Fragment required from "sap/ui/core/Fragment"
var fragmentId = this.getView().createId("fr1");
var tab = Fragment.byId(fragmentId, "tab1");
To make it work without explicit fragment ID and without static Fragment.byId() I used the following code snippet:
var prefix = this.getView().createId("").replace("--", "");
var fragment = sap.ui.xmlfragment(prefix, "-- XML fragment name --", this);
after this you can use this.getView().byId("tab1") as with any other control.

Multifield component issue

I am creating a multifield component having 2 textfields. Following is my dialog xml.
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0"
jcr:primaryType="cq:Dialog"
title="dialog"
xtype="dialog">
<items jcr:primaryType="cq:WidgetCollection">
<links
jcr:primaryType="cq:Widget"
fieldLabel="QuickLinks"
name="./links"
xtype="multifield">
<fieldConfig
jcr:primaryType="cq:Widget"
xtype="multifield">
<items jcr:primaryType="cq:WidgetCollection">
<title
jcr:primaryType="cq:Widget"
fieldLabel="Title"
hideLabel="{Boolean}false"
name="./jcr:title"
xtype="textfield"/>
<url
jcr:primaryType="cq:Widget"
fieldLabel="URL"
name="./jcr:url"
xtype="textfield"/>
</items>
</fieldConfig>
</links>
</items>
</jcr:root>
I am able to edit the content, and the content gets saved. However I have 2 problems - 1) When the dialog loads, it is empty always, and it doesnt show the saved content when I reopen the dialog 2) The up and down arrows are not working any more. Any suggestions to fix these is highly appreciated. Thank you very much.
The multifield xtype's field config takes only one item (i.e you can have one textfield in it. When multiple values are configured they will be stored as a multivalued property called links and when only one value is configured it'll be stored as a single valued property called links). The entire data configured in your multifield will be stored as links property in your node. You won't be able to get them as "jcr:title" and "jcr:url".
You should create a custom xtype say "linksXtype" that stores the "jcr:title" and "jcr:url" as a single string separated by some pattern say "***" ("jcr:title***jcr:url").
You can find the details of creating a custom xtype here : link
The xtype can be created like this:
Ejst.CustomWidget = CQ.Ext.extend(CQ.form.CompositeField, {
/**
* #private
* #type CQ.Ext.form.TextField
*/
hiddenField: null,
/**
* #private
* #type CQ.Ext.form.ComboBox
*/
jcrtitle: null,
/**
* #private
* #type CQ.Ext.form.TextField
*/
jcrurl: null,
constructor: function(config) {
config = config || { };
var defaults = {
"border": false,
"layout": "table",
"columns":2
};
config = CQ.Util.applyDefaults(config, defaults);
Ejst.CustomWidget.superclass.constructor.call(this, config);
},
// overriding CQ.Ext.Component#initComponent
initComponent: function() {
Ejst.CustomWidget.superclass.initComponent.call(this);
this.hiddenField = new CQ.Ext.form.Hidden({
name: this.name
});
this.add(this.hiddenField);
this.jcrtitle = new CQ.Ext.form.TextField({
fieldLabel:"Jcr url",
cls:"ejst-customwidget-1",
listeners: {
change: {
scope:this,
fn:this.updateHidden
}
},
optionsProvider: this.optionsProvider
});
this.add(this.jcrtitle);
this.jcrurl = new CQ.Ext.form.TextField({
fieldLabel:"Jcr Title",
cls:"ejst-customwidget-2",
listeners: {
change: {
scope:this,
fn:this.updateHidden
}
}
});
this.add(this.jcrurl);
},
// overriding CQ.form.CompositeField#setValue
setValue: function(value) {
var parts = value.split("/");
this.jcrtitle.setValue(parts[0]);
this.jcrurl.setValue(parts[1]);
this.hiddenField.setValue(value);
},
// overriding CQ.form.CompositeField#getValue
getValue: function() {
return this.getRawValue();
},
// overriding CQ.form.CompositeField#getRawValue
getRawValue: function() {
return this.jcrtitle.getValue() + "***" +
this.jcrurl.getValue();
},
// private
updateHidden: function() {
this.hiddenField.setValue(this.getValue());
}
});
// register xtype
CQ.Ext.reg('linksXtype', Ejst.CustomWidget);
change the dialog.xml to something like this
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0"
jcr:primaryType="cq:Dialog"
title="dialog"
xtype="dialog">
<items jcr:primaryType="cq:WidgetCollection">
<links
jcr:primaryType="cq:Widget"
fieldLabel="QuickLinks"
name="./links"
xtype="multifield">
<fieldConfig
jcr:primaryType="cq:Widget"
xtype="linksXtype">
</fieldConfig>
</links>
</items>
</jcr:root>
To fetch the values iterate over the string array stored as links property and split each string by "***"
EDIT :
Adobe consultancy services under its ACS-Commons package provides a more elegant multifieldpanel widget to handle this use case. It simplifies the approach and eliminates the need to write a custom xtype for every combination of required fields. The data is stored in form of JSON format and comes with taglibs to extract data from the node. Link : http://adobe-consulting-services.github.io/acs-aem-commons/features/widgets.html#multi-field-panel-since-150
As Sharath says, you'll need to define your own custom XType, rather than putting multiple fields in the multifield itself.
As an alternative to concatenating fields within a String[] property, another approach is to create child nodes for each field added, e.g. rather than:
<links
link="[Example|http://example.com,Google|http://google.com]"/>
You would end up with:
<links>
<link_1
title="Example"
url="http://example.com"/>
<link_2
title="Google"
url="http://google.com"/>
<links>
You can read the values back without needing to parse them from a String value. It also means that things like rollout which update pathfields should work as standard.
The code is too long to produce here in full, but there's a nice starting point of this on the Adobe forums here. (It has an Adobe copyright notice, but posted by a user — not sure of it's official status, but good as a reference implementation; EDIT: possibly related to the Citytechnic MultiCompositeField on Github, as spotted by ery).
The sample above also takes the same approach as the multifield itself — i.e. it reads from a fieldConfig node of the composite & creates a property for each entry on the child nodes it creates.
This makes the composite field completely reusable, as you only need one composite XType no matter how many variations you want to create, i.e. it would allow you to take the approach you outline in the question:
<links
jcr:primaryType="cq:Widget"
fieldLabel="QuickLinks"
name="./links"
xtype="mtmulticompositefield">
<fieldConfigs jcr:primaryType="cq:WidgetCollection">
<title
jcr:primaryType="cq:Widget"
fieldLabel="Title"
hideLabel="{Boolean}false"
name="./jcr:title"
xtype="textfield"/>
<url
jcr:primaryType="cq:Widget"
fieldLabel="URL"
name="./jcr:url"
xtype="textfield"/>
</fieldConfigs>
</links>
It also allows you to use more complex XTypes as children, e.g. images, without any further work.
I know problem is resolved by this time I commenting but this is just for reference.
1) When the dialog loads, it is empty always, and it doesnt show the saved content when I reopen the dialog
Answer : I was using extjs to create multifield for my dialog and in my extjs set() function code was like
setValue: function(value) {
var link = JSON.parse(value);
this.websiteName.setValue(link.text);
this.websiteLinks.setValue(link.text);
this.hiddenField.setValue(value);
},
but code should be
setValue: function(value) {
var link = JSON.parse(value);
this.websiteName.setValue(link.field1Name);
this.websiteLinks.setValue(link.field2Name);
this.hiddenField.setValue(value);
},
Just correcting this your dialog will show populated values. Also check your name property in dialog. It should be correct.
2) The up and down arrows are not working any more.
This issue is with your js file mostly. What I experienced. Whenever clicks are not working, check your js for errors in developer tools in browser. With a single syntax error your js stop working and clicks too.
Hope this helps someone :)