Search code examples
aemslingsightlysling-models

Display different markup in Sightly based on a Sling selector


The context

I'm working on an AEM 6 project that uses Sightly as the templating language. I'm facing a use case, in which I want to show or hide certain parts of the markup depending on the presence of a Sling selector.

For example, a request to /content/my-project/my-page.html should yield a basic page view and when a request is made to /content/my-project/my-page.ubermode.html, Sling should return the same content represented by a slightly different HTML document.

According to the Sling Cheat Sheet, it should be possible to use a different script.

I managed to implement this in a component by placing two Sightly scripts, mycomponent.html and ubermode.html (named after the selector)

/apps/(...)/mycomponent
            |- .content.xml
            |- _cq_editConfig.xml
            |- dialog.xml
            |- mycomponent.html
            |- ubermode.html

This does require some code duplication when it comes to the HTML structure but it works fine.

However, in this particular case, I need to do the same think at the renderer level (let's call this myapp/core/renderers/fancyPageRenderer). What's more, the renderer has a different renderer as its sling:resourceSuperType (let's call this parent renderer myapp/core/renderers/genericPageRenderer) and relies on a moderately complex series of includes (data-sly-include).

In fancyPageRenderer, I override one of the scripts initially defined and included in genericPageRenderer. This is the part that I'd like to be different when the ubermode selector is used. Let's call this script mainColumn.html

I've tried different naming conventions to match the selector but none of them worked in a satisfactory way.

This was the initial structure

/apps/(...)/renderers/fancyPageRenderer
                      |- .content.xml
                      |- mainColumn.html //this overrides a script included by a parent renderer

Here's what I tried:

/apps/(...)/renderers/fancyPageRenderer
                      |- .content.xml
                      |- mainColumn.uber.html
                      |- mainColumn.html

This simply didn't work and mainColumn.html would be included every time.


/apps/(...)/renderers/fancyPageRenderer
                      |- .content.xml
                      |- uber.html
                      |- mainColumn.html

This caused the uber.html script to be used but the resulting page did not contain any markup defined in the other scripts included in the genericPageRenderer


I imagine I could just copy all the relevant scripts and includes to fancyPageRenderer but that would result in massive and completely unacceptable code duplication.

I also know that it's possible to manually add, remove or replace selectors using data-sly-resource or just have the original selectors used but in my case, it's data-sly-include and not data-sly-resource that is widely used.

Is there an elegant way to work around this problem?


Solution

  • Eventually, I gave up on using script naming conventions to solve this problem and exposed a very simple Sling Model in the Sightly script of my renderer.

    Here's the current structure of fancyPageRenderer (which didn't change from the original):

    /apps/(...)/renderers/fancyPageRenderer
                          |- .content.xml
                          |- mainColumn.html //this overrides a script included by the parent renderer
    

    Here's what I used in the mainColumn.html Sightly script:

     <div class="fancy main-column" data-sly-use.uberMode="com.foo.bar.myapp.fancy.UberMode">
         <div data-sly-test="uberMode.enabled" >Uber-mode-only-content</div>
         <!-- Lots of markup here -->
         <div data-sly-test="!uberMode.enabled" >Explicitly non-uber-mode content</div>
         <div>Common content (but some uber-mode-dependend, nested divs as well, rendered the same way as above)</div>
     </div>
    

    and the underlying Sling Model, UberMode

    @Model(adaptables = SlingHttpServletRequest.class)
    public class UberMode {
    
        @Inject
        SlingHttpServletRequest request;
    
        private boolean enabled = false;
    
        @PostConstruct
        public void postConstruct() {
            if (request != null) {
                List<String> selectors = Arrays.asList(request.getRequestPathInfo().getSelectors());
                enabled = selectors.contains("ubermode");
            }
        }
    
        public boolean isEnabled() {
            return enabled;
        }
    }
    

    This allows me to avoid code duplication in Sightly and makes the selector-based logic unit-testable. Also, relying on naming conventions would get really tricky in a situation when I'd require more than one selector to decide what to render. Adding support for another relevant selector to this class would be quite straightforward in comparison.

    It also leaves me a lot of refactoring options. I could switch from using a selector to a query parameter or a header and only write a couple lines of code without even touching the Sigthly script that's effectively the client code of my class.