Zoom Mode - reggie7/aem-tools GitHub Wiki
Zoom mode is the most heavily used and also the most mature special mode showcased in AEM Tools. It came to be as an answer to growing complexity of components that I've come across in the projects I took part in. The idea is very simple here - we needed a way of zooming in from a big component to allow authoring of its smaller parts:
The simplest example from the video is enigmatic/zoomable. The zoomable component can be found at enigmatic/test/zoomable. It extends basic complex component. cq:editConfig defines the custom action config for zoom toggle button which comes from complex.js clientlib. zoomable.html defines what the regular view renders and zoomed.html is included from super type's zoom.html which drives the selector inside the zoom mode (the last sctipt is using template from lib to make the include).
The next examples are quite basic and come from gallery and en pages. Basic structure of all of the components is quite simple. First of all - each one extends complex component. On top of that the main container component exposes a zoomed.html script that the lib includes and every item component implements a zoom.html selector HTL script directly:
- gallery: zoomed.html + item/zoom.html
- slider: zoomed.html + item/zoom.html
- pricing table: zoomed.html + item/zoom.html
- testimonials: zoomed.html + item/zoom.html
The fixed components are:
They both take advantage of their super's main script in normal mode and simply implement respective zoomed.html scripts (configs does it directly and labels uses it's super's script).
Blog page shows two interesting examples of complex components that take advantage of zoom mode.
The first is quite simple, but presents an interesting application of the special mode. The stage component has a parallax background image under its remaining content. There are ways to do it differently (from the approach shown in the video) within a single view, but let's say we aim at having a separate image subcomponent to follow maximum reusability and aggregation principles. Then it's causing some authoring issues that are not so straightforward to overcome (and even if they are - let's remember we want to just show an example here and make a point out of how zoom can be used). One would need to play around with custom clientlibs usually to make it all work in a single view. Zoom mode allows us to avoid it - we simply show a publish rendition in the normal view and change it to a specific rendition for zoom mode. The zoomed rendition simply includes an image resource for editing just the way we wanted to.
Actual author UX is obviously a matter of debate in a production environment and depends on what the particular client expects, but the example stands. The above stage component shows what actually the previous fixed configs and labels components employ as well: zoom mode might apply to non-containers just as helpfully, when needed.
Blog's slider is one of two most complicated examples. It's purpose is to present nested zoom. Slider is a complex component that consists of items. And its items are also complex. So there has to be a way of making it all work together somehow. The second of those most complicated examples, hotspots component, is showcased on a separate page. Its purpose, on the other hand, is to present a very specific and entangled case that I've come across at some point of my career.
Under the hood
All of the above relies heavily on the custom parsys to which we can add custom selectors. The script that makes all of the above functionality work is parsys/zoom.html. It's using a ComplexComponent model class. When in zoom mode zoom.html of the parsys will be used instead of the selectors-free code. And the selector script does the following (using output from the ComplexComponent) c:
- displays c.child of the parsys if the child is an ancestor of c.target resource (c.outer is true),
- displays all its children if the parsys is a descendant of c.target resource (c.inner is true).
This will ensure to only show what is relevant in regards to the target resource passed as a parameter - just the way the video shows. Note that the described mechanism only covers content from paragraph systems of the page. Fixed resources need their own approach.
Up till this point it's basically the same as in the focus mode. The only difference is that focus.html will display additional info before the focused component which parsys/zoom.html won't do. That's because it was designed to work with components that extend complex base component (which focus does not assume).
Custom complex lib
So the first part of the zoom mechanism was parsys/zoom.html - it strips all useless markup out of our view in zoom mode (just as focus does as actually parsys/focus.html simply includes zoom.html).
The second part of zooming feature lies within complex base component. The zoom.html selector script is using a general template from lib.html to handle the rendering within zoom selector enabled page. Here again ComplexComponent plays a vital role. To enable zoom mode on a component the first thing to do is having it extend complex base component. By doing so the new one already inherits cq:editConfig - and thus the edit bar zoom in/out button as well. At this point the zoom selector will be called upon pressing the zoom button from author's perspective. As we've seen in the video a special URL is entered (the scheduled for refactoring complex.js clientlib takes care of this). The component from which zoom was called becomes target at this point (as indicated by a URL parameter). Now the selector script does the following via lib.html (using output from the ComplexComponent c) while rendering any component that extends complex:
- displays author info, if the current resource being rendered is exactly the c.target (what happens only as c.expand is true); at the same time a custom script named zoomed.html is included,
- displays one of the following scripts depending on the relation between the current resource and the target:
- if the current resource is a descendant of the target (c.inner is true) - inner.html is included,
- if the current resource is an ancestor of the target (c.outer is true) - outer.html is included,
- if the current resource and target are not linearly related (c.unrelated is true) - unrelated.html is included.
That's basically everything under the hood, the next section will supply examples that will clarify the theory and show how this mechanism is employed in practice to allow everything to work smoothly.
Usage
Basic rules of implementing zoom mode are quite similar to what focus mode needs in this matter. The difference lies in the fact here we'll need to extend complex component:
- make your new component extend complex base component
- you get cq:editConfig for free, in case you need to override it - remember to include the zoom in/out button,
- implement zoomed.html script with your custom rendering of the component to allow more authoring options.
The simplest example is the raw zoomable where zoomed.html does not add any authoring options, just simply shows the view change principles. Another basic example is the blog's stage component where zoomed.html presents a view that allows configuring the parallax background.
The next level of complexity in using zoom mode is when faced with container-like complex components. The problem is already explained for focus mode, so please get familiar with the scheme explanation first. The least steps for such a component to allow zoom-enabled view within a container are initially the same as explained above with the following changes/additions:
- usually zoomed.html will simply include a raw parsys,
- each item element for the container will need a separate zoom.html script to ensure a consistent look (the purpose is the same as that of item/focus.html).
There are plenty of examples that follow the container approach in aem-tools:
- gallery from its own page (refer to zoomed.html and item/zoom.html)
- slider from mobirise home page (refer to zoomed.html and item/zoom.html)
- pricing table from mobirise home page (refer to zoomed.html and item/zoom.html)
- user testimonials from mobirise home page (refer to zoomed.html and item/zoom.html)
A little non-standard approach can be checked within header component for which I've created a fixed resource in the mobirise home page. The header includes navigation and actually it's the topnav that is the target of zoom mode to enable editing of its links. It's achieved with header/zoomed.html acting basically as a proxy for topnav/zoom.html (refer also to column/zoom.html). Additionally the header shows an example where unrelated.html is overriden. This way we do not clear it out of view totally within zoom modes of other components.
Nesting zoom modes
Blog's slider presents the next level of complexity one can deal with via zoom mode. The video already shows it's about having a complex component that contains other complex components as its children. complex.js clientlib deals with switching between those modes depending on the target (parent vs child). Additionally the lib described above helps dealing with the issues that arise from having zoom selector scripts for those components working in different contexts. What do we mean by that?
Let's focus on the slider example as our story background. The standard view we start with is our blog page. It's selectors free, so there is no problem with determining which scripts should render the component.
The first step we would do is to enter slider's zoom mode view where the target is /content/mobirise/enigmatic/en/blog/jcr:content/content/slider. ComplexComponent c within slider's zoom selector will tell us that c.expand is true (c.inner, c.outer, c.unrelated will all be false then). This means that zoomed.html will be called (note that blog's slider extends the other one). So basically our slider will simply proxy to render a parsys of its items. But now each item also extends base complex component. So since the zoom selector drives the page - item's zoom.html will be used here. But this time its own ComplexComponent c will evaluate c.inner to be true (c.expand, c.outer, c.unrelated will all be false then). This means that inner.html will be included.
The next step now is to enter zoom mode view for one of the items within slider, so the target is now /content/mobirise/enigmatic/en/blog/jcr:content/content/slider/items/item_1. ComplexComponent c within slider's zoom selector will tell us that c.outer is true (c.inner, c.expand, c.unrelated will all be false then). This means that outer.html will be called. So basically our slider will simply proxy to render a parsys of its items. This time though parsys will evaluate its ComplexComponent to include only one child element - item_1, the target. So the view will not contain other items - causing no unnecessary distractions. Since item_1's type extends base complex component - zoom.html will be used here again. Its own ComplexComponent c will evaluate c.expand to be true (c.inner, c.outer, c.unrelated will all be false then). This means that zoomed.html will be included this time.
Rule of thumb
Following the above example we could describe the scenario below for introducing zoom mode of 2 levels:
- for the outer (parent) component's zoom:
- implement its zoomed.html,
- implement its items' inner.html scripts,
- for the inner (child) component's zoom:
- implement its zoomed.html,
- implement its container's outer.html.
This was the perspective of zoom levels. The same would rearrange from components' perspective as follows:
- in the outer (parent) component:
- implement zoomed.html,
- implement outer.html,
- in the inner (child) component:
- implement zoomed.html,
- implement inner.html.
More than 2 levels of zoom depth have not yet been tested (since I've not encountered such case in procuction yet).
Tricky cases
With a little more imagination and a lot more work zoom can be helpful in cases that would seem hopeless otherwise. Such a component is e.g. hotspots raw example shown in the video. Just as one can see there - hotspots are added via the image map tools of html5smartimage xtype. In production projects the hotspots can be basically anything. One example would be a dot that triggerred with a click action would open a layer. The layer can now be anything you want - only UX/design teams' creativity is the boundary here. In our case they're as simple as possible since we want to only illustrate something here. In RWD world the layers will look differently in mobile view - e.g. they might be rendered as a simple list under the image, which is exactly the case we present. This setup presents two challanges now: a single layer's configuration and the layers' order for mobile.
Easier thing to tackle is the single layers' configuration. In any real life scenario these will be too complex to put them in the same view as the main component. This is where zoom mode enters. The solution is quite simple here actually:
- cq:editConfig needs zoom action button,
- zoomed.html drives the configuration-frendly rendition,
- additionally outer.html has to be added for parent component (only because it extends complex as well).
To handle the ordering issue zoom mode was added to hotspots component directly. When the target for zoom is /content/enigmatic/hotspots/jcr:content/content/hotspots its zoomed.html will be called. It serves basically as a proxy to outer.html where the important thing is that it simply includes items but forces a special parsys type. Its purpose is to override inner.html from the base parsys. This way list.html is used instead of the main parsys.html script - thus we get rid of the Drag components here new area, but leave the ordering feature in place. But to make the construction of hotspots as flexible as possible the actual items resource is, as we've seen, a parsys/collection of hotspot items that are also collections/paragraph systems. Under this approach a single hotspot can actually hold any layer of arbitrary type (we can e.g. make separate video/iframe/image/etc. layer types) - giving us the flexibility intended. So we are rendering each item and, since it's actually a parsys - zoom will include inner.html again. And the latter simply renders the items in a disabled mode with all the selectors dropped - thus presenting us with a meaningful view of them.
Note that the second issue came from the fact that image holds the imageMap property while the actual layers are stored whithin items node. One could play with Ext JS, but which AEM BE dev really enjoys that?
Remarks
All of the above assumes the component to use zoom mode requires sling:resourceSuperType to point at complex. That is indeed convenient, but might not always be possible - then the solution is very simple:
- add zoom.html script to your component and allow it simply include /apps/enigmatic/foundation/complex/zoom.html,
- copy what you actually need from cq:editConfig.
The actual UX and UI designs of zoom mode for production environments are obviously subject to enhancements for a particular client - the version here is fully functional, yet it's still mostly the principles illustration and can be customized accordingly.