All Good Things...

Posted on Saturday, March 20, 2021

Nothing but good things to say

Eight Great Years

You may think this post is about how I left an awesome team at Jefferson last month for an exciting new opportunity at the American Red Cross, and you would be partially correct. It has been an honor and privilege to work alongside some of the most amazing colleagues one could ask for. I give all the respect in the world to the front-line clinical workers who risk their own well-being for the rest of us. These people truly do improve lives. My hope is that even in some small way the work we’ve done together has improved patient access to information and appointments, indeed removing some friction from what can be a stressful ordeal.

If you're a nerd of a certain caliber, you'll also recognize that I've used the same title as the final episode of Star Trek: The Next Generation, one of my all-time favorite shows. Captain Picard is by far the best captain of the Starship Enterprise. Don't @ me.

All that being said, what this post is really about is the final project I worked on in my final sprint at Jefferson: a carousel of patient testionials that is authorable in Adobe Experience Manager and utilizes a content fragment model and Sling Models.

Testimonial Carousel

With the previously-built solution for displaying patient testimonials, content was added to the properties at the component instance, meaning that if authors wanted to reuse the same testimonial on multiple pages, there was a lot of copying and pasting involved. Without even considering the carousel yet, this was a very non-DRY way to author content, and just an annoying user experience for the authors anyway.

My goal was to convert this to use a folder in the DAM as a central repository for all testimonial content that could be referenced individually or as a group. The best candidate for the job turned out to be the AEM Content Fragment.

The best tutorial I could find for using content fragment models that I would eventually want to iterate through turned out to be in a blog post instead of any official Adobe documentation. Much of the code for the single content fragment model borrows from their code.

Sling Model for Single Content Fragment

I realize that to an experienced Java developer this probably looks like "Baby's First Sling Model" but it's the first time I've built something like this so it's a milestone for me. Basically, this model takes either a string of the path to a content fragment resource that uses this model, or a ContentFragment resource itself, as in the case of the next model for iterating through multiple testimonials for a carousel, and provides several get methods for retrieving the initials, name, location, and quote, from the model, as well as the model itself.

// CFTestimonial.java

// package information and imports...

@Model(adaptables = Resource.class, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
public class CFTestimonial {

    @Inject @Self
    private Resource resource;
    @Inject
    ResourceResolver resourceResolver;
    private Optional<ContentFragment> contentFragment;

    @Inject 
    private String path;

    @PostConstruct
    public void init() {
        if(path != null) {
            Resource fragmentResource = resourceResolver.getResource(path);
            contentFragment = Optional.ofNullable(fragmentResource.adaptTo(ContentFragment.class));
        }
        else if(resource != null) {
            contentFragment = Optional.ofNullable(resource.adaptTo(ContentFragment.class));
        }
    }

    public String getModel() {
        return contentFragment
            .map(ContentFragment::getTemplate)
            .map(FragmentTemplate::getTitle)
            .orElse("");
    }

    public String getInitials() {
        return contentFragment
            .map(cf -> cf.getElement("initials"))
            .map(ContentElement::getContent)
            .orElse("");
    }

    public String getName() {
        return contentFragment
            .map(cf -> cf.getElement("name"))
            .map(ContentElement::getContent)
            .orElse("");
    }

    public String getLocation() {
        return contentFragment
            .map(cf -> cf.getElement("location"))
            .map(ContentElement::getContent)
            .orElse("");
    }

    public String getQuote() {
        return contentFragment
            .map(cf -> cf.getElement("testimonial"))
            .map(ContentElement::getContent)
            .orElse("");
    }

}

Sling Model for Iterating Content Fragments

Where the solution I was referencing started to break down was when I decided to have the carousel populated by paths rather than be tag-driven like the example. As such, most of this model is my own code. The following model takes an array of paths to content fragments that use the custom testimonial model and iterates through them. Those paths are specified by the author in the properties of the testimonial carousel component.

// CFTestimonialCarousel.java

// package information and imports...

@Model(adaptables = Resource.class, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
public class CFTestimonialCarousel {

    @SlingObject
    private Resource currentResource;

    @SlingObject
    private ResourceResolver resourceResolver;

    @Inject
    private Resource paths;

    private final List<CFTestimonial> testimonials = new ArrayList<>();

    @PostConstruct
    protected void init() {
      if(paths != null) {
        Iterator<Resource> pathsIterator = paths.listChildren();
        while(pathsIterator.hasNext()) {
          Resource path             = pathsIterator.next();
          ValueMap pathProperties   = path.adaptTo(ValueMap.class);
          String cfPath             = pathProperties.get("path","");
          Resource cfResource       = resourceResolver.getResource(cfPath);
          ContentFragment cf        = cfResource.adaptTo(ContentFragment.class);
          final CFTestimonial cfTestimonial = cfResource.adaptTo(CFTestimonial.class);
          if (cfTestimonial != null) {
            testimonials.add(cfTestimonial);
          }
        }
      }
    }
    public List<CFTestimonial> getTestimonials() {
      return testimonials;
    }
}

HTL

With the "hard part" out of the way, all that's left is to write is some HTL to render the ArrayList of testimonials, and then make it come alive with some CSS and JavaScript in the component's client library.

<sly data-sly-use.testimonialCarousel="tju.core.models.CFTestimonialCarousel" />

<!--/* check for testimonials */-->
<div data-sly-test="${!testimonialCarousel.testimonials.size}" style="height: 100px; text-align: center; padding: 25px;">
  Please choose at least one testimonial content fragment. Choose three or more for best results.
</div>
<div data-sly-test="${testimonialCarousel.testimonials.size}" class="testimonial-carousel-container">
  <div class="header-title">
    <h2 class="title-text">Testimonials</h2>
    <p>See what our clients have to say about us</p>
  </div>
  <div class="carousel">
    <div class="prev"></div>
    <!--/* iterate testimonials */-->
    <div data-sly-list.testimonial="${testimonialCarousel.testimonials}" class="testimonials">
      <div class="carousel-testimonial">
        <div class="testimonial-initials">${testimonial.initials}</div>
        <div class="testimonial-content">
          <div class="testimonial-name-and-location">
            <div class="testimonial-name">${testimonial.name}</div>
            <div class="testimonial-location" data-sly-test="${testimonial.location}">${testimonial.location}</div>
          </div>
          <div class="testimonial-quote">${testimonial.quote @ context='html'}</div>
        </div>
      </div>
    </div>
    <div class="next"></div>
  </div>
  <!-- /* iterate nav */ -->
  <div data-sly-list.testimonial="${testimonialCarousel.testimonials}" class="testimonial-nav">
    <button 
      class="testimonial-navitem" 
      aria-label="go to testimonial from ${testimonial.name}" 
      onclick="gotoTestimonial(${testimonialList.index @ context='html'})"
    >
      <span>${testimonial.name}</span>
    </button>
  </div>
</div>

The Final Product

And finally—no really!—this is how the carousel renders on the front end. My former colleague from Jefferson, Clayton Legg, did a great job with the design and initial concept of this component, among many others. Thanks to Bacon Ipsum for the delicious filler content. Feel free to expand the CodePen and take a look around at my HTML, CSS, and JS.