mirror of
https://github.com/wagtail/wagtail.git
synced 2024-11-25 05:02:57 +01:00
Add Stimulus BlockController
This commit is contained in:
parent
6e1bca24e5
commit
d2405eefe8
125
client/src/controllers/BlockController.test.js
Normal file
125
client/src/controllers/BlockController.test.js
Normal file
@ -0,0 +1,125 @@
|
||||
import { Application } from '@hotwired/stimulus';
|
||||
import { BlockController } from './BlockController';
|
||||
|
||||
const render = jest.fn();
|
||||
const unpack = jest.fn(() => ({ render }));
|
||||
window.telepath = { unpack };
|
||||
|
||||
describe('BlockController', () => {
|
||||
const eventNames = ['w-block:ready'];
|
||||
|
||||
const events = {};
|
||||
|
||||
eventNames.forEach((name) => {
|
||||
document.addEventListener(name, (event) => {
|
||||
events[name].push(event);
|
||||
});
|
||||
});
|
||||
|
||||
let application;
|
||||
let errors = [];
|
||||
|
||||
const setup = (html, { identifier = 'w-block' } = {}) => {
|
||||
document.body.innerHTML = `<main>${html}</main>`;
|
||||
|
||||
application = new Application();
|
||||
|
||||
application.register(identifier, BlockController);
|
||||
|
||||
application.handleError = (error, message) => {
|
||||
errors.push({ error, message });
|
||||
};
|
||||
|
||||
application.start();
|
||||
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
application?.stop();
|
||||
document.body.innerHTML = '';
|
||||
errors = [];
|
||||
eventNames.forEach((name) => {
|
||||
events[name] = [];
|
||||
});
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('does nothing if block element is not found', async () => {
|
||||
await setup('<div></div>');
|
||||
|
||||
expect(errors).toHaveLength(0);
|
||||
|
||||
expect(unpack).not.toHaveBeenCalled();
|
||||
|
||||
expect(events['w-block:ready']).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should render block if element is controlled', async () => {
|
||||
const data = { _args: ['...'], _type: 'wagtail.blocks.StreamBlock' };
|
||||
|
||||
await setup(
|
||||
`<div
|
||||
id="my-element"
|
||||
data-controller="w-block"
|
||||
data-w-block-data-value='${JSON.stringify(data)}'
|
||||
>
|
||||
</div>`,
|
||||
);
|
||||
|
||||
expect(errors).toHaveLength(0);
|
||||
expect(unpack).toHaveBeenCalledWith(data);
|
||||
expect(render).toHaveBeenCalledWith(
|
||||
document.getElementById('my-element'),
|
||||
'my-element',
|
||||
);
|
||||
expect(events['w-block:ready']).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should call the unpacked render function with provided initial & error data', async () => {
|
||||
const data = { _args: ['...'], _type: 'wagtail.blocks.StreamBlock' };
|
||||
const initialData = [{ type: 'paragraph_block', value: '...' }];
|
||||
const errorData = { messages: ['An error...'] };
|
||||
|
||||
await setup(
|
||||
`<div
|
||||
id="my-element"
|
||||
data-controller="w-block"
|
||||
data-w-block-arguments-value='${JSON.stringify([initialData, errorData])}'
|
||||
data-w-block-data-value='${JSON.stringify(data)}'
|
||||
>
|
||||
</div>`,
|
||||
);
|
||||
|
||||
expect(errors).toHaveLength(0);
|
||||
expect(unpack).toHaveBeenCalledWith(data);
|
||||
expect(render).toHaveBeenCalledWith(
|
||||
document.getElementById('my-element'),
|
||||
'my-element',
|
||||
initialData,
|
||||
errorData,
|
||||
);
|
||||
expect(events['w-block:ready']).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should throw an error if used on an element without an id', async () => {
|
||||
await setup('<div data-controller="w-block"></div>');
|
||||
|
||||
expect(errors).toHaveLength(1);
|
||||
expect(errors).toHaveProperty(
|
||||
'0.error.message',
|
||||
'Controlled element needs an id attribute.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if Telepath is not available in the window global', async () => {
|
||||
delete window.telepath;
|
||||
await setup('<div id="my-element" data-controller="w-block"></div>');
|
||||
|
||||
expect(errors).toHaveLength(1);
|
||||
expect(errors).toHaveProperty(
|
||||
'0.error.message',
|
||||
'`window.telepath` is not available.',
|
||||
);
|
||||
});
|
||||
});
|
86
client/src/controllers/BlockController.ts
Normal file
86
client/src/controllers/BlockController.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import { Controller } from '@hotwired/stimulus';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
initBlockWidget?: (id: string) => void;
|
||||
telepath: any;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the ability to unpack a Telepath object and render it on the controlled element.
|
||||
* Used to initialize the top-level element of a BlockWidget (the form widget for a StreamField).
|
||||
*
|
||||
* @example
|
||||
* <div
|
||||
* id="some-id"
|
||||
* data-controller="w-block"
|
||||
* data-w-block-data-value='{"_args":["..."], "_type": "wagtail.blocks.StreamBlock"}'
|
||||
* >
|
||||
* </div>
|
||||
*
|
||||
* @example - with initial arguments
|
||||
* <div
|
||||
* id="some-id"
|
||||
* data-controller="w-block"
|
||||
* data-w-block-data-value='{"_args":["..."], "_type": "wagtail.blocks.StreamBlock"}'
|
||||
* data-w-block-arguments-value='[[{ type: "paragraph_block", value: "..."}], {messages:["An error..."]}]'
|
||||
* >
|
||||
* </div>
|
||||
*/
|
||||
export class BlockController extends Controller<HTMLElement> {
|
||||
static values = {
|
||||
arguments: { type: Array, default: [] },
|
||||
data: { type: Object, default: {} },
|
||||
};
|
||||
|
||||
/** Array of arguments to pass to the render method of the block [initial value, errors]. */
|
||||
declare argumentsValue: Array<string>;
|
||||
/** Block definition to be passed to `telepath.unpack`, used to obtain a JavaScript representation of the block. */
|
||||
declare dataValue: object;
|
||||
|
||||
connect() {
|
||||
const telepath = window.telepath;
|
||||
|
||||
if (!telepath) {
|
||||
throw new Error('`window.telepath` is not available.');
|
||||
}
|
||||
|
||||
const element = this.element;
|
||||
const id = element.id;
|
||||
|
||||
if (!id) {
|
||||
throw new Error('Controlled element needs an id attribute.');
|
||||
}
|
||||
|
||||
const output = telepath.unpack(this.dataValue);
|
||||
output.render(element, id, ...this.argumentsValue);
|
||||
this.dispatch('ready', { detail: { ...output }, cancelable: false });
|
||||
}
|
||||
|
||||
static afterLoad() {
|
||||
/**
|
||||
* Provide a backwards compatible version of the original window global function.
|
||||
*
|
||||
* @deprecated RemovedInWagtail70
|
||||
*/
|
||||
window.initBlockWidget = (id: string) => {
|
||||
const body = document.querySelector(
|
||||
'#' + id + '[data-block]',
|
||||
) as HTMLElement;
|
||||
|
||||
if (!body) {
|
||||
return;
|
||||
}
|
||||
|
||||
const blockDefData = JSON.parse(body.dataset.data as string);
|
||||
if (window.telepath) {
|
||||
const blockDef = window.telepath.unpack(blockDefData);
|
||||
const blockValue = JSON.parse(body.dataset.value as string);
|
||||
const blockError = JSON.parse(body.dataset.error as string);
|
||||
|
||||
blockDef.render(body, id, blockValue, blockError);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@ import type { Definition } from '@hotwired/stimulus';
|
||||
// Order controller imports alphabetically.
|
||||
import { ActionController } from './ActionController';
|
||||
import { AutosizeController } from './AutosizeController';
|
||||
import { BlockController } from './BlockController';
|
||||
import { BulkController } from './BulkController';
|
||||
import { ClipboardController } from './ClipboardController';
|
||||
import { CloneController } from './CloneController';
|
||||
@ -34,6 +35,7 @@ export const coreControllerDefinitions: Definition[] = [
|
||||
// Keep this list in alphabetical order
|
||||
{ controllerConstructor: ActionController, identifier: 'w-action' },
|
||||
{ controllerConstructor: AutosizeController, identifier: 'w-autosize' },
|
||||
{ controllerConstructor: BlockController, identifier: 'w-block' },
|
||||
{ controllerConstructor: BulkController, identifier: 'w-bulk' },
|
||||
{ controllerConstructor: ClipboardController, identifier: 'w-clipboard' },
|
||||
{ controllerConstructor: CloneController, identifier: 'w-clone' },
|
||||
|
Loading…
Reference in New Issue
Block a user