Skip to content

Vuex

Intro

The XalokNext editor/SSR library uses the Vuex library to manage its state in a centralized place, a library that follows the flux workflow for working with data.

Vuex modules

Vuex allows splitting the state into different modules, each with its own role. XalokNext library registers the following Vuex modules:

  • page: a Vuex module where the state of the page (article/board) being edited is kept - categories, authors, tags, etc.
  • templateConfig: a Vuex module where the moduleConfig dumped by wf-directives is kept
  • module: a module where XalokNext's modules data is being kept
  • contentModels: a Vuex module where details of content models used by the page are being kept
  • moduleSort: a Vuex module where the code handling the sorting of XalokNext's modules keeps its state (what XalokNext module is being hovered/dragged in the sorting interface)
  • misc: a Vuex module keeping whatever doesn't fit in the other modules. For now, only the currently focused text module's wfRolePath.

Store extenders

XalokNext allows registering callbacks to be invoked when a specific Vuex action is being dispatched:

  • preAction(ACTION_TYPE, callback): execute callback before XalokNext handles this action
  • postAction(ACTION_TYPE, callback): execute callback after XalokNext handles this action

The XalokNext skeleton already comes with a sample store extender in src/App/Bundle/CmsAdminBundle/Resources/public/javascripts/wfvue/store/sample.js, this is being registered in src/App/Bundle/CmsAdminBundle/Resources/public/javascripts/wfvue/store/index.js

Store constants

wfvue exports some constants that you are encouraged to use in your code for referencing action types, mutation names or getters.

Example: Using store constants:

javascript
define(["wfed/wfvue/store/util/constants"], function({
    CONTENT_MODEL_SELECT
}) {
    return (store) => 
        store.postAction(CONTENT_MODEL_SELECT,
            ({ wfRolePath, contentModel }) => {
                // do something after a content model has been selected in the `wfRolePath` module
            }
        );
});

Store getters

Among the exported constants there are constants for getters, functions that can be used to get data from the store.

Example: Using the GETTER_MODULE_CONFIG and GETTER_MODULE_DATA getters:

javascript
define(["wfed/wfvue/store/util/constants"], function ({
    GETTER_MODULE_CONFIG,
    GETTER_MODULE_DATA,
    CONTENT_MODEL_SELECT,
}) {
    return (store) => {
        store.postAction(
            CONTENT_MODEL_SELECT,
            ({wfRolePath, contentModel}) => {
                const moduleConfig = store.getters[GETTER_MODULE_CONFIG](
                    wfRolePath
                );

                const moduleData = store.getters[GETTER_MODULE_DATA](
                    wfRolePath
                );
            }
        );
    };
});

Note: Check the Modules section for some utils that XalokNext recommends for working with composites' config and data objects.

Dispatching actions

It's likely that the store extender needs to do some extra changes to the state. To do so, one can dispatch additional actions from the store extender

Example: Dispatching an action from a store exteder:

javascript
define(["wfed/wfvue/store/util/constants"], function({
  CONTENT_MODEL_SELECT,
  MODULE_PARTIAL_UPDATE
}) {
    return (store) =>
        store.postAction(CONTENT_MODEL_SELECT,
            async ({ wfRolePath, contentModel }) => {
                await store.dispatch(MODULE_PARTIAL_UPDATE, {
                    wfRolePath,
                    data: {
                        // put the processed data of the module here
                    }
                })
                
                // code after the MODULE_PARTIAL_UPDATE action has been dispatched
            }
        );
});

Note: The dispatch is asynchronous, the above code first waits for it to finish before moving on processing additional code.

Available actions

Warning

This is an incomplete list of the available actions, feel free to add new examples.

MODULE_ADD

Adds a module.

Payload:

  • wfRolePath (array) - the role path of which module to add after. Think of it as the role path of the module on which the editor presses the "+" button.
  • role (string) - the role of the module to be added.
  • data (object, optional) - the data of the module to be added.

Example: adding a content_image module after the second paragraph

javascript
store.dispatch(MODULE_ADD, {
    wfRolePath: ['paragraph--1'],
    role: 'content_image'
});

Example: adding a paragraph module after the second paragraph and setting its contents:

javascript
store.dispatch(MODULE_ADD, {
    wfRolePath: ['paragraph--1'],
    role: 'paragraph',
    data: {
        content: `Third paragraph`
    }
});

Example: the action returns the role path of the added module. This can be useful, for example, if you need to add multiple modules, one after the other.

javascript
const thirdParagraphRolePath = await store.dispatch(MODULE_ADD, {
    wfRolePath: ['paragraph--1'],
    role: 'paragraph',
    data: {
        content: `Third paragraph`
    }
});
await store.dispatch(MODULE_ADD, {
    wfRolePath: thirdParagraphRolePath,
    role: 'paragraph',
    data: {
        content: `Fourth paragraph`
    }
});

Other actions

You can find out the payload of an action by manually interacting with the module in the editor and checking the Chrome DevTools' console. Check the first line that is prefixed with action that happens after you perform the desired behaviour.

For example, after selecting a content model in the main_image module:

dispatch content model select in console

Or, after updating the contents of the image_credits text submodule of the main_image module:

dispatch module content update in console

As a general rule of thumb, wfed/wfvue/store/util/constants exports constants with the same name as the action's type, minus the WF/ prefix.

For example, in the case of selecting the content model above, the action's type is:

WF/CONTENT_MODEL_SELECT

The name of the exported constant is:

CONTENT_MODEL_SELECT

Delaying execution

A preAction store extender has the possibility of delaying the execution of wfvue handling of the action that it extends or cancelling it altogether by registering the wfWait property to the passed action.

Example: Using the wfWait property:

javascript
define(["wfed/wfvue/store/util/constants"], function({
    CONTENT_MODEL_SELECT,
}) {
    return (store) =>
        store.preAction(CONTENT_MODEL_SELECT,
            async (action) => {
                action.wfWait = new Promise((resolve, reject) => {
                    // call `resolve` when you're ready for the base action to be handled, or
                    // call `reject` if you want to prevent base from handling the action
                });
            }
        );
});

Canceling actions

Canceling an action (must be done in a preAction extender) leads to XalokNext not applying any mutations for that action.

For canceling actions, XalokNext comes with wfed/wfvue/store/util/action_canceler, an util that can be used to cancel the action.

Example: Using the action_canceler util:

javascript
define(["wfed/wfvue/store/util/constants", 
    "wfed/wfvue/store/util/wf_role_path",
    "wfed/wfvue/store/util/action_canceler"], function({
        CONTENT_MODEL_SELECT,
    }, { getPrimaryRole },
    actionCanceler) {
    
    return (store) =>
        store.preAction(CONTENT_MODEL_SELECT,
            async (action) => {
                if (getPrimaryRole(action.wfRolePath[0]) === 'special_module') {
                    actionCanceler(
                        action,
                        `The special_module shouldn't accept any content models, it handles them in a different way...`
                    )
                }
            }
        );
});

Initializing modules data

Sometimes it can be useful to provide some defaults for modules, for example when initializing the settings of a module, some options should be already checked.

Example: The following code assumes a module:

html
<ul wf-role="related">
    <li wf-role="item"
        wf-allow="+-"
    >
        <wf-setting name="options" type="checkbox">
            <title>Related options</title>
            <option value="withImage">With image</option>
        </wf-setting>
</ul>

It enables the withImage checkbox by default for the related/item modules:

javascript
define(["wfvue", "wfed/wfvue/store/util/wf_role_path"], function (wfvue, { joinPrimaryPath }) {
    if (!IS_CLIENT) {
        // on the server it renders the saved modules, it doesn't initialize any
        return;
    }

    const original = wfvue.bridgeCommon.initializeModuleData;
    wfvue.bridgeCommon.initializeModuleData = function (wfRolePath, data) {
        data = original(wfRolePath, data);

        if (joinPrimaryPath(wfRolePath) !== 'related/item') {
            return data;
        }

        data.__settings = data.__settings || {};
        data.__settings.options = data.__settings.options || "";

        const options = data.__settings.options.split(",");

        if (!options.includes("withImage")) {
            options.push("withImage");
        }

        data.__settings.options = options.join(",");

        return data;
    };
});

This script should be included in admin/wfvue/store/index.js. Including it in app_setup will call it too late, for new pages, the store will already be initialized before app_setup registers.

javascript
// src/App/Bundle/CmsAdminBundle/Resources/public/javascripts/wfvue/store/index.js
define(["_wfed/wfvue/store"], function (baseWfvueStore) {
    return (store) => {
        baseWfvueStore(store);

        IS_CLIENT && require("admin/wfvue/store/sample")(store);
        IS_CLIENT && require("admin/wfvue/store/initialize_module_data");
    };
});

Note: Note the difference in admin/wfvue/store/index.js between requiring this and requiring a store extender. The code for the sample store extender returns a function that expects a store argument, whereas this overwrites a function in wfvue.bridgeCommon and does not return a function. Therefore: require(__STORE_EXTENDER__)(store) vs require(__STORE_INITIALIZER__).

Note: The script above uses the wf role path util (joinPrimaryPath) to easily check the primary role path of the module (wfRolePath holds the actual role path of the module, it could be ['related', 'item--2']).

The editor options "trick"

Vuex doesn't allow extending getters in an easy manner. Thus, for changing the context that is being passed to the editor of a module, a "trick" has been implemented: the GET_EDITOR_OPTIONS action is being dispatched - write a store extender for it and add additional details in the action object.

Example: Add data to the editor's context:

javascript
define([
    "wfed/wfvue/store/util/constants",
    "wfed/wfvue/store/util/content_model_collection",
], function (
    {
        GET_EDITOR_OPTIONS,
    },
    { getFullIdsRoleMap }
) {
    return (store) =>
        store.postAction(GET_EDITOR_OPTIONS, (action) => {
            action.customEditorContext = {
                // some extra data to be passed to the editor
            }
        });
});