AEM with Vue.js part 2

Building a custom component

So far we have covered the basic setup of making VueJs work with AEM. Now I will show you how to build an AEM component that is enhanced with VueJs.

To do this, I have created a FAQ component with foldable items. The end result looks like this:

FAQ

As a first step, I created a vanilla FAQ component without any VueJs code. I added a dialog where the author can set a title and add a list of FAQ items:

FAQ list properties

The component is backed by a Sling Model that reads the items and maps each of them to a FaqItem Java bean. A faqItems property containing a list of these beans is exposed to the HTL template.

The first iteration of our HTL template simply renders the items as an unordered list.

<h2>${properties.title}</h2>
<ul data-sly-list="${faqModel.faqItems}">
    <li>
        <p>${item.question}</p>
        <p>${item.answer}</p>
   </li> 
</ul>
FAQ2

As this is everyday AEM development, most of you will be familiar with this, so I will not go into detail. You can examine the code in the GIT repo if you would like to dig deeper into it.

Let's jump immediately to the VueJs part.

GIT TAG: 5-vanilla-faq

Add VueJs to the component

I created a FaqList Vue component that renders the title and has a slot which renders whatever is put inside of the element. To support the @Component decoration, I had to enable experimentalDecorators in the Typescript config:

<template>
    <div>
        <h1>{{title}}</h1>
        <div>
            <slot></slot>
        </div>
    </div>
</template>

<script lang="ts">
    import Vue from 'vue';
    import { Component, Prop } from 'vue-property-decorator';

    @Component
    export default class FaqList extends Vue {
        @Prop() private title!: string;
    };
</script>

I also added an index.ts file that associates a tag with this component:

Vue.component('avd-faq-list', FaqList);

In the HTL template, we can use this tag to call the Vue component:

<avd-faq-list title="${properties.title}">
    <ul data-sly-list="${faqModel.faqItems}">
        <li>
            <p>${item.question}</p>
            <p>${item.answer}</p>
        </li> 
    </ul>
</avd-faq-list>

To simplify debugging, I also added a dev build script that runs Webpack in development mode.

If we build and deploy these changes, the page looks the same, but when we inspect the page with Vue Devtools, it shows the FaqList Vue component in action and the title property that is passed to it:

model message

Great!

Passing AEM component properties as attributes is a good way to access simple data in our Vue component. More complex data, such as the list of FAQ items, could also be passed in this way by converting the list to a JSON array in the AEM model and parsing this back to an object model in the Vue component. However, that doesn't really feel nice.

A better approach is to create a second Vue component to represent a single FAQ item and nesting this inside of the FaqList component.

Let's also pass the answer and question in template elements instead of attributes. In our example, both are simple text strings, but for other cases, it could be much more complex HTML. It will look much cleaner if we put it inside a tag instead of an attribute.

In the HTL template this looks like this:

<avd-faq-list title="${properties.title}">
    <avd-faq-item data-sly-repeat="${faqModel.faqItems}">
        <template #question>${item.question}</template>
        <template #answer>${item.answer}</template>
    </avd-faq-item>
</avd-faq-list>

The FaqItem Vue component enriches the FAQ item by showing the answer only if the question is clicked. It uses named slots to inject the content of the question and the answer into the appropriate place in the template. It has a property to store the open/closed state and a method that toggles the state when the question is clicked:

<template>
    <div class="faq-item">
        <h4 class="faq-question">
            <a href="#" @click.stop.prevent="expand">
                <slot name="question"></slot>
                <span class="caret" :class="{open: isOpen}"></span>
            </a>
        </h4>
        <div class="faq-answer" :class="{open: isOpen}">
            <p><slot name="answer"></slot></p>
        </div>
    </div>
</template>

<script lang="ts">
    import Vue from 'vue';
    import { Component } from 'vue-property-decorator';

    @Component
    export default class FaqItem extends Vue {


        private isOpen = false;


        expand() {
            this.isOpen = !this.isOpen;
            return false;
        }
    };
</script>

Our AEM component is now powered with dynamic front-end behavior. Clicking the question will show/hide the answer below:

text property
GIT TAG: 6-vuejs-faq

Adding styles

Of course we want to make all this look pretty as well. We want to write styling for this component and include it in our clientlib.

To do this, we will generate a bundle.css file with Webpack so that we can use Sass instead of plain CSS. Let's add the following dependencies:

npm i --save-dev css-loader sass-loader mini-css-extract-plugin @types/mini-css-
extract-plugin

And configure Webpack:

// add to rules
{
    test: /\.scss$/,
    use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader']
}

// add to plugins
new MiniCssExtractPlugin({
    filename: './[name].css'
})

Then we can include the generated CSS bundle in clientlibs-site:

#src/main/content/jcr_root/apps/aem-vue-demo/clientlibs/clientlib-site/css.txt
#base=dist
bundle.css

And now we can easily add styles to our AEM component:

// faq.scss

.cmp--faq__container {
  padding: 1em;
  border: 1px solid black;
  border-radius: 20px;
}

Or add scoped styles to our VueJs components:

// FaqItem.vue
<style scoped lang="scss">
...
</style>

GIT TAG: 7-styling

Additional tweaks for you to explore

So far we have been running the Webpack build by hand. Luckily it is easy to include it in the Maven build of ui.apps using the Frontend Maven plugin .

Another nice enhancement is to add a "watch" build that instructs Webpack to watch our files for changes and combine this with aemfed to enable hot-refresh of our code directly in a local AEM instance. To optimize performance, you should exclude all files that are compiled by Webpack from your aem-front command.

The build mechanism can also be scaled up to build several clientlibs in order to support multiple sites / brands.

To guarantee a consistent style and quality as the codebase grows, you could add linting with ESLint and start writing unit tests with Jest .

Conclusion

With a few simple additions to the default AEM archetype, we have added a front-end build process that allows us to enrich our AEM components with Typescript, Sass and VueJs while keeping all component- related files nicely grouped together in a single folder.

faq.html

Not only has this given us the power to develop richer and more dynamic components easier, it has also drastically improved our development workflow and made our AEM integration projects a lot more fun.

Enjoyed this Blog?

Consider subscribing to our Blog. We will send you an email when a new one is published.