Search code examples
tagswagtailwagtail-streamfield

Wagtail - how to get tags to work with `telepath` (tags in streamfield)?


I can use tags in regular page fields without any issue. When using tags within blocks (within a streamfield), the UI works and the tags are saved BUT the current page tags do not show up when loading the page in the admin. That's because the current value is not in the template anymore, it's in a JSON loaded via telepath.

I can confirm that the tags are saved and present in the data passed to initBlockWidget in the page source but these are ignored. Also, if I used a regular text field instead of the tag-widget, I can see the saved-values in the admin.

This is the code I have (which used to be enough before the refactor with telepath).

from wagtail.admin.widgets import AdminTagWidget

class TagBlock(TextBlock):
    @cached_property
    def field(self):
        field_kwargs = {"widget": AdminTagWidget()}
        field_kwargs.update(self.field_options)
        return forms.CharField(**field_kwargs)

I think the following link is what I need to complete somehow to get it to work: https://docs.wagtail.io/en/stable/reference/streamfield/widget_api.html#form-widget-client-side-api

I've tried with this:

class AdminTagWidgetAdapter(WidgetAdapter):
    class Media:
        js = [
            "wagtailadmin/js/vendor/tag-it.js",
            "js/admin/admin-tag-widget-adapter.js",
        ]


register(AdminTagWidgetAdapter(), AdminTagWidget)

And under js/admin/admin-tag-widget-adapter.js:

console.log("adapter"); // this shows up in the console


class BoundWidget { // copied from wagtail source code
    
    constructor(element, name, idForLabel, initialState) {
        var selector = ':input[name="' + name + '"]';
        this.input = element.find(selector).addBack(selector);  // find, including element itself
        this.idForLabel = idForLabel;
        this.setState(initialState);
    }
    getValue() {
        return this.input.val();
    }
    getState() {
        return this.input.val();
    }
    setState(state) {
        this.input.val(state);
    }
    getTextLabel(opts) {
        const val = this.getValue();
        if (typeof val !== 'string') return null;
        const maxLength = opts && opts.maxLength;
        if (maxLength && val.length > maxLength) {
            return val.substring(0, maxLength - 1) + '…';
        }
        return val;
    }
    focus() {
        this.input.focus();
    }
}

// my code here:

class AdminTagWidget {
    constructor(html, idPattern) {
        this.html = html;
        this.idPattern = idPattern;
    }

    boundWidgetClass = BoundWidget;

    render(placeholder, name, id, initialState) {
        console.log("RENDER", placeholder, name, id, initialState); // this does not show

        var html = this.html.replace(/__NAME__/g, name).replace(/__ID__/g, id);
        var idForLabel = this.idPattern.replace(/__ID__/g, id);
        var dom = $(html);
        $(placeholder).replaceWith(dom);
        // eslint-disable-next-line new-cap
        return new this.boundWidgetClass(dom, name, idForLabel, initialState);
    }
}
console.log("here") // does show in the console

// variants I've tried:
//window.telepath.register('wagtail.admin.widgets.tags.AdminTagWidget', AdminTagWidget);
//window.telepath.register('wagtail.widgets.AdminTagWidget', AdminTagWidget);
window.telepath.register('path.where.its.used.AdminTagWidget', AdminTagWidget)

The log from my custom render method does not show. It seems that I'm not calling the right path within window.telepath.register but I don't know how what the string is supposed to be...

I'm not even sure if this is the right way forward.


Notes:

  • it works in regular field, the question is about tags in blocks
  • I'm using Wagtail version 2.13.2 but I've also tried with 2.15 without any difference.
  • In the console, I can log window.telepath and see my custom widget. It's just not "applied" to anything

Solution

  • Your WidgetAdapter class needs a js_constructor attribute:

    class AdminTagWidgetAdapter(WidgetAdapter):
        js_constructor = 'myapp.widgets.AdminTagWidget'
    
        class Media:
            js = [
                "wagtailadmin/js/vendor/tag-it.js",
                "js/admin/admin-tag-widget-adapter.js",
            ]
    

    Any string value will work here - it just needs to uniquely identify the class, so it's recommended to use a dotted module-like path to avoid colliding with others. This then matches the string you pass to window.telepath.register on the Javascript side:

    window.telepath.register('myapp.widgets.AdminTagWidget', AdminTagWidget)