Skip to content

Custom module editor

For cases when data cannot be edited inline - for example: a (match) score widget, for which the editor must select the match id, the list of matches is returned from an API, a custom module editor can be used.

To create a custom module editor, create a JS file in admin/view/module_editor, use the file's name as wf-module-editor attribute of that composite module. This should return a Backbone view that will be used to edit the module's data in a modal dialog.

Example:

html
<div wf-module="wfed/composite/module"
     wf-role="score"
     wf-module-editor="score_editor"
     wf-new
     wf-allow=""
>
    <div wf-module="wfed/dynamic/generic"
         wf-role="score"
         wf-url="{{ path('app_widget_score', {sportId: "__sportId__"}) }}"
         wf-bind="urlParams:sportId|module_sportId"
         wf-toolbar-position="none"
         wf-new
    ></div>
</div>

Notes:

  • wf-module-editor attribute on the composite module points to the editor's filename
  • module_XXX is used to bind the url param to the module's data

Create the file admin/view/module_editor/score_editor.js:

javascript
define(["i18n", "jquery", "backbone", "backbone.epoxy"], function (
    i18n,
    $,
    Backbone,
    Epoxy
) {
    const Base = Backbone.Epoxy.View;

    return Base.extend({
        events: {
            "click .btn-save": "onSaveClick",
        },
        initialize(options) {
            Base.prototype.initialize.apply(this, arguments);

            this.module = options.module;
            this.model = new Backbone.Model(this.module.model.attributes);

            this.viewModel = new Backbone.Model({
                errorMessage: null,
                loading: false,
            });

            this.viewModel.on("change:loading", (_model, loading) => {
                loading && this.viewModel.set("errorMessage", null);
            });

            this.fetchSportChoices();
        },
        getTitle() {
            return "score";
        },
        getIcon() {
            return "score";
        },
        getDialogOptions() {
            return { minHeight: 350 };
        },
        render() {
            this.el.innerHTML = `
                <div class="score-form">
                    <div class="alert alert-danger" style="overflow: auto; max-height: 100px"
                         data-bind="text:errorMessage,toggle:errorMessage"></div>
                    <div class="form-row controls-group">
                        <label for="sportId" class="control-label">
                            ${i18n.get("score-form.label.sport")}
                        </label>
                        <input type="hidden" id="sportId"
                               data-placeholder="${i18n.get(
                                   "score.placeholder.sport"
                               )}"
                               data-bind="value:sportId" />
                    </div>
                    <div class="form-row" style="text-align: center; color: #999; font-style: italic;">
                        <label data-bind="toggle:loading">
                            ${i18n.get("score-form.label.loading")}
                        </label>
                    </div>
                    <div class="form-row" style="text-align: center;"
                         data-bind="toggle:all(sportId)">
                        <button class="btn btn-primary btn-save" style="margin-bottom: 0;">
                            ${i18n.get("score.button.save")}
                        </button>
                    </div>
                </div>
            `;

            this.applyBindings();

            return this;
        },
        fetchSportChoices() {
            return this.getJSON("/widget/score/sports").then((choices) => {
                // implement logic to update the form with the choices received
            });
        },
        getJSON(url) {
            this.viewModel.set("loading", true);
            const stopLoading = () => this.viewModel.set("loading", false);

            return $.getJSON(url)
                .fail((response) => {
                    this.viewModel.set(
                        "errorMessage",
                        _get(
                            response,
                            "responseJSON.detail",
                            response.responseText
                        )
                    );
                })
                .always(stopLoading);
        },
        onSaveClick() {
            this.module.model.set(this.model.toJSON());

            this.trigger("done", this.model);
        },
    });
});

The custom module editor receives a module option with the instance of the module that's being edited. It creates a copy of those module's attributes (options.module.model.attributes) to work on. When the form's editing is finished, it saves the data back to the module's mode (in onSaveClick: this.module.model.set(this.model.toJSON())). Triggering done event will close the editor.

The editor view may implement the following (all optional) methods, they are used by the CMS to customize the editor's popup:

  • getTitle the title of the dialog, by default it uses the module's role will be used
  • getIcon if you want to display an icon to the left of the modal's title (you must implement the .icon-__ICON__ class in the CSS if you're not using one that's already available in the CMS)
  • getDialogOptions to overwrite the default options for the dialog that Xalok uses for the editor.