Provides powerful menu editor to replace category based menus in Magento 2.
List of menus is located in Admin Panel under Content > Elements > Menus
.
Following example shows how to replace default Magento 2 menu, by the user-defined menu with identifier main
.
<referenceBlock name="catalog.topnav" remove="true"/>
<referenceBlock name="store.menu">
<block name="main.menu" class="Snowdog\Menu\Block\Menu">
<arguments>
<argument name="menu" xsi:type="string">main</argument>
</arguments>
</block>
</referenceBlock>
You have to add new folder with menu ID and add same structure like in default folder. For example, to overwrite templates of menu with ID menu_main
the folders structure should looks like this:
Snowdog_Menu
└─ templates
└─ menu_main
│- menu
│ │- node_type
│ │ │- category.phtml
│ │ └─ ...
│ └─ sub_menu.phtml
└─ menu.phtml
To add new type node you have to add:
- new backend block that also implements
\Snowdog\Menu\Api\NodeTypeInterface
and is defined in di.xml - create new vue component for new type node and define it in di.xml
Backend block will be directly injected into menu editor.
Menu UI in admin panel is build with Vue.js.
Every node type has its own vue component located inside view/adminhtml/web/vue/menu-type
directory.
(See view/adminhtml/web/vue/menu-type/category.vue
or view/adminhtml/web/vue/menu-type/cms-block.vue
examples for a reference)
UI initialization starts in view/adminhtml/templates/menu/nodes.phtml
where we initialize menu.js
and we pass list of paths of Vue components that are assigned to each node type using "vueComponents"
property (see two fragments of code from nodes.phtml
below).
$vueComponents = $block->getVueComponents();
<script type="text/x-magento-init">
{
"*": {
"menuNodes": {
"vueComponents": <?= json_encode($vueComponents) ?>,
// ...
}
}
}
</script>
To show new node in UI we need to add 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>
Where we need to define
component_name
- example:cms_block
component-file-name
- example:cms-block
Then in view/adminhtml/web/vue/menu-type/
we add component-file-name.vue
ex. cms-block.vue
In new vue file we register our component (component_name
ex. cms_block
) and we add our logic we need.
<template>
...
</template>
<script>
define(['Vue'], function(Vue) {
Vue.component('component_name', {
// example: Vue.component('cms_block', {
...
}
})
</script>
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
- example: cms_block
(the same as component_name
)
Foo\Bar\Block\NodeType\MyNode
- 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>
When saving menu changes we send form post request 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
watch the jsonList
element, and when data changes, trigger custome event and update data in serialized_nodes
field.
In mounted
hook, the method wait for the ui Component hidden field and update it's value by passing 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
.
This is not an ideal way of mutating data, and we plan to refactor it.
For now look at menu-type.vue
. You can find:
<component
:is="item['type']"
:item="item"
:config="config"
/>
This loads dynamically a component of a chosen type of node. For example for a node type: cms_block
-> cms-block.vue
Cms block node type component uses autocomplete.vue
input type component with prop item :item="item"
. Once user makes some changes, the data is propagated up to the root App.vue
component, stringified and saved in a hidden input.
This feature allows you to add custom templates to each menu node type and node submenu. And it allows to select the custom templates in menu admin edit page.
The custom templates override the default ones that are provided by the module.
- Create a directory inside your theme files that will contain the custom templates with the following structure:
Snowdog_Menu
└─ templates
└─ {menu_identifier}
└─ menu
└─ custom
└─ {custom_templates_directories}
{menu_identifier}
is the identifier that you enter when you create a menu on menu admin page.{custom_templates_directories}
is a list of container directories for the custom templates.- The name of the custom templates container directory can be either a node type (Check Available Node Types) or
sub_menu
. - Once the custom templates container directories are ready, you have to add the custom templates
PHTML
files to them. (Template file name must not be a node type.) - After that, you can proceed to your menu admin edit page to select the custom templates that you want to use for your nodes. (Check Configuring Nodes Custom Templates.)
After adding your custom templates, you can select the templates that you want to use for your menu nodes in menu admin edit page.
In menu admin edit page, the Node template
field will contain a list of available node type custom templates.
And the Submenu template
field will contain a list of available submenu templates. (Submenu template applies to the child nodes of a node.)
category
product
cms_page
cms_block
custom_url
category_child
wrapper
/rest/V1/menus
: retrieves available menus/rest/V1/nodes
: retrieves nodes by menuId
snowdogMenus
: Returns a list of active menus filtered by the array argumentidentifiers
.
Usage:
query SnowdogMenusExample {
snowdogMenus(identifiers: ["foo", "bar"]) {
items {
menu_id
identifier
title
css_class
creation_time
update_time
}
}
}
snowdogMenuNodes
: Returns a list of active menu nodes filtered by the menuidentifier
argument.
Usage:
query SnowdogMenuNodesExample {
snowdogMenuNodes(identifier: "foobar") {
items {
node_id
menu_id
type
content
classes
parent_id # Parent node ID
position
level
title
target # (0 for "_self", 1 for "_blank")
image
image_alt_text
creation_time
update_time
additional_data
}
}
}
- Queries HTTP method must be
GET
in order to cache their results.
We are not providing any CSS or JS, only basic HTML, which means this module is not out of box supported by any theme, you always need to write some custom code to get expected results or pick some ready to use theme / extension, built on top of this module.
To contribute/resolving issues, fork this repo, add changes on a new branch, go to Pull Request tab and open a "New pull request" from your forked branch, to our develop branch. All PR will be labeled as "safe to test" to run automatic code testing and reviewed by our staff.