Vue components disappear after AEM changes

When you edit a component in Adobe Experience Manager (AEM) the system of AEM is smart enough to not reload the page but to only reload the html of the component. For content editors this is a nice way to change multiple settings on multiple components on a page without wasting any time waiting on the page to reload to see the new settings.

The problem

This comes with an issue; component in combination with 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.

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 components 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 show that every time we call REFRESH_FRONTEND - which AEM does for you when you setup your editConfig.xml the correct way – we fire an native JavaScript event and send it to the iFrame. Our Vue instance lives inside the iFrame and could listen to that event and act 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);

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.