Skip to content

Adding new types of nodes

Ola Frankowska edited this page Aug 8, 2023 · 3 revisions

General steps

  1. Add a new backend block. It should implement \Snowdog\Menu\Api\NodeTypeInterface. Define it in di.xml. The block will be directly injected into the menu editor.
  2. Create a new vue component for admin and define it in di.xml.
  3. Create a node template in view/frontend/templates/menu/node_type.

Adding new node type to admin panel

The menu tree configurator in admin panel is built with Vue.js.

Every node type has its own vue component located in view/adminhtml/web/vue/menu-type. You can check existing components for reference.

UI initialization starts in view/adminhtml/templates/menu/nodes.phtml. We initialize menu.js and pass a list of paths of Vue components that are assigned to each node type using "vueComponents" property:

$vueComponents = $block->getVueComponents();
<script type="text/x-magento-init">
    {
        "*": {
            "menuNodes": {
                "vueComponents": <?= json_encode($vueComponents) ?>,
                // ...
            }
        }
    }
</script>

To show a new node in UI we need to add a new vue component via di.xml:

<?xml version="1.0" encoding="UTF-8" ?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Snowdog\Menu\Model\VueProvider">
        <arguments>
            <argument name="components" xsi:type="array">
                <item name="component_name" xsi:type="string">component-file-name</item>
            </argument>
        </arguments>
    </type>
</config>

We need to define here:

  • component_name: for example cms_block
  • component-file-name: for example cms-block

Then in view/adminhtml/web/vue/menu-type/ we need to add {component-file-name}.vue, for example cms-block.vue.

In the new vue file, we need to register our component (component_name ex. cms_block):

<template>
    ...
</template>
<script>
    define(['Vue'], function(Vue) {
        Vue.component('component_name', {
            props: {
                // ...
            },
            template: template,
            // ...
        })
    })
</script>

Replace component_name with a proper name, for example: cms_block.

Newly created block with additional method should be added via di.xml defining block instance and node type code (code will be stored in database):

<?xml version="1.0" encoding="UTF-8" ?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Snowdog\Menu\Model\NodeTypeProvider">
        <arguments>
            <argument name="providers" xsi:type="array">
                <item name="my_node_type" xsi:type="object">Foo\Bar\Block\NodeType\MyNode</item>
            </argument>
        </arguments>
    </type>
</config>
  • my_node_type: for example: cms_block (the same as component_name)
  • Foo\Bar\Block\NodeType\MyNode: for example: Snowdog\Menu\Block\NodeType\CmsBlock

In our cms_block example it would be:

<item name="cms_block" xsi:type="object">Snowdog\Menu\Block\NodeType\CmsBlock</item>

How data is saved from vue components

When saving menu changes we send a form postrequest that contains several fields like: form_key, id, title, identifier, css_class, stores[], serialized_nodes.

serialized_nodes data is stored in a hidden field created with ui_Component in snowmenu_menu_form.xml. It has to be updated to save menu data. A watcher in app.vue watches the jsonList element. When data changes, it triggers a custom event and updates data in serialized_nodes field.

In mounted hook, we wait for the UI Component hidden field to be loaded and then update its value by passing the JSON list.

app.vue:

// watcher
watch: {
    jsonList: function (newValue) {
        this.updateSerializedNodes(newValue)
    }
},
// update method:
updateSerializedNodes(value) {
    const updateEvent = new Event('change');
    const serializedNodeInput = document.querySelector('[name="serialized_nodes"]');
    // update serialized_nodes input value
    serializedNodeInput.value = value;
    // trigger change event to set value
    serializedNodeInput.dispatchEvent(updateEvent);
}

The list and item objects are passed from app.vue to child components. As they are objects, they are passed by reference, and editing them in child components updates the value of serialized_nodes in app.vue.