Passing data to component in AEM - aem

Background
I am trying to make a dynamic navigation component in AEM. Currently I have been trying to find information in the docs and online to help me but have not been able to find much.
My application loads vue components into AEM so the front end is controlled by VUE and the data is passed in through props.
I would like to pass the pages and sub pages of those pages into the vue component, so I can create a dynamic navigation with a drop down nav for any links.
Example
Currently I found a way to use a java class to load the root pages links. I am doing that like this,
package services.agro.something.core.models;
import java.util.*;
import java.util.Iterator;
import com.day.cq.wcm.api.Page;
import com.day.cq.wcm.api.PageFilter;
import com.adobe.cq.sightly.WCMUsePojo;
public class SidebarNavigationModel extends WCMUsePojo{
private List<Page> items = new ArrayList<Page>();
private Page rootPage;
// Initializes the navigation
#Override
public void activate() throws Exception {
rootPage = getCurrentPage().getAbsoluteParent(2);
if (rootPage == null) {
rootPage = getCurrentPage();
}
Iterator<Page> childPages = rootPage.listChildren(new PageFilter(getRequest()));
while (childPages.hasNext()) {
items.add(childPages.next());
}
}
// Returns the navigation items
public List<Page> getItems() {
return items;
}
// Returns the navigation root
public Page getRoot() {
return rootPage;
}
}
Then I could use this data like this,
<div data-sly-use.sidebarNavigationModel="services.agro.gde.core.models.SidebarNavigationModel" class="sidebar-navigation ae-spacing-indent">
<div class="sidebar-nav-container">
// This is a vue component that I am trying to pass data to. The data comes through as commas and when I expect an array an error throws in console stating it expects array but receives a string.
<sidebar-navigation title="${properties.title}" items="${sidebarNavigationModel.items}"></sidebar-navigation>
</div>
// This outputs 16 commas to the screen ${sidebarNavigationModel.items}
<ul class="nav navbar-nav navbar-center">
// This outputs a link to home
<li>HOME</li>
// This outputs 19 links like I expect
<li class="nav navbar-nav navbar-left" data-sly-repeat="${sidebarNavigationModel.items}">
${item.title}
</li>
</ul>
</div>
Question
If I could better understand the data set that is returned in the loop I believe I could pass it to the vue component as I have tried.
Why does this output commas when outside of the loop and why when passing it to vue does it appears to be a string?
How can I get the sub pages for a site when it loads into a data set I can pass to my vue component from the template?

HTL/Sightly is trying to render the array but for each item it will render nothing (since it cannot evaluate and render the Page object). What you could do is either massage the data in your Use-Object to return exactly the needed string or process it in the script like:
[<sly data-sly-repeat="${logic.items}">{title="${item.title}",path="${sidebarNavigationModel.path}"}${itemList.last?'':','}</sly>]

The vue component is printing commas because the list returned by java class is a list of com.day.cq.wcm.api.Page objects and not a list of String. Sightly understands that it is a Page object and hence able to invoke page methods on it to retrieve path and title.
Getting a list of subpages - There are different ways you can do this. Depends a lot on your content structure and your requirement - what you consider are child and parent pages.
You can set an attribute in page properties for all the parents pages and use this attribute to identify a parent page and then construct a map of parent - child pages.
Or if your content structure runs more than a level deep and you need child pages from deep down the heirarchy, you can recursively fetch all the child pages and format into a data structure your vue component understands.
Or you could use Page API's like hasChild(), listChildren(), etc to drive your logic of constructing parent-child nav structure.
All this can be achieved from your java class.

Related

Best way to send event from AngularJS parent component to Angular 7 child component? (ngUpgrade)

I have an AngularJS (1.7) app which is being migrated to Angular 7, using ngUpgrade. So the AngularJS and Angular frameworks are running at the same time, new components are written in Angular, and sometimes these modern Angular components are used inside of legacy AngularJS components.
In my case, the parent component needs to communicate with the child component in certain circumstances.
The ngUpgrade docs clearly show how to pass data and propagate events from the child Angular component to the parent AngularJS component:
<legacy-angularjs-component>
<div>{{ $legacyCtrl.whatever }}</div>
<modern-angular-child-component
[data]="$ctrl.initialDataForChildComponent"
(changed)="$ctrl.onChildComponentChanged($event)"
>
</modern-angular-child-component>
</legacy-angularjs-component>
To make that work, you just need to add a couple properties to the Angular child component: #Input() data; for the initial data, and #Output() changed = new EventEmitter<WhateverThing>(); that can then be used to propagate events to the parent component by doing this.changed.emit(this.whateverThing).
That all works, but what about propagating events from the parent to the child? I know how to do this in Angular, e.g. with #ViewChild or using observables, but those mechanisms are not available in my app's AngularJS environment. So the parent component cannot use them.
Two approaches I have tried that do work are:
Creating a separate service, which both components share.
Pass a reference to the parent controller into the child controller like this:
<legacy-angularjs-component>
<div>{{ $legacyCtrl.whatever }}</div>
<modern-angular-child-component
[observe]="$ctrl.registerObserver",
>
</modern-angular-child-component>
</legacy-angularjs-component>
...and then having the child component invoke this observe function:
ngOnInit() {
if (this.observe) {
// Pass reference to child component to the
// parent, so parent can directly send it messages.
this.observe(this);
}
}
This way, the parent has a direct reference to the child once the components are set up. The child implements some TypeScript interface that defines all the methods of the child that the parent can invoke to inform the child of events.
Both of those do work, but they both strike me as fairly kludgey and a lot of rigamarole to have to do for something as simple as sending an event to a child component.
Since this is easy to do in both AngularJS and Angular, I wondered if I might be missing an easier/simpler way to do the same thing in the context of ngUpgrade, where the parent is AngularJS and the child is Angular 7.
Or is my second approach a reasonable way to do it?
Well, as Eazy-E once said, "Ask, and ye shall immediately think of a simpler answer, right after you post the question in public."
A better solution than either of my first two mentioned above is to simply use regular Angular one-way bindings and use the OnChanges mechanism provided by Angular.
<legacy-angularjs-component>
<div>{{ $legacyCtrl.whatever }}</div>
<modern-angular-child-component
[observed]="$ctrl.valueThatChildIsObserving"
>
</modern-angular-child-component>
</legacy-angularjs-component>
Then, in the child component:
import { Component, Input, OnChanges, SimpleChanges } from '#angular/core';
export class MyChildCompoennt implements OnChanges {
#Input() observed: SomeWhateverObject;
ngOnChanges(changes: SimpleChanges) {
console.log(changes);
// Inspect changes (it contains old and new values
// for `observed` object. That object could be a string
// or something more complicated with multiple properties.\
}
}
This way, when the parent component wants to tell the child to do something, it can just do this.valueThatChildIsObserving = someWhatever;.
Angular will handle invoking the child componentngOnChanges(), passing it a structure containing the old and new value of the property.
You have to implement the child component so that it can inspect the changed value and execute the correct action, but that is pretty simple and avoids having to have the parent/child share references to each other.
I think this is a reasonably simple way to implement "propagate some kind of event from parent component to child component" in a hybrid AngularJS/Angular app using ngUpgrade.

How can I inherit page properties in AEM 6.2?

In our AEM 6.2 project, I ran to a scenario where I need to config a navigation in one page (let call this Homepage), all of the other pages can use home navigation config or use their own navigation config values.
I decided to use live copy because the clone pages can cancel the linked properties at any time and use their own values. But there is two problem with this approach:
Users must set the template for clone pages by edit their jcr: content/sling:resourceType and jcr: content/cq: template because the navigation is used in all pages and our web uses about 5+ templates.
Live copy does no allowed clone pages are children of source pages. But I was required to make web structure like this :
Home
|_ Page 1
|_ Page 1.1
|_ Page 2
|_ Page 3
Live copy maybe not suitable for this situation, we change to use HTL ${inheritedPageProperties}, this solve the template and structure issue but it creates two new problems:
Inherited properties in property config dialog of child page will be blank (because they are not set and called via ${inheritedPageProperties} )
If users change properties in "Page 1" page, "Page 1.1" (and Page 1.1.1, etc ...) will use these values (Because ${inheritedPageProperties} search the upper nodes to get value).
What our client want are :
All page can use navigation setting only from the homepage or their own page (use home page by default).
If use homepage properties, these values must show in their config dialog.
Try to avoid config template in CRXDE Lite
The website must have the parent-child structure
How can I achieve these requirements?
You can achieve this with a simple Sling Model and Sling's CompositeValueMap
CompositeValueMap docs state:
An implementation of the ValueMap based on two ValueMaps: - One containing the properties - Another one containing the defaults to use in case the properties map does not contain the values. In case you would like to avoid duplicating properties on multiple resources, you can use a CompositeValueMap to get a concatenated map of properties.
We can use this by supplying the descendant's value map (the current page's) then finding the correct ancestor and supplying its properties valuemap as the defaults.
for the purposes of this question, I always assume that 2rd descendant from root is always the ancestor (you can find your ancestor according to your requirnments)
package com.sample.helpers;
import com.day.cq.wcm.api.Page;
import com.day.cq.wcm.api.PageManager;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.api.wrappers.CompositeValueMap;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.OSGiService;
import org.apache.sling.models.annotations.injectorspecific.Self;
import javax.annotation.PostConstruct;
#Model(adaptables = Resource.class)
public class CustomInheritedPageProperties
{
ValueMap inheritedProperties;
#Self
Resource currentResource;
#OSGiService
PageManager pageManager;
#PostConstruct
protected void init() {
// get the current page, or the "descendant"
Page descendant = pageManager.getContainingPage(currentResource);
/* You have to add your custom logic to get the ancestor page.
* for this question's purposes, I'm always assuming it's the 3rd decendant of root
* more here: https://helpx.adobe.com/experience-manager/6-2/sites/developing/using/reference-materials/javadoc/com/day/cq/wcm/api/Page.html#getParent(int)
*/
Page ancestor = descendant.getParent(2);
// create a CompositeValueMap where the properties are descendant's and the defaults are ancestor's
inheritedProperties = new CompositeValueMap(descendant.getProperties(), ancestor.getProperties());
}
public ValueMap getInheritedProperties()
{
return inheritedProperties;
}
}
Now you can use this as follows
<sly data-sly-use.propHelper="com.sample.helpers.CustomInheritedPageProperties">>/sly>
<!--/* someProp here refers to the property you wish to get (inherited, of course)*/-->
<h1>propHelper.inheritedProperties.someProp</h1>

How to create page detail in AEM 6.1

I want to create a page detail of product containing the following information such as name, ID, type, status, quantity, description etc by AEM 6.1. But it seems difficult to me.
Assuming there is a table of product list. Once clicking the Read One link in a row, the detail page of the item will absolutely be rendered.
Do you have any ideas ?
I think this really depends on how your product details (ID, type, status, quantity, description, etc) are stored.
A good solution would be to create a new page component that reads an ID http parameter passed when clicking on the READ ONE button as an argument into the rendering of the new (PRODUCT DETAIL) page.
so the all the buttons would look like this: www.mywebsite/products/productdetails?ID=x where x is the ID of the product in the given row when clicked.
Your new page component would have to have a code snippet similar to this:
<div id="product-detail-component" data-sly-use.product="com.mypackage.models.ProductModel">
<ul>
<li>${product.ID} </li>
<li>${product.description}</li>
</ul>
</div>
And then you would need a new Sling Model class that can handle a http parameter
#Model({adaptables= Resource.class,SlingHttpServletRequest.class}) public class Product Model {
#PostConstruct
public void init()
{
/*
1.retrieve HTTP parameter which is ID
2.access resourceResolver/resource to locate node containing product properties relative to resource location
*/
}
}
This is a very basic and not functional example, but I think it may give you a better direction.

Given a Path get a reference to the Resource in Sightly

The component dialog has a pathfield widget where the authors can set a page path. In the Sightly component, I would like to look up that page resource and get (and display) properties from it.
The dialog...
<linkedPathLocation jcr:primaryType="cq:Widget"
fieldLabel="Linked Path"
name="./linkedPathLocation"
xtype="pathfield"
fieldDescription="Select a page. URL, Title, Description and Image are properties of the selected page"/>
The component code I would like to work (it's not).
<div class="row" data-sly-resource.page = "${properties.linkedPathLocation}">
<h1 >${page.title}</h1>
<p>${page.description}</p>
</div>
My question: Is there a way in Sightly to resolve and use some resource from a given path? If not, I could create a USE-API class and to do the following...
Page page = resourceResolver.resolve("/path/to/resource").adaptTo(Page.class);
I feel there should be a better answer which allows resources to be resolved directly from the Sightly, but the following USE-API solution works if not...
Java Use Class
public class PageHelper extends WCMUsePojo {
String pagePath = "";
#Override
public void activate() {
pagePath = get("path", String.class);
}
public Page getPage() {
return this.getPageManager().getPage(pagePath);
}
}
The component Sightly...
<div class="row" data-sly-use.linkedPage = "${'com.package.PageHelper' # path = properties.linkedPathLocation}">
<h1 >${linkedPage.page.title}</h1>
<p>${linkedPage.page.description}</p>
</div>
What you are trying to do, in essence, is render a Resource within the context of the rendering of another Resource. data-sly-resource seems the appropriate attribute to use, but instead of attempting to nest additional elements into the element containing the data-sly-resource you should define another Sightly .html file which dictates how the nested resource is to be rendered.
Let us say that your Resource is of type application/components/content/type. Within type.html you might have the following statement
<sly data-sly-resource="${properties.linkedPathLocation} # resourceType='application/components/content/type/subtype' />
You would then be able to define /apps/application/components/content/type/subtype/subtype.html containing the rendering to produce which would be invoked in the context of the Resource identified by your path.
The main purpose of Sightly templates is separation of responsibility (frontend & backend) so that we can have simple, clean and beautiful html markup which is designer friendly and easily readable.
For your case, writing a Class (Java/Javascript) to process dialog information, and supply it back to Sightly template is correct way. Read here for more details.

Play Framework Html class and Form Rendering without Input

I have 2 questions. Version 2.4.x of the Play! Framework.
Whenever I render a layout using layoutName.render(inputargs...); it returns an "Html" object. I have not found a reference to this class in any of the docs. Where do I find this class? This doesn't seem to yield anything. (Edit2: Found in play.twirl.api.Html)
I am rendering a search bar on every page which is created using an input form. The problem is that the main layout needs my model class for the form as an argument: mainLayout.render("title", something_else, new Form<Search>()); Is there any way of writing it as mainLayout.render("title", something_else); whilst still retaining the form?
Edit3: Apparently the form is not needed at all; so long the name of the input is the same as the model's name any form will work:
#helper.form(action = routes.SearchPage.search()) {
<input type="text" name="term">
}
will work for a search term, but only as long as "term" is a public variable of the class in which we are going to store the post data.
Edit:
Currently using a method that inserts the empty form. The problem is that the Html class is not found. I have no idea where it is located.
public static Html renderMainLayout(String title, java.util.ArrayList<String> content) {
play.data.Form<Search> userForm = play.data.Form.form(Search.class);
return layout.render(title, content, userForm);
}