kopia lustrzana https://github.com/wagtail/wagtail
Refactor Stimulus util & module export
- Accessing constructor was confusing and createController is not a core requirement - Avoid modifying the base Stimulus application class - Expose `window.StimulusModule` as the module output - Expose `window.wagtail.app` as the Wagtail Stimulus application instance - Rename root element variable to `root` in initStimulus (so we do not conflict with potential action option registration that uses `element` variable names) - Pull in the wagtail.components to the same core.js file and add JSDoc for exposed global - Relates to #10197pull/11067/head
rodzic
5c92c8cb8d
commit
1da3e5fde7
|
@ -1,11 +1,34 @@
|
|||
import $ from 'jquery';
|
||||
import * as StimulusModule from '@hotwired/stimulus';
|
||||
|
||||
import { Icon, Portal } from '../..';
|
||||
import { coreControllerDefinitions } from '../../controllers';
|
||||
import { escapeHtml } from '../../utils/text';
|
||||
import { initStimulus } from '../../includes/initStimulus';
|
||||
|
||||
/** initialise Wagtail Stimulus application with core controller definitions */
|
||||
window.Stimulus = initStimulus({ definitions: coreControllerDefinitions });
|
||||
/** Expose a global to allow for customisations and packages to build with Stimulus. */
|
||||
window.StimulusModule = StimulusModule;
|
||||
|
||||
/**
|
||||
* Wagtail global module, useful for debugging and as the exposed
|
||||
* interface to access the Stimulus application instance and base
|
||||
* React components.
|
||||
*
|
||||
* @type {Object} wagtail
|
||||
* @property {Object} app - Wagtail's Stimulus application instance.
|
||||
* @property {Object} components - Exposed components as globals for third-party reuse.
|
||||
* @property {Object} components.Icon - Icon React component.
|
||||
* @property {Object} components.Portal - Portal React component.
|
||||
*/
|
||||
const wagtail = window.wagtail || {};
|
||||
|
||||
/** Initialise Wagtail Stimulus application with core controller definitions. */
|
||||
wagtail.app = initStimulus({ definitions: coreControllerDefinitions });
|
||||
|
||||
/** Expose components as globals for third-party reuse. */
|
||||
wagtail.components = { Icon, Portal };
|
||||
|
||||
window.wagtail = wagtail;
|
||||
|
||||
window.escapeHtml = escapeHtml;
|
||||
|
||||
|
@ -225,11 +248,3 @@ $(() => {
|
|||
$(this).removeClass('hovered');
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Wagtail global module, mainly useful for debugging.
|
||||
// =============================================================================
|
||||
|
||||
const wagtail = window.wagtail || {};
|
||||
|
||||
window.wagtail = wagtail;
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
jest.mock('../..');
|
||||
|
||||
document.addEventListener = jest.fn();
|
||||
|
||||
require('./core');
|
||||
|
||||
describe('core', () => {
|
||||
const [event] = document.addEventListener.mock.calls[0];
|
||||
|
||||
it('exposes the Stimulus application instance for reuse', () => {
|
||||
expect(Object.keys(window.wagtail.app)).toEqual(
|
||||
expect.arrayContaining(['debug', 'logger']),
|
||||
);
|
||||
|
||||
expect(window.wagtail.app.load).toBeInstanceOf(Function);
|
||||
expect(window.wagtail.app.register).toBeInstanceOf(Function);
|
||||
});
|
||||
|
||||
it('exposes components for reuse', () => {
|
||||
expect(Object.keys(window.wagtail.components)).toEqual(['Icon', 'Portal']);
|
||||
});
|
||||
|
||||
it('exposes the Stimulus module for reuse', () => {
|
||||
expect(Object.keys(window.StimulusModule)).toEqual(
|
||||
expect.arrayContaining(['Application', 'Controller']),
|
||||
);
|
||||
});
|
||||
|
||||
it('DOMContentLoaded', () => {
|
||||
expect(event).toBe('DOMContentLoaded');
|
||||
});
|
||||
});
|
|
@ -1,4 +1,3 @@
|
|||
import { Icon, Portal } from '../..';
|
||||
import { initTooltips } from '../../includes/initTooltips';
|
||||
import { initTabs } from '../../includes/tabs';
|
||||
import initSidePanel from '../../includes/sidePanel';
|
||||
|
@ -8,12 +7,6 @@ import {
|
|||
} from '../../includes/panels';
|
||||
import { initMinimap } from '../../components/Minimap';
|
||||
|
||||
// Expose components as globals for third-party reuse.
|
||||
window.wagtail.components = {
|
||||
Icon,
|
||||
Portal,
|
||||
};
|
||||
|
||||
/**
|
||||
* Add in here code to run once the page is loaded.
|
||||
*/
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
jest.mock('../..');
|
||||
|
||||
document.addEventListener = jest.fn();
|
||||
|
||||
require('./wagtailadmin');
|
||||
|
||||
describe('wagtailadmin', () => {
|
||||
const [event] = document.addEventListener.mock.calls[0];
|
||||
|
||||
it('exposes components for reuse', () => {
|
||||
expect(Object.keys(window.wagtail.components)).toEqual(['Icon', 'Portal']);
|
||||
});
|
||||
|
||||
it('DOMContentLoaded', () => {
|
||||
expect(event).toBe('DOMContentLoaded');
|
||||
});
|
||||
});
|
|
@ -4,36 +4,7 @@ import { initStimulus } from './initStimulus';
|
|||
jest.useFakeTimers();
|
||||
|
||||
/**
|
||||
* Example controller (shortcut method definitions object) from documentation
|
||||
*/
|
||||
const wordCountController = {
|
||||
STATIC: {
|
||||
values: { max: { default: 10, type: Number } },
|
||||
},
|
||||
connect() {
|
||||
this.setupOutput();
|
||||
this.updateCount();
|
||||
},
|
||||
setupOutput() {
|
||||
if (this.output) return;
|
||||
const template = document.createElement('template');
|
||||
template.innerHTML = `<output name='word-count' for='${this.element.id}'></output>`;
|
||||
const output = template.content.firstChild;
|
||||
this.element.insertAdjacentElement('beforebegin', output);
|
||||
this.output = output;
|
||||
},
|
||||
updateCount(event) {
|
||||
const value = event ? event.target.value : this.element.value;
|
||||
const words = (value || '').split(' ');
|
||||
this.output.textContent = `${words.length} / ${this.maxValue} words`;
|
||||
},
|
||||
disconnect() {
|
||||
this.output && this.output.remove();
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Example controller from documentation as an ES6 class
|
||||
* Example controller.
|
||||
*/
|
||||
class WordCountController extends Controller {
|
||||
static values = { max: { default: 10, type: Number } };
|
||||
|
@ -51,7 +22,7 @@ class WordCountController extends Controller {
|
|||
setupOutput() {
|
||||
if (this.output) return;
|
||||
const template = document.createElement('template');
|
||||
template.innerHTML = `<output name='word-count' for='${this.element.id}' style='float: right;'></output>`;
|
||||
template.innerHTML = `<output name='word-count' for='${this.element.id}' class='output-label'></output>`;
|
||||
const output = template.content.firstChild;
|
||||
this.element.insertAdjacentElement('beforebegin', output);
|
||||
this.output = output;
|
||||
|
@ -115,52 +86,6 @@ describe('initStimulus', () => {
|
|||
expect(application.controllers[0]).toBeInstanceOf(TestMockController);
|
||||
});
|
||||
|
||||
it('should support registering a controller via an object with the createController static method', async () => {
|
||||
const section = document.createElement('section');
|
||||
section.id = 'example-a';
|
||||
section.innerHTML = `<input value="some words" id="example-a-input" data-controller="example-a" data-action="change->example-a#updateCount" />`;
|
||||
|
||||
// create a controller and register it
|
||||
application.register(
|
||||
'example-a',
|
||||
application.constructor.createController(wordCountController),
|
||||
);
|
||||
|
||||
// before controller element added - should not include an `output` element
|
||||
expect(document.querySelector('#example-a > output')).toEqual(null);
|
||||
|
||||
document.querySelector('section').after(section);
|
||||
|
||||
await Promise.resolve();
|
||||
|
||||
// after controller connected - should have an output element
|
||||
expect(document.querySelector('#example-a > output').innerHTML).toEqual(
|
||||
'2 / 10 words',
|
||||
);
|
||||
|
||||
await Promise.resolve();
|
||||
|
||||
// should respond to changes on the input
|
||||
const input = document.querySelector('#example-a > input');
|
||||
input.setAttribute('value', 'even more words');
|
||||
input.dispatchEvent(new Event('change'));
|
||||
|
||||
expect(document.querySelector('#example-a > output').innerHTML).toEqual(
|
||||
'3 / 10 words',
|
||||
);
|
||||
|
||||
// removal of the input should also remove the output (disconnect method)
|
||||
input.remove();
|
||||
|
||||
await Promise.resolve();
|
||||
|
||||
// should call the disconnect method (removal of the injected HTML)
|
||||
expect(document.querySelector('#example-a > output')).toEqual(null);
|
||||
|
||||
// clean up
|
||||
section.remove();
|
||||
});
|
||||
|
||||
it('should support the documented approach for registering a controller via a class with register', async () => {
|
||||
const section = document.createElement('section');
|
||||
section.id = 'example-b';
|
||||
|
@ -203,30 +128,4 @@ describe('initStimulus', () => {
|
|||
// clean up
|
||||
section.remove();
|
||||
});
|
||||
|
||||
it('should provide access to a base Controller class on the returned application instance', () => {
|
||||
expect(application.constructor.Controller).toEqual(Controller);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createController', () => {
|
||||
const createController = initStimulus().constructor.createController;
|
||||
|
||||
it('should safely create a Stimulus Controller class if no args provided', () => {
|
||||
const CustomController = createController();
|
||||
expect(CustomController.prototype instanceof Controller).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should create a Stimulus Controller class with static properties', () => {
|
||||
const someMethod = jest.fn();
|
||||
|
||||
const CustomController = createController({
|
||||
STATIC: { targets: ['source'] },
|
||||
someMethod,
|
||||
});
|
||||
|
||||
expect(CustomController.targets).toEqual(['source']);
|
||||
expect(CustomController.someMethod).toBeUndefined();
|
||||
expect(CustomController.prototype.someMethod).toEqual(someMethod);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,81 +1,24 @@
|
|||
import type { Definition } from '@hotwired/stimulus';
|
||||
import { Application, Controller } from '@hotwired/stimulus';
|
||||
|
||||
type ControllerObjectDefinition = Record<string, () => void> & {
|
||||
STATIC?: {
|
||||
classes?: string[];
|
||||
targets?: string[];
|
||||
values: typeof Controller.values;
|
||||
};
|
||||
};
|
||||
import { Application } from '@hotwired/stimulus';
|
||||
|
||||
/**
|
||||
* Extend the Stimulus application class to provide some convenience
|
||||
* static attributes or methods to be accessed globally.
|
||||
*/
|
||||
class WagtailApplication extends Application {
|
||||
/**
|
||||
* Ensure the base Controller class is available for new controllers.
|
||||
*/
|
||||
static Controller = Controller;
|
||||
|
||||
/**
|
||||
* Function that accepts a plain old object and returns a Stimulus Controller.
|
||||
* Useful when ES6 modules with base class being extended not in use
|
||||
* or build tool not in use or for just super convenient class creation.
|
||||
*
|
||||
* Inspired heavily by
|
||||
* https://github.com/StackExchange/Stacks/blob/v1.6.5/lib/ts/stacks.ts#L84
|
||||
*
|
||||
* @example
|
||||
* createController({
|
||||
* STATIC: { targets = ['container'] }
|
||||
* connect() {
|
||||
* console.log('connected', this.element, this.containerTarget);
|
||||
* }
|
||||
* })
|
||||
*
|
||||
*/
|
||||
static createController = (
|
||||
controllerDefinition: ControllerObjectDefinition = {},
|
||||
): typeof Controller => {
|
||||
class NewController<X extends Element> extends Controller<X> {}
|
||||
|
||||
const { STATIC = {}, ...controllerDefinitionWithoutStatic } =
|
||||
controllerDefinition;
|
||||
|
||||
// set up static values
|
||||
Object.entries(STATIC).forEach(([key, value]) => {
|
||||
NewController[key] = value;
|
||||
});
|
||||
|
||||
// set up class methods
|
||||
Object.assign(NewController.prototype, controllerDefinitionWithoutStatic);
|
||||
|
||||
return NewController;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialises the Wagtail Stimulus application and dispatches and registers
|
||||
* custom event behaviour.
|
||||
* Initialises the Wagtail Stimulus application, loads the provided controller
|
||||
* definitions and returns the app instance.
|
||||
*
|
||||
* Loads the supplied core controller definitions into the application.
|
||||
* Turns on debug mode if in local development (for now).
|
||||
* Turns on debug mode if in local development.
|
||||
*/
|
||||
export const initStimulus = ({
|
||||
debug = process.env.NODE_ENV === 'development',
|
||||
definitions = [],
|
||||
element = document.documentElement,
|
||||
root = document.documentElement,
|
||||
}: {
|
||||
debug?: boolean;
|
||||
definitions?: Definition[];
|
||||
element?: HTMLElement;
|
||||
root?: HTMLElement;
|
||||
} = {}): Application => {
|
||||
const application = WagtailApplication.start(element);
|
||||
|
||||
application.debug = debug;
|
||||
application.load(definitions);
|
||||
|
||||
return application;
|
||||
const app = Application.start(root);
|
||||
app.debug = debug;
|
||||
app.load(definitions);
|
||||
return app;
|
||||
};
|
||||
|
|
|
@ -32,8 +32,8 @@ export class StimulusWrapper extends React.Component<{
|
|||
|
||||
componentDidMount() {
|
||||
const { debug = false, definitions = [] } = this.props;
|
||||
const element = this.ref.current || document.documentElement;
|
||||
this.application = initStimulus({ debug, definitions, element });
|
||||
const root = this.ref.current || document.documentElement;
|
||||
this.application = initStimulus({ debug, definitions, root });
|
||||
}
|
||||
|
||||
componentDidUpdate({ debug: prevDebug }) {
|
||||
|
|
Ładowanie…
Reference in New Issue