This is Chapter 6 of a multi-part tutorial. Chapter 5 can be found here and an overview can be found here.
You can check out the finished code on GitHub or you can download the solution package:
下載
In this part of the WKND tutorial, a Byline Component is built to be used on the Article pages that displays authored information about the article's contributor.

The implementation of this new component includes a dialog that collects the byline content and a custom Sling Model dynamically retrieves the byline's
- Name
- Image
- Occupations
for display by a HTL script.

First, create the Byline component node structure and define a dialog. This represents the Component in AEM and implicitly defines the component's resource type by its location in the JCR.
The dialog exposes the interface with which content authors can provide. For this implementation, the AEM WCM Core Component's Image component will be leveraged to handle the authoring and rendering of the Byline's image, so it will be set as our component's sling:resourceSuperType.
-
jcr:title = Byline jcr:description = Displays a contributor's byline. componentGroup = WKND.Content sling:resourceSuperType = core/wcm/components/image/v2/image
<?xml version="1.0" encoding="UTF-8"?> <jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" jcr:primaryType="cq:Component" jcr:title="Byline" jcr:description="Displays a contributor's byline." componentGroup="WKND.Content" sling:resourceSuperType="core/wcm/components/image/v2/image"/>
This byline.html is revisited later in this part once the Sling Model is created. The HTL file's current state allows the component to display in the AEM Sites' Page Editor when it's drag and dropped onto the page.
Next, define a dialog for the Byline component with the following fields:
- Name: a text field the contributor's name.
- Image: a reference to the contributor's bio picture.
- Occupations: a list of occupations attributed to the contributor. Occupations should be sorted alphabetically in an ascending order (a to z).
-
Update the cq:dialog with the following XML. It is easiest to open up the .content.xml and copy/paste the following XML into it.
<?xml version="1.0" encoding="UTF-8"?> <jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:granite="http://www.adobe.com/jcr/granite/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0" jcr:primaryType="nt:unstructured" jcr:title="Byline" sling:resourceType="cq/gui/components/authoring/dialog"> <content jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/coral/foundation/container"> <items jcr:primaryType="nt:unstructured"> <tabs jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/coral/foundation/tabs" maximized="{Boolean}false"> <items jcr:primaryType="nt:unstructured"> <!-- This allows the Core Components' Image component's asset definition tab --> <asset jcr:primaryType="nt:unstructured" sling:hideResource="{Boolean}false"/> <!-- This hides the Core Components' Image component's metadata tab --> <metadata jcr:primaryType="nt:unstructured" sling:hideResource="{Boolean}true"/> <properties jcr:primaryType="nt:unstructured" jcr:title="Properties" sling:resourceType="granite/ui/components/coral/foundation/container" margin="{Boolean}true"> <items jcr:primaryType="nt:unstructured"> <columns jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/coral/foundation/fixedcolumns" margin="{Boolean}true"> <items jcr:primaryType="nt:unstructured"> <column jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/coral/foundation/container"> <items jcr:primaryType="nt:unstructured"> <name jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/coral/foundation/form/textfield" fieldLabel="Name" fieldDescription="The contributor's name to display." emptyText="Enter the contributor's name to display." name="./name" required="{Boolean}true"/> <occupations jcr:primaryType="nt:unstructured" fieldLabel="Occupations" fieldDescription="A list of the contributor's occupations." required="{Boolean}false" sling:resourceType="granite/ui/components/coral/foundation/form/multifield"> <field jcr:primaryType="nt:unstructured" name="./occupations" emptyText="Enter an occupation" sling:resourceType="granite/ui/components/coral/foundation/form/textfield"/> </occupations> </items> </column> </items> </columns> </items> </properties> </items> </tabs> </items> </content> </jcr:root>
Note lines 15-22 above. These node definitions use Sling Resource Merger to control which dialog tabs are inherited from the sling:resourceSuperType component, in this case the Core Components' Image component.
Following the same approach as with the Dialog creation, create a Policy dialog (formerly known as a Design Dialog) to hide unwanted fields in the Policy configuration inherited from the Core Components' Image component.
-
Update the
cq :design_dialog with the following XML. It is easiest to open up the .content.xml and copy/paste the XML below into it.<?xml version="1.0" encoding="UTF-8"?> <jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:granite="http://www.adobe.com/jcr/granite/1.0" jcr:primaryType="nt:unstructured" jcr:title="Byline" sling:resourceType="cq/gui/components/authoring/dialog"> <content jcr:primaryType="nt:unstructured"> <items jcr:primaryType="nt:unstructured"> <tabs jcr:primaryType="nt:unstructured"> <items jcr:primaryType="nt:unstructured"> <properties jcr:primaryType="nt:unstructured"> <items jcr:primaryType="nt:unstructured"> <content jcr:primaryType="nt:unstructured"> <items jcr:primaryType="nt:unstructured"> <decorative jcr:primaryType="nt:unstructured" sling:hideResource="{Boolean}true"/> <altValueFromDAM jcr:primaryType="nt:unstructured" sling:hideResource="{Boolean}true"/> <titleValueFromDAM jcr:primaryType="nt:unstructured" sling:hideResource="{Boolean}true"/> <displayCaptionPopup jcr:primaryType="nt:unstructured" sling:hideResource="{Boolean}true"/> <disableUuidTracking jcr:primaryType="nt:unstructured" sling:hideResource="{Boolean}true"/> </items> </content> </items> </properties> <features jcr:primaryType="nt:unstructured"> <items jcr:primaryType="nt:unstructured"> <content jcr:primaryType="nt:unstructured"> <items jcr:primaryType="nt:unstructured"> <accordion jcr:primaryType="nt:unstructured"> <items jcr:primaryType="nt:unstructured"> <orientation jcr:primaryType="nt:unstructured" sling:hideResource="{Boolean}true"/> <crop jcr:primaryType="nt:unstructured" sling:hideResource="{Boolean}true"/> </items> </accordion> </items> </content> </items> </features> </items> </tabs> </items> </content> </jcr:root>
The basis for the preceeding Policy dialog XML was obtained from the Core Components Image (v2) component.
Like in the Dialog configuration, Sling Resource Merger is used to hide irrelevant fields that are otherwise inherited from the sling:resourceSuperType, as seen by the node definitions with only a
jcr :primaryType and sling:hideResource="{Boolean}true" property.
We'll add the Byline component in its current state to an Article page to verify that the node definition is correct, AEM sees the new component definition and the component's dialog works for authoring.
AEM authors configure and author components via the dialogs. At this point in the development of the Byline component the dialogs are included for collecting the data, however the logic to render the authored content has not yet been added.
-
With the dialog open, and the first tab (Asset) active, open the left sidebar, and from the asset finder, drag an image into the Image drop-zone.
Any AEM Asset can be used, but to use the same bio picture as in this tutorial, download the attached image and upload to AEM Assets. (Image courtesy of @staceygabby at unsplash.com)
下載
-
After adding an image, click on the Properties tab to enter the Name and Occupations.
When entering occupations, enter them in reverse alphabetical order (so the alphabetizing business logic we'll implement in the Sling Model is readily apparent).
Tap the Check icon in the top right to save the changes.
-
After saving the dialog, navigate to CRXDE Lite and review how the component's content is stored on the Byline component content node under the AEM page.
Find the Byline component content node beneath the jcr:content/root/responsivegrid/responsivegrid node i.e /content/wknd/en/sports/la-skateparks/jcr:content/root/responsivegrid/responsivegrid/byline.
Notice the property names of name, occupations, and fileReference are stored on the byline node.
Also, notice the sling:resourceType of the node is set to wknd/components/content/byline which is what binds this content node to the Byline component implementation.
/content/wknd/en/art/exhibitions/jcr:content/root/responsivegrid/byline
Next, we'll create a Sling Model to act as the data model and house the business logic for the Byline component.
Sling Models are annotation driven Java "POJO's" (Plain Old Java Objects) that facilitate the mapping of data from the JCR to Java variables, and provide a number of other nicities when developing in the context of AEM.
In order to most efficiently use Sling Models, the project's POM's need updating. Update the reactor and core pom.xml files to use the latest standards and practices. We'll also use the opportunity to leverage the Apache Commons-Lang3 library which is provided by AEM and provides a suite of very useful utility classes.
-
Ensure that the uber-jar dependency is at least 6.3.2
<dependencies> ... <dependency> <groupId>com.adobe.aem</groupId> <artifactId>uber-jar</artifactId> <version>6.3.2</version> <classifier>apis</classifier> <scope>provided</scope> </dependency> ... </dependencies>
-
Add a dependency for Core Components - Core. This is the dependency for the Sling Models associated with Core Components.
<dependencies> ... <dependency> <groupId>com.adobe.cq</groupId> <artifactId>core.wcm.components.core</artifactId> <version>2.2.0</version> <scope>provided</scope> </dependency> ... </dependencies>
-
Add dependency for Apache commons-lang3. This provides a number of helpful utilities classes.
``` <dependencies> ... <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.6</version> <scope>provided</scope> </dependency> ... </dependencies> ```
The full contents of the updated reactor pom.xml can be found here.
The <scope>provided</scope> indicates that AEM will be satisfying this dependency upon deployment. The version 3.6 is used as this is the version exposed by AEM which can be determined by looking up the exported version in the AEM Web Console's Dependency Finder (http://localhost:4502/system/console/depfinder).
-
Edit
aem -guides-wknd/core/pom.xml and add a matching dependency for core.wcm.components.core entry so the core project is aware of it. Note that the version and scope is not provided here, but rather these will be derived from the definition in the reactor pom.<dependencies> ... <dependency> <groupId>com.adobe.cq</groupId> <artifactId>core.wcm.components.core</artifactId> </dependency> ... </dependencies>
-
Update the maven-bundle-plugin in the core pom.xml to use the Sling ModelsScannerPlugin.
In the maven-bundle-plugin's instructions, remove the configuration <Sling-Model-Packages>...</Sling-Model-Packages>, and replace it with <_plugin>org.apache.sling.bnd.models.ModelsScannerPlugin</_plugin>.
The replaced configuration required the project explicitly specify the packages containing Sling Models. Sling ModelsScannerPlugin allows for the creation of Sling Models in any Java packages, resulting in more flexibility.
<plugins> ... <plugin> <groupId>org.apache.felix</groupId> <artifactId>maven-bundle-plugin</artifactId> <extensions>true</extensions> <configuration> <instructions> <Import-Package>javax.inject;version=0.0.0,*</Import-Package> <Bundle-SymbolicName>com.adobe.aem.guides.wknd.core</Bundle-SymbolicName> <_plugin>org.apache.sling.bnd.models.ModelsScannerPlugin</_plugin> </instructions> </configuration> <dependencies> <dependency> <groupId>org.apache.sling</groupId> <artifactId>org.apache.sling.bnd.models</artifactId> <version>1.0.0</version> </dependency> </dependencies> </plugin> ... </plugins>
Next, create a public Java Interface for the Byline. Byline.java defines the public methods needed to drive the byline.html HTL script.
-
@Version("0.0.1") package com.adobe.aem.guides.wknd.core.components; import org.osgi.annotation.versioning.Version;
-
package com.adobe.aem.guides.wknd.core.components; import java.util.List; /** * Represents the Byline AEM Component for the WKND Site project. **/ public interface Byline { /*** * @return a string to display as the name. */ String getName(); /*** * Occupations are to be sorted alphabetically in a descending order. * * @return a list of occupations. */ List<String> getOccupations(); /*** * @return a boolean if the component has content to display. */ boolean isEmpty(); }
The first two methods expose the values for the Name and Occupations for the Byline component.
The isEmpty() method is used to determine if the component has any content to render or if it is waiting to be configured.
Notice there is no method for the Image; we'll take a look at as to why that is later.
BylineImpl.java is the implementation of the Sling Model that implements the Byline.java interface defined earlier. The full code for BylineImpl.java can be found at the bottom of this section.
-
Open BylineImpl.java. It is auto-populated with all of the methods defined in the interface Byline.java.
package com.adobe.aem.guides.wknd.core.components.impl; import java.util.List; import com.adobe.aem.guides.wknd.core.components.Byline; public class BylineImpl implements Byline { @Override public String getName() { // TODO Auto-generated method stub return null; } @Override public List<String> getOccupations() { // TODO Auto-generated method stub return null; } @Override public boolean isEmpty() { // TODO Auto-generated method stub return false; } }
-
Add the Sling Model annotations by updating BylineImpl.java with the following class-level annotations. This @Model(..) annotation is what turns the class into a Sling Model.
import org.apache.sling.api.SlingHttpServletRequest; import org.apache.sling.models.annotations.Model; import org.apache.sling.models.annotations.DefaultInjectionStrategy; ... @Model( adaptables = {SlingHttpServletRequest.class}, adapters = {Byline.class}, resourceType = {BylineImpl.RESOURCE_TYPE}, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL ) public class BylineImpl implements Byline { protected static final String RESOURCE_TYPE = "wknd/components/content/byline"; ... }
Let's review this annotation and its parameters
:. - The @Model annotation registers BylineImpl as a Sling Model when it is deployed to AEM.
- The
adaptables parameter specifies that this model can be adapted by the request. - The adapters parameter allows the implementation class to be registered under the Byline interface. This allows the
HTL script to call the Sling Model via the interface (instead of theimpl directly). More details about adapters can be found here. - The resourceType points to the Byline component resource type (created earlier) and helps to resolve the correct model if there are multiple implementations. More details about associating a model class with a resource type can be found here.
The first method we'll tackle is getName() which simply returns the value stored to the byline's JCR content node under the property name.
For this, the @ValueMapValue Sling Model annotation can be used to inject the value into a Java field in the Sling Model.
... import org.apache.sling.models.annotations.injectorspecific.ValueMapValue; ... public class BylineImpl implements Byline { ... @ValueMapValue private String name; ... @Override public String getName() { return name; } ... }
Because the JCR property shares the same name as the Java field (both are "name"), @ValueMapValue automatically resolves this association and injects the value at the property name into the Java field.
If the JCR property had been named something different, additional annotations can be used to coerce the value injection. For example, if the JCR property had been named fullName, the following would inject its value into the Java field name.
import org.apache.sling.models.annotations.injectorspecific.ValueMapValue; import javax.inject.Named; ... @ValueMapValue @Named("fullName") private String name;
The next method to implement is getOccupations()` This method should collect all the occupations stored in the JCR property occupations and return a sorted (alphabetically) collection of them.
Using the same technique explored in getName() the property value can be injected into the Sling Model.
@ValueMapValue private List<String> occupations;
Now that the JCR property values are available in the Sling Model via the injected Java field occupations, the sorting business logic can be applied in the getOccupations() method.
import java.util.Collections; ... public class BylineImpl implements Byline { ... public List<String> getOccupations() { if (occupations != null) { Collections.sort(occupations); return occupations; } else { return Collections.emptyList(); } } ... } ...
The last public method is isEmpty() which determines when the component should consider itself "authored enough" to render.
For this component, we have business requirements stating that all three fields, name, image and occupations must be filled out.
import org.apache.commons.lang3.StringUtils; ... public class BylineImpl implements Byline { ... @Override public boolean isEmpty() { if (StringUtils.isBlank(name)) { // Name is missing, but required return true; } else if (occupations == null || occupations.isEmpty()) { // At least one occupation is required return true; } else if (/* image is not null*/) { // A valid image is required return true; } else { // Everything is populated, so this component is not considered empty return false; } } ... }
Checking the name and occupation conditions are trivial (and the Apache Commons Lang3 provides the always handy StringUtils class), however, there is a problem in that it's unclear how the presence of the Image can be validated since we're using the Core Components Image component to surface the image.
There are two ways to tackle this:
1. Check if the fileReference JCR property resolves to an asset.
OR
2. Convert this resource into a Core Component Image Sling Model and ensure the getSrc() method is not empty.
We will opt for approach #2. The first approach is likely sufficient, but in this tutorial, the latter will be used to explore other features of Sling Models.
The first thing to do is create a private method that gets the Image. This method is left private because we do not need to expose the Image object in the HTL itself, and its only used to drive isEmpty().
import com.adobe.cq.wcm.core.components.models.Image; ... private Image getImage() { Image image = null; // Figure out how to populate the image variable! return image; }
There are two approaches to get the Image Sling Model:
1. Use the @Self annotation, to automatically adapt the current request to the Image.class
@Self
private Image image;
OR
2. Use the Apache Sling ModelFactory OSGi service, which is a very handy service, and helps us create Sling Models of other types in Java code.
We will opt for approach #2.
註解:
In a real-world implementation, approach #1, using @Self is preferred since it's the simpler, more elegant solution. In this tutorial we'll use the second approach, as it requires us to explore more facets of Sling Models!
Since Sling Models are Java POJO's, and not OSGi Services, the usual OSGi injection annotations, like @Reference, cannot be used, meaning the following would not work (modelFactory would be null).
import org.apache.sling.models.factory.ModelFactory; ... public class BylineImpl implements Byline { ... // will be null, so will not use @Reference annotation @Reference private ModelFactory modelFactory; }
Instead, Sling Models provide a special @OSGiService annotation that provides similar functionality.
import org.apache.sling.models.factory.ModelFactory; import org.apache.sling.models.annotations.injectorspecific.OSGiService; public class BylineImpl implements Byline { ... @OSGiService private ModelFactory modelFactory; }
With the ModelFactory available, a Core Component Image Sling Model can be created using modelFactory.getModelFromWrappedRequest(SlingHttpServletRequest request, Resource resource, java.lang.Class<T> targetClass), however, this method requires both a request and resource, neither yet available in the Sling Model.
For this, more Sling Model annotations are used!
To get the current request the @Self annotation
import org.apache.sling.models.annotations.injectorspecific.Self; ... @Self private SlingHttpServletRequest request;
Remember, using @Self Image image to inject the Core Component Image Sling Model was an option above - the @Self annotation tries to inject the adaptable object (in our case a SlingHttpServletRequest), and adapt to the annotation field type. Since the Core Component Image Sling Model is adaptable from SlingHttpServletRequest objects, this would have worked and is less code than our more exploratory approach.
Now we've injected the variables necessary to instantiate our Image model via the ModelFactory API. We will use Sling Model's @PostConstruct annotation to obtain this object after the Sling Model instantiates.
@PostConstruct is incredibly useful and acts in a similar capacity as a constructor, however, it is invoked after the class is instantiated and all annotated Java fields are injected. Whereas other Sling Model annotations annotate Java class fields (variables), @PostConstruct annotates a void, zero parameter method, typically named init() (but can be named anything).
import javax.annotation.PostConstruct; ... public class BylineImpl implements Byline { ... private Image image; @PostConstruct private void init() { image = modelFactory.getModelFromWrappedRequest(request, request.getResource(), Image.class); } ... }
Remember, Sling Models are NOT OSGi Services, so it is safe to maintain class state. Often the @PostConstruct derives and sets up Sling Model class state for later use, similar to what a plain constructor does.
Note that if the @PostConstruct method throws an exception, the Sling Model will not instantiate (it will be null).
/** * @return the Image Sling Model of this resource, or null if the resource cannot create a valid Image Sling Model. */ @Nullable private Image getImage() { return image; }
@Override public boolean isEmpty() { if (StringUtils.isBlank(name)) { // Name is missing, but required return true; } else if (occupations == null || occupations.isEmpty()) { // At least one occupation is required return true; } else if (getImage() == null || StringUtils.isBlank(getImage().getSrc())) { // A valid image is required return true; } else { // Everything is populated, so this component is not considered empty return false; } }
Note multiple calls to getImage() is not problematic as returns the initialized image class variable and does not invoke modelFactory.getModelFromWrappedRequest(...) which isn't an incredibly costly, but worth avoiding calling unnecessarily.
The final BylineImpl.java should look like:
package com.adobe.aem.guides.wknd.core.components.impl; import java.util.Collections; import java.util.List; import javax.annotation.PostConstruct; import org.apache.commons.lang3.StringUtils; import org.apache.sling.api.SlingHttpServletRequest; import org.apache.sling.models.annotations.DefaultInjectionStrategy; import org.apache.sling.models.annotations.Model; import org.apache.sling.models.annotations.injectorspecific.OSGiService; import org.apache.sling.models.annotations.injectorspecific.Self; import org.apache.sling.models.annotations.injectorspecific.ValueMapValue; import org.apache.sling.models.factory.ModelFactory; import com.adobe.aem.guides.wknd.core.components.Byline; import com.adobe.cq.wcm.core.components.models.Image; import com.drew.lang.annotations.Nullable; @Model( adaptables = {SlingHttpServletRequest.class}, adapters = {Byline.class}, resourceType = {BylineImpl.RESOURCE_TYPE}, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL ) public class BylineImpl implements Byline { protected static final String RESOURCE_TYPE = "wknd/components/content/byline"; @Self private SlingHttpServletRequest request; @OSGiService private ModelFactory modelFactory; @ValueMapValue private String name; @ValueMapValue private List<String> occupations; private Image image; @PostConstruct private void init() { image = modelFactory.getModelFromWrappedRequest(request, request.getResource(), Image.class); } @Override public String getName() { return name; } @Override public List<String> getOccupations() { if (occupations != null) { Collections.sort(occupations); return occupations; } else { return Collections.emptyList(); } } @Override public boolean isEmpty() { final Image image = getImage(); if (StringUtils.isBlank(name)) { // Name is missing, but required return true; } else if (occupations == null || occupations.isEmpty()) { // At least one occupation is required return true; } else if (image == null || StringUtils.isBlank(image.getSrc())) { // A valid image is required return true; } else { // Everything is populated, so this component is not considered empty return false; } } /** * @return the Image Sling Model of this resource, or null if the resource cannot create a valid Image Sling Model. */ @Nullable private Image getImage() { return image; } }
In the ui.apps module, open /apps/wknd/components/content/byline/byline.html we created in the earlier set up of the AEM Component.
<div data-sly-use.placeholderTemplate="core/wcm/components/commons/v1/templates.html"> </div> <sly data-sly-call="${placeholderTemplate.placeholder @ isEmpty=false}"></sly>
Let's review what this HTL script does so far:
- The placeholderTemplate points to Core Components' placeholder, which displays when the component is not fully configured. This renders in AEM Sites Page Editor as a grey box with the component title, as defined above in the
cq :Component `'s jcr :title property. - The data-sly-call="${placeholderTemplate.placeholder @ isEmpty=false} loads the
placeholderTemplate defined above and passes in a boolean value (currently hard-coded to false) into the placeholder template. When isEmpty is true, the placeholder template renders the grey box, else it renders nothing.
Update byline.html with the following skeletal HTML structure:
<div data-sly-use.placeholderTemplate="core/wcm/components/commons/v1/templates.html" class="cmp-byline"> <div class="cmp-byline__image"> <!-- Include the Core Components Image Component --> </div> <h2 class="cmp-byline__name"><!-- Include the name --></h2> <p class="cmp-byline__occupations"><!-- Include the occupations --></p> </div> <sly data-sly-call="${placeholderTemplate.placeholder @ isEmpty=true}"></sly>
Note the CSS classes follow the BEM naming convention. While the use of BEM conventions isn't mandatory, BEM is recommended as it's used in Core Component CSS classes and generally results in clean, readable CSS rules.
The Use block statement is one of the most common, as it's used to instantiate Sling Model objects in the HTL script and assign it to an HTL variable.
data-sly-use.byline="com.adobe.aem.guides.wknd.components.Byline" uses the Byline interface (com.adobe.aem.guides.wknd.components.Byline) implemented by BylineImpl and adapts the current SlingHttpServletRequest to it, and the result is stored in a HTL variable name byline (data-sly-use.<variable-name>).
<div data-sly-use.byline="com.adobe.aem.guides.wknd.core.components.Byline" data-sly-use.placeholderTemplate="core/wcm/components/commons/v1/templates.html" class="cmp-byline">...</div>
HTL borrows from
For example, invoking the Byline Sling Model's getName() method can be shortened to byline.name, similarly instead of byline.isEmpty, this can be shorted to byline.empty. Using full method names, byline.getName or byline.isEmpty, works as well. Note the () are never used to invoke methods in HTL (similar to JSTL).
Java methods that require a parameter cannot be used in HTL. This is by design to keep the logic in HTL simple.
The Byline name can be added to the component by invoking the getName() method on the Byline Sling Model, or in HTL: ${byline.name}.
<h2 class="cmp-byline__name">${byline.name}</h2>
HTL Expressions Options act as modifiers on content in
Expressions are added via the @ operator in the HTL expression. To join the list of occupations with ", ", the following code is used:
<p class="cmp-byline__occupations">${byline.occupations @ join=', '}</p>
Most HTL scripts for AEM Components leverage the placeholder paradigm to provide a visual cue to authors indicating a component is incorrectly authored and will not display on AEM Publish. The convention to drive this decision is to implement a method on the component's backing Sling Model, in our case: Byline.isEmpty().
isEmpty() is invoked on the Byline Sling Model and the result (or rather its negative, via the `!` operator) is saved to an HTL variable named hasContent:
<div data-sly-use.byline="com.adobe.aem.guides.wknd.core.components.Byline" data-sly-use.placeholderTemplate="core/wcm/components/commons/v1/templates.html" data-sly-test.hasContent="${!byline.empty}" ...>
Note the use of data-sly-test, the HTL test block is interesting in that it both sets an HTL variable AND renders/doesn't render the HTML element it's on, based on if the result of the HTL expression is truthy or not. If truthy, the HTML element renders, else it does not render.
This HTL variable hasContent can now be re-used to conditionally show/hide the placeholder.
<sly data-sly-call="${placeholderTemplate.placeholder @ isEmpty=!hasContent}"></sly>
<div data-sly-use.byline="com.adobe.aem.guides.wknd.core.components.Byline" data-sly-use.placeholderTemplate="core/wcm/components/commons/v1/templates.html" data-sly-test.hasContent="${!byline.empty}" class="cmp-byline"> <div class="cmp-byline__image"> <!-- Include the Core Components Image component --> </div> <h2 class="cmp-byline__name">${byline.name}</h2> <p class="cmp-byline__occupations">${byline.occupations @ join=', '}</p> </div> <sly data-sly-call="${placeholderTemplate.placeholder @ isEmpty=!hasContent}"></sly>
Since we use sling:resourceSuperTyped the Core Components Image component to provide the authoring of the image, we can also use the Core Component Image component to render the image!
For this, we need to include the current byline resource, but force the resource type of the Core Components Image component, using resource type core/
<div class="cmp-byline__image" data-sly-resource="${ '.' @ resourceType = 'core/wcm/components/image/v2/image' }"></div>
This data-sly-resource, included the current resource via the relative path '.', and forces the inclusion with the resourceType of core/wcm/components/image/v2/image.
The Core Component resource type is used directly, and not via a proxy, because this is an in-script use, and it's never persisted to our content.
Completed byline.html below:
<div data-sly-use.byline="com.adobe.aem.guides.wknd.core.components.Byline" data-sly-use.placeholderTemplate="core/wcm/components/commons/v1/templates.html" data-sly-test.hasContent="${!byline.empty}" class="cmp-byline"> <div class="cmp-byline__image" data-sly-resource="${ '.' @ resourceType = 'core/wcm/components/image/v2/image' }"> </div> <h2 class="cmp-byline__name">${byline.name}</h2> <p class="cmp-byline__occupations">${byline.occupations @ join=','}</p> </div> <sly data-sly-call="${placeholderTemplate.placeholder @ isEmpty=!hasContent}"></sly>
After deploying the update, the image now appears and we have a un-styled, but working Byline component.

The AEM Web Console's Sling Models Status view displays all the registered Sling Models in AEM. The Byline Sling Model can be validated as being installed and recognized by reviewing this list.
If the BylineImpl was not displayed in this list, then there was is likely an issue with the Sling Model's annotations or with the SlingModelsScanner Module configuration in the core pom.xml.


The Byline component needs to be styled to align with the creative design for the Byline component. This will be achieved by using LESS, which AEM provides support for via its Client Libraries, to generate the necessary CSS to provide the correct default style for the Byline component.
After styling, the Byline component should adopt the follow aesthetic.

Add default styles for the Byline component. In the ui.apps project under /apps/wknd/clientlibs/clientlib-site/components.
.cmp-byline { .cmp-byline__image { // Align to the left of Name and Occupations .cmp-image__image { // This is the BEM class applied by the embedded Core Image component to the actual img tag // Create a 60px x 60px, circular image } } .cmp-byline__name { // Default text color // Right of image } .cmp-byline__occupations { // Color grey // All uppercase // Align right of image } }
Starting from this skeletal framework of the CSS class we are "allowed" to target makes writing clean CSS more straightforward, and reduces the temptation to style
Let's drop in the style implementations, and re-use the previously defined LESS variables from /apps/wknd/clientlibs/clientlib-site/site/less/variables.less.
/* WKND Byline - default.less*/ .cmp-byline { @imageSize: 60px; .cmp-byline__image { float: left; /* This class targets a Core Component Image CSS class */ .cmp-image__image { width: @imageSize; height: @imageSize; border-radius: @imageSize / 2; object-fit: cover; } } .cmp-byline__name { margin-left: @imageSize + 20px; margin-bottom: .25rem; } .cmp-byline__occupations { margin-left: @imageSize + 20px; color: @gray-light; font-size: @font-size-small; text-transform: uppercase; font-weight: 600; } }
Update /apps/wknd/clientlibs/clientlib-site/main.less to include byline.less.
Add an import statement to include
/* WKND main.less */ ... /* Import WKND Components styles */ ... @import "components/byline/byline.less";
Deploy the code to AEM, clear the browser cache to ensure stale CSS is not being served, and refresh the page with the Byline component to get the full styled.

If you get stuck or have additional questions make sure to check out the Experience League forums for AEM or view existing GitHub issues.
Didn't find what you were looking for? Think you found an error? Please file a GitHub issue for the WKND project.
Next part in the tutorial:
- Getting Started with AEM Sites Chapter 7 - Teaser and Carousel Components
- View the finished code on GitHub or download the finished package for this part of the tutorial:
下載
Note if you do not see src/main/java source folder in Eclipse you can add the folders by right clicking src and adding folders for main and java. After adding the folders you should see the src/main/java package appear.

Note if you have unresolved package imports for some of the new dependencies added to the core project, try updating the wknd-sites-guide.core maven project. You can do this by right-clicking wknd-sites-guide.core > Maven > Update Project.
