Vue components disappear in AEM

When you edit a component in Adobe Experience Manager (AEM) the system is smart enough to not reload the page but to only reload the html of the component. A nice trick to let editors edit content without wasting any time waiting on the page to reload.

The problem

Unfortunately this comes with an issue; frontend frameworks. The vision of most frontend frameworks is that only that framework should do dom manipulations. AEM changing the dom (reload the component) result in a situation where the frontend framework doesn't know about the changes since something changed outside its scope. Reloading custom components are therefore not picked up by the frontend framework and are not visible. Vue’s lifecycle only starts when you load a page. So it seems impossible to solve this problem – maybe only with a hard page refresh but that will result in a bad authoring experience.

The solution

I wrote a little piece of code to solve the issue, during the process I discovered a few extra problems, I'll describe the process so you all can benefit from it. The solution is tested on AEM 6.3 and 6.4.

Refresh methods

The problem starts with AEM trying to reload the component. This behaviour is defined in the _cq_editConfig.xml inside the component's folder. The options for the cq:listeners are REFRESH_SELF (default), REFRESH_PAGE and REFRESH_PARENT. `Self` is the default behaviour and reloads the component. `Page` does a page refresh, and `Parent` reloads the direct parent’s component. The options are actually JavaScript methods provided by AEM out of the box globally available on the window object. Since it's just a method to call a global available JavaScript method we could write our own; REFRESH_FRONTEND for example.

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0"
    cq:actions="[EDIT,COPYMOVE,DELETE,INSERT]"
    jcr:primaryType="cq:EditConfig"
    cq:disableTargeting="{Boolean}true">
    <cq:listeners
        jcr:primaryType="cq:EditListenersConfig"
        afteredit="REFRESH_FRONTEND" />
</jcr:root>

Dealing with iFrames

Now we know we can write our own JavaScript based refresh method it should possible to tell Vue to reload everything after a content editor changed a setting without reloading the whole page.

A new problem is that your frontend component does not live in the same frame as AEM. AEM implements an iFrame with your page in it. To send Vue the notification to refresh we need to send a message through frames.

function REFRESH_FRONTEND() {
    // iframe with all content
    var contentFrame = document.getElementById('ContentFrame');
    // Send message to iframe to tell Vue it should reload
    contentFrame.contentWindow.postMessage('edit', '*');
}

The script shows that every time we call REFRESH_FRONTEND - which AEM does for you when you setup your editConfig.xml the correct way – we fire a native JavaScript event and send it to the iFrame. Our Vue instance lives inside the iFrame and listens to that event and acts on it. Sending the message through frames is not possible with default simple JavaScript events, but we could benefit from PostMessage.

Your App initiation could look something like:

let app = null;
function initiateApp() {
    // global App instance
    app = new Vue({ el: appId });
}
// refresh App instance after edit in AEM
window.addEventListener('message', (e) => {
    if (e.data === 'edit') {
        app.$destroy();
        initiateApp();
    }
}, false);

This initiation will initiate Vue on page load as Vue is designed. When the PostMessage event is received it will detroy the Vue instance so Vue will stop listening to events in components and it will stop the lifecycle of the Vue application. Right after we destroyed Vue we do initialisation, like we do on page load, but now with the new html refreshed by AEM. This method is resource unfriendly and should never face customers. Vue will rerender and recalculate everything on the page. Since this method is only active and applied for authors it should be safe to use.

Telling AEM to keep author experience

We did a few nice things already; we can trigger a script after a content editor changes something and tell Vue to reload. But we introduced a new problem; AEM didn’t know Vue changed the page, so editors can’t click component anymore to change settings. So we solved the problem where Vue didn't know about mutations to the dom from AEM, but now we have the same problem but vica versa. This problem is easily solved by telling AEM with a JavaScript method to reload the author experience. We need to add:

var loadEvent = new Event('load');
contentFrame.dispatchEvent(loadEvent);

to the REFRESH_FRONTEND method. This will tell AEM that the page is loaded and AEM could start it's scripts - including the one to enable editing. Since Vue can take one JavaScript cycle to full render a page we can use the setTimeout hack to wait one JavasSript cycle before we tell AEM to reload. The full script will be:

function REFRESH_FRONTEND() {
    // iframe with all content
    var contentFrame = document.getElementById('ContentFrame');
    // Send message to iframe to tell Vue it should reload
    contentFrame.contentWindow.postMessage('edit', '*');
    // wait a cycle to make sure Vue has reload
    // and trigger AEM to determine active components
    setTimeout(function() {
        var loadEvent = new Event('load');
        contentFrame.dispatchEvent(loadEvent);
    }, 1);
}

Summary

The problem was not that AEM removed the component, but was the frontend library Vue not knowing AEM changed the dom. Therefore Vue didn't do anything when changed where made resulting in not rendered html nodes. By adding an extra option to the mutation listeners in AEM we'd manage to call a JavaScript method which told both Vue and AEM to reload the instances. The Vue application lives inside an iFrame so we needed to use PostMessages to communicatie between iFrames.