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 themoduleConfig
dumped by wf-directives is keptmodule
: a module where XalokNext's modules data is being keptcontentModels
: a Vuex module where details of content models used by the page are being keptmoduleSort
: 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'swfRolePath
.
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 actionpostAction(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:
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:
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:
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
store.dispatch(MODULE_ADD, {
wfRolePath: ['paragraph--1'],
role: 'content_image'
});
Example: adding a paragraph
module after the second paragraph
and setting its contents:
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.
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:
Or, after updating the contents of the image_credits
text submodule of the main_image
module:
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:
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:
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:
<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:
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.
// 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:
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
}
});
});