Template and multilevel nesting in Vue Context menu component

14 Dec 202424 minutes to read

Item template

The itemTemplate property in the ContextMenu component allows you to define custom templates for displaying menu items within the context menu. This feature is particularly useful when you want to customize the appearance or layout of the menu items beyond the default text-based list.

<template>
    <div class="control-section">
        <div class="contextmenu-control">
            <div id='contextmenutarget'></div>
            <ejs-contextmenu cssClass="e-contextMenu-template" id="contextmenu" ref="contextMenu"
                target="#contextmenutarget" :items="data" :itemTemplate="'itemTemplate'" :beforeOpen="addtemplateClass">
                <template v-slot:itemTemplate="{ data }">
                    <div class="menu-wrapper">
                        <span :class="`${data.iconCss} icon-right`"></span>
                        <div class="text-content">
                            <span class="text"></span>
                            <span class="description"></span>
                        </div>
                    </div>
                </template>
            </ejs-contextmenu>
        </div>
    </div>
</template>

<script setup>
import { ref, onMounted } from 'vue';
import { ContextMenuComponent as EjsContextmenu } from "@syncfusion/ej2-vue-navigations";

const data = ref([
    {
        answerType: 'Selection',
        description: "Choose from options",
        iconCss: 'e-icons e-list-unordered'
    },
    {
        answerType: 'Yes / No',
        description: "Select Yes or No",
        iconCss: 'e-icons e-check-box',
    },
    {
        answerType: 'Text',
        description: "Type own answer",
        iconCss: 'e-icons e-caption',
        items: [
            {
                answerType: 'Single line',
                description: "Type answer in a single line",
                iconCss: 'e-icons e-text-form'
            },
            {
                answerType: 'Multiple line',
                description: "Type answer in multiple line",
                iconCss: 'e-icons e-text-wrap',
            }
        ]
    },
    {
        answerType: 'None',
        iconCss: 'e-icons e-mouse-pointer',
        description: "No answer required"
    }
]);

const addtemplateClass = (args) => {
    if (args.element.classList.contains('e-ul')) {
        args.element.classList.add('e-contextMenu-template');
    }
};

onMounted(() => {
    const menuObj = ref('contextMenu').value.ej2Instances;
    if (window.browserDetails.isDevice) {
        document.getElementById("contextmenutarget").textContent =
            "Touch hold to open the Context Menu and select the answer type";
        menuObj.animationSettings.effect = "ZoomIn";
    } else {
        document.getElementById("contextmenutarget").textContent =
            "Right click/Touch hold to open the Context Menu and select the answer type";
        menuObj.animationSettings.effect = "SlideDown";
    }
});
</script>

<style>
/* Import Syncfusion styles */
@import "../node_modules/@syncfusion/ej2-base/styles/material.css";
@import "../node_modules/@syncfusion/ej2-buttons/styles/material.css";
@import "../node_modules/@syncfusion/ej2-inputs/styles/material.css";
@import "../node_modules/@syncfusion/ej2-popups/styles/material.css";
@import "../node_modules/@syncfusion/ej2-navigations/styles/material.css";

/* Local styles */
#target {
    border: 1px dashed;
    height: 150px;
    padding: 10px;
    position: relative;
    text-align: justify;
    color: gray;
    user-select: none;
}

.e-contextMenu-template .menu-wrapper {
    display: flex;
    align-items: center;
    padding: 2px;
}

.e-contextMenu-template .menu-wrapper .text-content {
    display: flex;
    flex-direction: column;
}

.e-contextMenu-template .menu-wrapper .text {
    font-weight: 600;
}

.e-contextMenu-template .menu-wrapper .description {
    font-size: 0.8em;
}

.e-contextMenu-template .menu-wrapper .icon-right {
    padding: 8px 8px 8px 0px;
    font-size: 1.5em;
}

.e-contextMenu-template .e-caret {
    margin-top: -34px !important;
}

.e-contextMenu-template .e-menu-item {
    height: auto !important;
    line-height: unset !important;
}

.contextmenu-control {
    margin: 5% 25%;
    width: auto;
    -webkit-touch-callout: none;
    /* iOS Safari */
    -webkit-user-select: none;
    /* Safari */
}

#contextmenutarget {
    border: 1px dashed;
    height: 250px;
    padding: 10px;
    position: relative;
    text-align: center;
    font-size: 14px;
    line-height: 17px;
    justify-content: center;
    display: flex;
    align-items: center;
}

@media only screen and (max-width: 700px) {
    .contextmenu-control {
        margin: 5% 10%;
        width: auto;
    }

    #contextmenutarget {
        line-height: 19;
        font-size: 12px;
    }
}
</style>
<template>
    <div class="control-section">
        <div class="contextmenu-control">
            <div id="contextmenutarget"></div>
            <ejs-contextmenu cssClass="e-contextMenu-template" id="contextmenu" ref="contextMenu"
                target="#contextmenutarget" :items="data" :itemTemplate="'itemTemplate'" :beforeOpen="addTemplateClass">
                <template v-slot:itemTemplate="{ data }">
                    <div class="menu-wrapper">
                        <span :class="`${data.iconCss} icon-right`"></span>
                        <div class="text-content">
                            <span class="text"></span>
                            <span class="description"></span>
                        </div>
                    </div>
                </template>
            </ejs-contextmenu>
        </div>
        <div id="action-description">
            <p>This sample demonstrates the template support functionalities of the ContextMenu. You can customize the
                menu
                items using templates to enhance flexibility and integrate custom content.</p>
        </div>
    </div>
</template>

<script>
import { ContextMenuComponent } from "@syncfusion/ej2-vue-navigations";

export default {
    components: {
        'ejs-contextmenu': ContextMenuComponent
    },
    data() {
        return {
            data: [
                {
                    answerType: 'Selection',
                    description: "Choose from options",
                    iconCss: 'e-icons e-list-unordered'
                },
                {
                    answerType: 'Yes / No',
                    description: "Select Yes or No",
                    iconCss: 'e-icons e-check-box',
                },
                {
                    answerType: 'Text',
                    description: "Type your own answer",
                    iconCss: 'e-icons e-caption',
                    items: [
                        {
                            answerType: 'Single line',
                            description: "Type answer in a single line",
                            iconCss: 'e-icons e-text-form'
                        },
                        {
                            answerType: 'Multiple line',
                            description: "Type answer in multiple lines",
                            iconCss: 'e-icons e-text-wrap',
                        }
                    ]
                },
                {
                    answerType: 'None',
                    iconCss: 'e-icons e-mouse-pointer',
                    description: "No answer required"
                },
            ]
        };
    },
    methods: {
        addTemplateClass(args) {
            if (args.element.classList.contains('e-ul')) {
                args.element.classList.add('e-contextMenu-template');
            }
        }
    },
    mounted() {
        const menuObj = this.$refs.contextMenu.ej2Instances;
        const targetElement = document.getElementById("contextmenutarget");
        if (window.navigator.userAgent.match(/(iPhone|iPod|iPad|Android)/)) {
            targetElement.textContent = "Touch hold to open the Context Menu and select the answer type";
            menuObj.animationSettings.effect = "ZoomIn";
        } else {
            targetElement.textContent = "Right click/Touch hold to open the Context Menu and select the answer type";
            menuObj.animationSettings.effect = "SlideDown";
        }
    }
};
</script>

<style>
@import "../node_modules/@syncfusion/ej2-base/styles/material.css";
@import "../node_modules/@syncfusion/ej2-buttons/styles/material.css";
@import "../node_modules/@syncfusion/ej2-navigations/styles/material.css";
@import "../node_modules/@syncfusion/ej2-popups/styles/material.css";

.contextmenu-control {
    margin: 5% 25%;
    width: auto;
    -webkit-touch-callout: none;
    -webkit-user-select: none;
}

#contextmenutarget {
    border: 1px dashed;
    height: 250px;
    padding: 10px;
    position: relative;
    text-align: center;
    font-size: 14px;
    line-height: 17px;
    justify-content: center;
    display: flex;
    align-items: center;
}

.e-contextMenu-template .menu-wrapper {
    display: flex;
    align-items: center;
    padding: 2px;
}

.e-contextMenu-template .menu-wrapper .text-content {
    display: flex;
    flex-direction: column;
}

.e-contextMenu-template .menu-wrapper .text {
    font-weight: 600;
}

.e-contextMenu-template .menu-wrapper .description {
    font-size: 0.8em;
}

.e-contextMenu-template .menu-wrapper .icon-right {
    padding: 8px 8px 8px 0px;
    font-size: 1.5em;
}

.e-contextMenu-template .e-caret {
    margin-top: -34px !important;
}

.e-contextMenu-template .e-menu-item {
    height: auto !important;
    line-height: unset !important;
}

@media only screen and (max-width: 700px) {
    .contextmenu-control {
        margin: 5% 10%;
        width: auto;
    }

    #contextmenutarget {
        line-height: 19px;
        font-size: 12px;
    }
}
</style>

Customize the specific menu items

The ContextMenu items can be customized by using the Render event. The item render event triggers while rendering each menu item. The event argument will be used to identify the menu item and customize it based on the requirement. In the following sample, the menu item is rendered with key code for specified action in ContextMenu using the template. Here, the key code is specified for Save as, View page source, and Inspect in the right side corner of the menu items by adding span element in the beforeItemRender event.

<template>
  <div>
    <div id="target">Right click / Touch hold to open the ContextMenu</div>
    <ejs-contextmenu target='#target' :items='menuItems' :beforeItemRender="onBeforeItemRender"></ejs-contextmenu>
  </div>
</template>

<script setup>

import { ContextMenuComponent as EjsContextmenu } from "@syncfusion/ej2-vue-navigations";
import { enableRipple, createElement } from '@syncfusion/ej2-base';

enableRipple(true);

const menuItems = [
  {
    text: 'Save as...'
  },
  {
    text: 'View page source'
  },
  {
    text: 'Inspect'
  }];
const onBeforeItemRender = (args) => {
  var shortCutSpan = createElement('span');
  var text = args.item.text;
  var shortCutText = text === 'Save as...' ? 'Ctrl + S' : (text === 'View page source' ?
    'Ctrl + U' : 'Ctrl + Shift + I');
  shortCutSpan.textContent = shortCutText;
  args.element.appendChild(shortCutSpan);
  shortCutSpan.setAttribute('class', 'shortcut');
};

</script>

<style>
@import "../node_modules/@syncfusion/ej2-base/styles/material.css";
@import "../node_modules/@syncfusion/ej2-buttons/styles/material.css";
@import "../node_modules/@syncfusion/ej2-inputs/styles/material.css";
@import "../node_modules/@syncfusion/ej2-popups/styles/material.css";
@import "../node_modules/@syncfusion/ej2-navigations/styles/material.css";

#target {
  border: 1px dashed;
  height: 150px;
  padding: 10px;
  position: relative;
  text-align: justify;
  color: gray;
  user-select: none;
}

.shortcut {
  float: right;
  font-size: 10px;
  opacity: 0.5;
  padding-left: 50px;
}
</style>
<template>
    <div>
        <div id="target">Right click / Touch hold to open the ContextMenu</div>
        <ejs-contextmenu target='#target' :items='menuItems' :beforeItemRender="onBeforeItemRender"></ejs-contextmenu>
    </div>
</template>

<script>

import { ContextMenuComponent } from "@syncfusion/ej2-vue-navigations";
import { enableRipple, createElement } from '@syncfusion/ej2-base';

enableRipple(true);

export default {
    name: "App",
    components: {
        "ejs-contextmenu": ContextMenuComponent
    },
    data() {
        return {
            menuItems: [
                {
                    text: 'Save as...'
                },
                {
                    text: 'View page source'
                },
                {
                    text: 'Inspect'
                }]
        };
    },
    methods: {
        onBeforeItemRender: function (args) {
            var shortCutSpan = createElement('span');
            var text = args.item.text;
            var shortCutText = text === 'Save as...' ? 'Ctrl + S' : (text === 'View page source' ?
                'Ctrl + U' : 'Ctrl + Shift + I');
            shortCutSpan.textContent = shortCutText;
            args.element.appendChild(shortCutSpan);
            shortCutSpan.setAttribute('class', 'shortcut');
        }
    }
}
</script>

<style>
@import "../node_modules/@syncfusion/ej2-base/styles/material.css";
@import "../node_modules/@syncfusion/ej2-buttons/styles/material.css";
@import "../node_modules/@syncfusion/ej2-inputs/styles/material.css";
@import "../node_modules/@syncfusion/ej2-popups/styles/material.css";
@import "../node_modules/@syncfusion/ej2-navigations/styles/material.css";

#target {
    border: 1px dashed;
    height: 150px;
    padding: 10px;
    position: relative;
    text-align: justify;
    color: gray;
    user-select: none;
}

.shortcut {
    float: right;
    font-size: 10px;
    opacity: 0.5;
    padding-left: 50px;
}
</style>

To create span element, createElement utility function used from ej2-base.

Multilevel nesting

The Multiple level nesting supports in ContextMenu. It can be achieved by mapping the items property inside the parent menuItems. In the below sample, three level nesting of ContextMenu is provided.

<template>
    <div>
        <div id="target">Right click / Touch hold to open the ContextMenu</div>
        <ejs-contextmenu target='#target' :items='menuItems'></ejs-contextmenu>
    </div>
</template>

<script setup>

import { ContextMenuComponent as EjsContextmenu } from "@syncfusion/ej2-vue-navigations";
import { enableRipple } from '@syncfusion/ej2-base';

enableRipple(true);

const menuItems = [
    {
        text: 'Show All Bookmarks',
    },
    {
        text: 'Bookmarks Toolbar',
        items: [
            {
                text: 'Most Visited',
                items: [
                    {
                        text: 'Google',
                    },
                    {
                        text: 'Gmail'
                    }]
            },
            {
                text: 'Recently Added'
            }
        ]
    }];

</script>

<style>
@import "../node_modules/@syncfusion/ej2-base/styles/material.css";
@import "../node_modules/@syncfusion/ej2-buttons/styles/material.css";
@import "../node_modules/@syncfusion/ej2-inputs/styles/material.css";
@import "../node_modules/@syncfusion/ej2-popups/styles/material.css";
@import "../node_modules/@syncfusion/ej2-navigations/styles/material.css";

#target {
    border: 1px dashed;
    height: 150px;
    padding: 10px;
    position: relative;
    text-align: justify;
    color: gray;
    user-select: none;
}
</style>
<template>
    <div>
        <div id="target">Right click / Touch hold to open the ContextMenu</div>
        <ejs-contextmenu target='#target' :items='menuItems'></ejs-contextmenu>
    </div>
</template>

<script>

import { ContextMenuComponent } from "@syncfusion/ej2-vue-navigations";
import { enableRipple } from '@syncfusion/ej2-base';

enableRipple(true);

export default {
    name: "App",
    components: {
        "ejs-contextmenu": ContextMenuComponent
    },
    data() {
        return {
            menuItems: [
                {
                    text: 'Show All Bookmarks',
                },
                {
                    text: 'Bookmarks Toolbar',
                    items: [
                        {
                            text: 'Most Visited',
                            items: [
                                {
                                    text: 'Google',
                                },
                                {
                                    text: 'Gmail'
                                }]
                        },
                        {
                            text: 'Recently Added'
                        }
                    ]
                }]
        };
    }
}
</script>

<style>
@import "../node_modules/@syncfusion/ej2-base/styles/material.css";
@import "../node_modules/@syncfusion/ej2-buttons/styles/material.css";
@import "../node_modules/@syncfusion/ej2-inputs/styles/material.css";
@import "../node_modules/@syncfusion/ej2-popups/styles/material.css";
@import "../node_modules/@syncfusion/ej2-navigations/styles/material.css";

#target {
    border: 1px dashed;
    height: 150px;
    padding: 10px;
    position: relative;
    text-align: justify;
    color: gray;
    user-select: none;
}
</style>