Unlayer Examples
Documentation

Menu Custom Tool

In this example, we will create a custom tool for Menu. This tool will help us create navigation menus in emails and web pages.

This example is built using vanilla JavaScript and lodash. Lodash is available in Unlayer's environment by default. Check our documentation for more details.


Register Tool

We will create a menuTool.js file for the custom tool, which will be passed to unlayer.init.

Our tool needs some templates for different HTML views so we will first define those using lodash template function.

var emptyTemplate = _.template(`
<div style="padding: 15px; border: 2px dashed #CCC; background-color: #EEE; color: #999; text-align: center;">
  MENU
</div>
`);

var menuTemplate = _.template(`
<div class="menu">
  <% _.forEach(items, function(item) { %>
    <a href="<%= item.url %>" target="_blank"><%= item.text %></a>
  <% }); %>
</div>
`);

Now let's register our tool. Notice that this tool is using a custom property editor called menu_editor which we will build below.

unlayer.registerTool({
  name: 'menu_tool',
  label: 'My Menu',
  icon: 'fa-bars',
  supportedDisplayModes: ['web', 'email'],
  options: {
    default: {
      title: null,
    },
    menu: {
      title: 'Menu Items',
      position: 1,
      options: {
        menu: {
          label: 'Menu Items',
          defaultValue: {
            items: [],
          },
          widget: 'menu_editor', // Custom Property Editor
        },
      },
    },
  },
  values: {},
  renderer: {
    Viewer: unlayer.createViewer({
      render(values) {
        // If the user has added no items yet, show empty placeholder template
        if (values.menu.items.length == 0) return emptyTemplate();

        return menuTemplate({ items: values.menu.items });
      },
    }),
    exporters: {
      web: function (values) {
        return menuTemplate({ items: values.menu.items });
      },
      email: function (values) {
        return menuTemplate({ items: values.menu.items });
      },
    },
    head: {
      css: function (values) {},
      js: function (values) {},
    },
  },
});

Then, we'll pass the menuTool.js URL to the editor in init option customJS.

  • The URL must be absolute
  • You can load multiple URLs by passing more in the array
  • If you don't have the option to pass a URL, you can directly pass your JavaScript code as a string
unlayer.init({
  id: 'editor-container',
  displayMode: 'email',
  customJS: [
    'https://examples.unlayer.com/examples/menu-custom-tool/menuTool.js',
  ],
});

Now we need to register our menu editor which the user will use to add, update or delete menu items.

Our editor needs a HTML template to render the menu items and input fields. We will define add those using a lodash template.

var editorTemplate = _.template(`
<% _.forEach(items, function(item, index) { %>
  <div class="menu-item" style="padding: 10px; margin: 10px 0px; background-color: #FFF; border: 1px solid #CCC;">
    <div class="blockbuilder-widget-label">
      <label>Text</label>
    </div>
    <input class="page-text form-control" data-index="<%= index %>" type="text" value="<%= item.text %>" />

    <div class="blockbuilder-widget-label pt-2">
      <label>URL</label>
    </div>
    <input class="page-url form-control" data-index="<%= index %>" type="text" value="<%= item.url %>" />

    <a class="delete-btn" data-index="<%= index %>" style="display: inline-block; cursor: pointer; color: red; margin-top: 10px; font-size: 12px;">
      Delete Item
    </a>
  </div>
<% }); %>

<div>
  <a class="add-btn" style="display: block; text-align: center; padding: 10px; background-color: #EEE; border: 1px solid #CCC; color: #999; cursor: pointer;">
    Add New Item
  </a>
</div>
`);

Now we'll register our menu_editor property editor that will render the template above and attach events to the buttons and input fields.

In the render function, we will render the HTML template. And in the mount function, we will attach events to input fields and delete button. Learn More

unlayer.registerPropertyEditor({
  name: 'menu_editor',
  layout: 'bottom',
  Widget: unlayer.createWidget({
    render(value, updateValue, data) {
      return editorTemplate({ items: value.items });
    },
    mount(node, value, updateValue, data) {
      var addButton = node.querySelector('.add-btn');
      addButton.onclick = function () {
        var newItems = value.items.slice(0);
        newItems.push({
          text: 'Page',
          url: 'https://mysite.com',
        });
        updateValue({ items: newItems });
      };

      // Text Change
      // Look for inputs with class page-text and attach onchange event
      node.querySelectorAll('.page-text').forEach(function (item) {
        item.onchange = function (e) {
          // Get index of item being updated
          var itemIndex = e.target.dataset.index;

          // Get the item and update its value
          var updatedItems = value.items.map(function (item, i) {
            if (i == itemIndex) {
              return {
                text: e.target.value,
                url: item.url,
              };
            }

            return {
              text: item.text,
              url: item.url,
            };
          });

          updateValue({ items: updatedItems });
        };
      });

      // URL Change
      // Look for inputs with class page-url and attach onchange event
      node.querySelectorAll('.page-url').forEach(function (item) {
        item.onchange = function (e) {
          // Get index of item being updated
          var itemIndex = e.target.dataset.index;

          // Get the item and update its value
          var updatedItems = value.items.map(function (item, i) {
            if (i == itemIndex) {
              return {
                text: item.text,
                url: e.target.value,
              };
            }

            return {
              text: item.text,
              url: item.url,
            };
          });

          updateValue({ items: updatedItems });
        };
      });

      // Delete
      node.querySelectorAll('.delete-btn').forEach(function (item) {
        item.onclick = function (e) {
          // Get index of item being deleted
          var itemIndex = e.target.dataset.index;
          var updatedItems = value.items
            .map(function (item, i) {
              if (i == itemIndex) {
                return false;
              }

              return {
                text: item.text,
                url: item.url,
              };
            })
            .filter(function (item) {
              return item;
            });

          updateValue({ items: updatedItems });
        };
      });
    },
  }),
});

More Styling Options

Once your menu tool is working, you will need to add more styling options to update fonts, colors, padding etc. You can use our built-in property editors to easily add more options for the user to customize the menu.


Live Preview

Here's a live demo preview of our Menu custom tool. Drag and drop the custom tool My Menu and add menu items.