What are we going to achieve ❓
In this article, I will share an approach to build a three level nested multi-field (which can be extended to N level) and steps on how we can parse the same in our AEM component. The major issue that I wanted to tackle is writing a generic code to parse the multi-field as JSON without using the deprecated JsonItemWriter class and its dump method.
The use case is to display a hierarchy of country, state and city.
This approach is an extension of the one here. Please feel free to suggest improvements in terms of making the code generic and reusable. The code has been tested on AEM 6.4 GA. For AEM 6.3 you might need service pack 2 or more.
Touch UI Dialog:
Touch UI Dialog
Output on UI:
Level 1: Country, Level 2: State, Level 3: City
For this, we will need four sling models to start with (For every extra nested multi-field addition, we will need a new sling model with this approach):
To parse the level 1 multi-field items (list of countries).
To read level 1 multi-field (country) properties and parse level 2 multi-field items (list of states).
To read level 2 multi-field (state) properties and parse level 3 multi-field items (list of cities).
To read level 3 multi-field (city) properties.
Important point about the dialog is to set the composite property to true for each multi-field that is a parent of another nested multi-field.
composite:true to support nesting of multifields
Now the code for sling models:
For this let’s first visualize the content hierarchy.
Resource and countries node below the resource
Thus, we need to Inject a List<Resource> and mark it as Optional as it may or may not be present based on the authoring.
The rest of the code simply converts the List of Resource to List of appropriate class i.e. Country in this case.
@Inject
@Optional
private List<Resource> countries;
private List<Country> countriesList = new ArrayList<>();
public List<Country> getCountriesList() {
return countriesList;
}
public void setCountriesList(List<Country> countriesList) {
this.countriesList = countriesList;
}
@PostConstruct
protected void init() {
logger.debug("In init of CountriesModel");
if (countries != null && !countries.isEmpty()) {
for (Resource resource : countries) {
Country student = resource.adaptTo(Country.class);
countriesList.add(student);
}
}
}
This might be confusing as usually Sling models injects properties but countries is a child node. Referring to the official documentation of Sling, it becomes clear that grandchildren injection is allowed for Collections using Inject annotation.
Grandchildren injection for collections in Sling models
We extend the same logic to the country and state nodes which hold the list of states and cities respectively.
Each country has a name and a list of states under it.
@Inject
@Optional
private String country;
@Inject
@Optional
private List<Resource> states;
@Optional
private List<State> stateList = new ArrayList<>();
public List<State> getStateList() {
return stateList;
}
public void setStateList(List<State> stateList) {
this.stateList = stateList;
}
@PostConstruct
protected void init() {
logger.debug("In init method of Country model.");
if(!states.isEmpty()) {
for (Resource resource : states) {
State state = resource.adaptTo(State.class);
stateList.add(state);
}
}
}
And each state has a name with a list of cities under it.Each state has a name and a list of cities under it.
@Inject
@Optional
private List<Resource> cities;
@Inject
@Optional
private String state;
@Optional
private List<City> cityList = new ArrayList<>();
public List<City> getCityList() {
return cityList;
}
public void setStateList(List<City> cityList) {
this.cityList = cityList;
}
@PostConstruct
protected void init() {
logger.debug("In init method of Country model.");
if(!cities.isEmpty()) {
for (Resource resource : cities) {
City city = resource.adaptTo(City.class);
cityList.add(city);
}
}
}
Thus using Coral UI, you no longer have to worry about writing custom js to populate dialog values in case of nested multi-field. Using sling models it is easy to visualize the code in terms of content hierarchy and write Java code pretty much in the same manner as data is stored in the JCR.
For the complete codebase and more such demos please refer to this project. This project also has the logic to expose the multi-field data as a json using sling models for SPA based implementations. The point to note here is I have tried to do this without using deprecated APIs.
For those interested in how to expose the multi-field data as JSON please refer MultifieldToJson.java file from the repo.
The code simply uses a recursive function to do the following:
Check for properties at each level and add to JsonObject.
2. Check if the node has children. Add a child object to a JsonArray and repeat until no children are left.
3. If the current node has children starting with the item (for multi-field), then add each JsonObject to “items” JsonArray to preserve uniformity.
private JsonObject checkForChildren(Resource resource) throws RepositoryException {
Node resNode = resource.adaptTo(Node.class);
JsonObject resourceJson = new JsonObject();
if (null != resNode) {
for (PropertyIterator resProp = resNode.getProperties(); resProp.hasNext(); ) {
Property property = resProp.nextProperty();
if (!propertiesToIgnore.contains(property.getName()))
resourceJson.addProperty(property.getName(), property.getValue().getString());
}
if (resource.hasChildren()) {
JsonArray multiJson = new JsonArray();
for (Iterator<Resource> children = resource.listChildren(); children.hasNext(); ) {
Resource childResource = children.next();
JsonObject obj = checkForChildren(childResource);
//if resource has children, list children as json objects.
//but for multi use JsonArray
if (childResource.getName().startsWith("item"))
multiJson.add(obj);
else
resourceJson.add(childResource.getName(), obj);
}
if (multiJson.size() > 0)
resourceJson.add("items", multiJson);
}
}
return resourceJson;
}
The items array holds data for each level of multi-field. This code is generic and works for any number of nested multi-fields. Also, there is a utility function that takes the name of the multi-field root node so that this can be re-used for any multi-field as shown in the code below (note how I have passed countries as the name which can be replaced by any other multi-field root node name):
Java:
@Optional
@RequestAttribute
private String name; //get the name using an optional request param
public String getJsonMulti() throws RepositoryException {
Resource childResource = request.getResource().getChild(name); //get the root node using name
if (childResource != null) {
JsonObject resourceJson = checkForChildren(childResource);
return resourceJson.toString();
}
return StringUtils.EMPTY;
}
HTL:
<div data-sly-use.multi2Items=”${‘org.namaste.aem.core.models.MultifieldToJson’ @name=’countries’}”> Multi Json : ${multi2Items.getJsonMulti}</div>
For a quick glance at the HTL file refer this.
Below is how the json looks.
For a quick glance at the HTL file refer this.
Below is how the json looks.
Json output for multi-field.
Note: The code in this project is only meant to serve as a reference and you must thoroughly test it before using it in your own project.
Hope this helped. Cheers!
Note: The code in this project is only meant to serve as a reference and you must thoroughly test it before using it in your own project.
Hope this helped. Cheers!
No comments:
Post a Comment
If you have any doubts or questions, please let us know.