v7.3+ Clientside validation
Intro
Starting with v7.3.0-alpha.23 XalokNext supports adding clientside validation: showing messages (e.g. warnings) to the editors after they interact with the CMS, before sending this page to the server for saving.
Adding a validation
store.dispatch(VALIDATION_ADD, {
id: WARNING_ID,
type: "warning",
message: i18n.get("wfed.page_edit_error.main_image_has_odd_id")
});
id
(optional - defaults to a random value): an ID for this validation, useful in case you want to remove this validation later on.type
(optional - defaults to'error'
): the type used to display this validation. Possible values:error
,warning
,success
andinfo
message
(required): the message to display to the user.
Removing a validation
store.dispatch(VALIDATION_REMOVE, {
id: WARNING_ID,
});
Payload:
id
(required): the ID of the validation to remove
Hard errors
Note that by default all the validations are "soft" - that is, they are displayed to the user, but the user is allowed to save the article/board.
If one wants to block the user from saving/publishing the article/board, one should cancel the VALIDATE
action:
Example: Cancel the validation on some condition:
define([
"i18n",
"wfed/wfvue/store/util/wf_role_path",
"wfed/wfvue/store/util/constants",
"wfed/wfvue/store/util/modules_data_walker",
"wfed/wfvue/store/util/action_canceler",
], function (
i18n,
{ joinPath },
{ VALIDATE, VALIDATION_ADD },
moduleDataWalker,
actionCanceler
) {
return (store) => {
store.preAction(VALIDATE, async (action) => {
moduleDataWalker(
store.state.module,
async (moduleData, wfRolePath) => {
if (!moduleData?.content?.includes("FORBIDDEN_WORD")) {
return;
}
actionCanceler(
action,
"The module exceeds the maximum length"
);
await store.dispatch(VALIDATION_ADD, {
id: `forbidden_word/${joinPath(wfRolePath)}`,
message: i18n.get("wfed.page_edit_error.forbidden_words_are_not_allowed"),
extra: {
wfRolePath,
},
});
}
);
});
};
});
Example
The following example shows a (soft) warning if the editor selects an image with an odd ID (1, 3, 5, etc...) in the main_image
module.
define([
"i18n",
"wfed/wfvue/store/util/wf_role_path",
"wfed/wfvue/store/util/constants",
"wfed/wfvue/store/util/modules_data_walker",
"wfed/wfvue/store/util/module_is_default_text",
"wfed/wfvue/store/util/action_canceler",
], function (
i18n,
{ joinPath },
{
VALIDATION_ADD,
VALIDATION_REMOVE,
CONTENT_MODEL_SELECT,
INITIALIZE,
GETTER_MODULE_FULL_DATA,
},
moduleDataWalker
) {
return (store) => {
const checkRolePath = (wfRolePath) =>
joinPath(wfRolePath) === "main_image";
const checkContentModelType = (contentModel) =>
contentModel?.contentType === "image";
// You will most likely want to implement some useful condition in the following function
const checkContentModelDetails = (contentModel) =>
Boolean(contentModel.id % 2);
const WARNING_ID = "odd-id/main_image";
const addViolation = () => {
store.dispatch(VALIDATION_ADD, {
id: WARNING_ID,
type: "info",
message:
i18n.get("wfed.page_edit_error.main_image_has_odd_id") +
new Date(),
});
};
const removeViolation = () => {
store.dispatch(VALIDATION_REMOVE, {
id: WARNING_ID,
});
};
store.postAction(
CONTENT_MODEL_SELECT,
async ({ contentModel, wfRolePath }) => {
if (!checkRolePath(wfRolePath)) {
return;
}
if (!checkContentModelType(contentModel)) {
return;
}
if (checkContentModelDetails(contentModel)) {
addViolation();
} else {
removeViolation();
}
}
);
store.postAction(INITIALIZE, async ({ module }) => {
moduleDataWalker(module, (moduleData, wfRolePath) => {
if (!checkRolePath(wfRolePath)) {
return;
}
const {
cm: { image },
} = store.getters[GETTER_MODULE_FULL_DATA](wfRolePath);
if (!checkContentModelType(image)) {
return;
}
if (!checkContentModelDetails(image)) {
return;
}
addViolation();
});
});
};
});
Note: See how the joinPath
role path util is used to more easily check if we're interested in the module or not (joinPath(wfRolePath) === "main_image"
rather than wfRolePath.length === 1 && wfRolePath[0] === "main_image"
). The same util can be used for nested modules, e.g. joinPath(wfRolePath) === "gallery/slide"
.
Since the callback must be executed for many modules, it's useful to add a condition such as this early on in the execution of the callback.
Note: See how the code needs to act both post CONTENT_MODEL_SELECT
and post INITIALIZE
- the second is there to show the warning after the article is saved.
Note: See how the id
property for the violation is the same in both VIOLATION_ADD
and VIOLATION_REMOVE
actions.
Note: See how the violation is removed in the else
clause for CONTENT_MODEL_SELECT
- otherwise it'll remain on the screen even if the editor selects a "good" image. This is not needed in the INITIALIZE
case, as that code is executed only once, with the initial data of the article - the violation either is or is not there.
Note: See how the GETTER_MODULE_FULL_DATA
vuex getter is used to extract the full module data - moduleData.__contentModels
contains only the content models' descriptions ({type: "image", id: 1}
), this getter returns in the cm.image
the full details of the image that you'll likely want to use for validation