0

I originally asked this question on the wagtail issue queue, which I guess was the wrong place for it. (Though I do think this is a bug in the documentation.)

Anyway, my issue is that I have a custom StructBlock class that uses a ListBlock inside it. I need to define a js_initializer() method on my custom class that causes the form to trigger both my own initializer and ListBlock's initializer.

My initial attempt, based on the docs, looked like this:

# my_blocks.py
class ImageGalleryBlock(blocks.StructBlock):
    images = ListBlock(ImageChooserBlock(label='Image'))

    def js_initializer(self):
        return "ImageGallery"

    @property
    def media(self):
        return forms.Media(
            js=['app/js/admin/image-gallery.js']
        )


# image-gallery.js
function ImageGallery(prefix) {
    // Set up the Image Gallery block's custom form behavior...
}

This make the ImageGallery() function run, but did not run the ListBlock's initializer, so none of its buttons worked.

On the wagtail issue queue, I was recommended to try something like this:

def js_initializer(self):
    initializer_js = super(HeadingBlock, self).js_initializer()
    my_custom_js = 'ImageGallery("%s")' % self.definition_prefix
    if initializer_js:
        # child blocks have custom JS initializers and need to be used
        return '%s,\n%s' % (initializer_js, my_custom_js)
    return my_custom_js

# image-gallery.js
function ImageGallery(prefix) {
  var init_image_gallery = function(element_prefix) {
    // Do stuff...
  };

  return init_image_gallery;
}

I had to make a few improvements to the original suggestion to get the ImageGallery() part to function, but it still doesn't run the ListBlock initializer.

Here's what the initializer code that gets generated for ImageGalleryBlock looks like:

{
    'name': ('ImageGalleryBlock'),
    'initializer': (StructBlock({
        'images': (ListBlock({
            'definitionPrefix': ('blockdef-63')
        }))
    }),
    ImageGallery("blockdef-91"))
},

I get the feeling that what I actually need to do is add another key to the dict being passed into StructBlock, but I haven't got the foggiest clue how.

coredumperror
  • 8,471
  • 6
  • 33
  • 42
  • Please include a [minimal, complete and verifiable code example](https://stackoverflow.com/help/mcve) - you haven't shared enough of ImageGalleryBlock to see where StructBlock and ListBlock are involved. – gasman Dec 09 '17 at 00:41
  • @gasman Whoops, I messed up my `ImageGalleryBlock` definition a bit. Abridged it too far, you might say. It should be right, now. – coredumperror Dec 09 '17 at 05:29

1 Answers1

2

A js_initializer method returns a Javascript expression which is evaluated once, on page load, and spits out a function; this initializer function then gets called each time your block is inserted into the form, passing the ID prefix to identify the HTML elements that should receive Javascript behaviours. It's important to understand that this is a two step process - the initial evaluation on page load (which often takes the form of a function call that returns the function to be used in the second step), and calling the initializer function for each block on the form.

Whenever a block acts as a wrapper for other blocks, as StructBlock does, it's responsible for ensuring that this contract is enforced for its child blocks: when its own js_initializer is evaluated on page load, the child js_initializers need to be evaluated at that point too, and when its initializer function is called, it calls those child initializer functions.

By subclassing StructBlock and overriding js_initializer, you're effectively adding another layer of wrapping around StructBlock: your new js_initializer needs to evaluate to a single function that both calls StructBlock's initializer function, and performs your custom image gallery setup. Here's how to do that:

image-gallery.js:

/* ImageGallery gets called once on startup; the function it returns will
be called whenever we need to set up an image gallery block on the form */
function ImageGallery(parentInitializer) {
    return function(elementPrefix) {
        /* call the original StructBlock initializer */
        parentInitializer(elementPrefix);

        /* do whatever JS setup you need for the image gallery behaviour */
        $('#' + elementPrefix + '-gallery').doStuff();
    };
}

my_blocks.py:

class ImageGalleryBlock(blocks.StructBlock):
    images = ListBlock(ImageChooserBlock(label='Image'))

    def js_initializer(self):
        parent_initializer = super(ImageGalleryBlock, self).js_initializer()
        return "ImageGallery(%s)" % parent_initializer

    @property
    def media(self):
        # need to pull in StructBlock's own js code as well as our own
        return super(ImageGalleryBlock, self).media + forms.Media(
            js=['app/js/admin/image-gallery.js']
        )
gasman
  • 23,691
  • 1
  • 38
  • 56
  • Now that is one great explanation! Everything finally works! Though I did need to replace the second "s" you used in "js_initialiser()" with a "z", since that function is spelled in the American way. I think it would be a really good idea to add this to the docs. – coredumperror Dec 11 '17 at 19:21
  • This is still not in the docs, and it really, *really* needs to be. I just spent another hour pulling my hair out over why a ListBlock on one of my new custom blocks failed to function properly, because I'd forgotten about this issue with `js_initializer`. – coredumperror May 15 '19 at 18:36