Add new hooks for plugins (#8103)

* Notify beforeUpdate on disabled plugins

cc?

cc2

cc3

typo

* init, unInit, enabled, disabled

self review :)

update the new hook signatures to unified

merge error

* Review update

* start/stop, cc

* types, jsdoc

* stop between destroy and uninstall
This commit is contained in:
Jukka Kurkela
2020-11-28 22:57:45 +02:00
committed by GitHub
parent d6b4fe2629
commit 78dbeea1f0
8 changed files with 243 additions and 68 deletions

View File

@@ -499,3 +499,4 @@ All helpers are now exposed in a flat hierarchy, e.g., `Chart.helpers.canvas.cli
* `afterDatasetsUpdate`, `afterUpdate`, `beforeDatasetsUpdate`, and `beforeUpdate` now receive `args` object as second argument. `options` argument is always the last and thus was moved from 2nd to 3rd place.
* `afterEvent` and `beforeEvent` now receive a wrapped `event` as the `event` property of the second argument. The native event is available via `args.event.native`.
* Initial `resize` is no longer silent. Meaning that `resize` event can fire between `beforeInit` and `afterInit`
* New hooks: `install`, `start`, `stop`, and `uninstall`

View File

@@ -1,6 +1,6 @@
import defaults from './core.defaults';
import registry from './core.registry';
import {mergeIf, valueOrDefault} from '../helpers/helpers.core';
import {callback as callCallback, mergeIf, valueOrDefault} from '../helpers/helpers.core';
/**
* @typedef { import("./core.controller").default } Chart
@@ -9,6 +9,10 @@ import {mergeIf, valueOrDefault} from '../helpers/helpers.core';
*/
export default class PluginService {
constructor() {
this._init = [];
}
/**
* Calls enabled plugins for `chart` on the specified hook and with the given args.
* This method immediately returns as soon as a plugin explicitly returns false. The
@@ -19,18 +23,34 @@ export default class PluginService {
* @returns {boolean} false if any of the plugins return false, else returns true.
*/
notify(chart, hook, args) {
args = args || {};
const descriptors = this._descriptors(chart);
const me = this;
for (let i = 0; i < descriptors.length; ++i) {
const descriptor = descriptors[i];
if (hook === 'beforeInit') {
me._init = me._createDescriptors(chart, true);
me._notify(me._init, chart, 'install');
}
const descriptors = me._descriptors(chart);
const result = me._notify(descriptors, chart, hook, args);
if (hook === 'destroy') {
me._notify(descriptors, chart, 'stop');
me._notify(me._init, chart, 'uninstall');
}
return result;
}
/**
* @private
*/
_notify(descriptors, chart, hook, args) {
args = args || {};
for (const descriptor of descriptors) {
const plugin = descriptor.plugin;
const method = plugin[hook];
if (typeof method === 'function') {
const params = [chart, args, descriptor.options];
if (method.apply(plugin, params) === false) {
return false;
}
const params = [chart, args, descriptor.options];
if (callCallback(method, params, plugin) === false) {
return false;
}
}
@@ -38,6 +58,7 @@ export default class PluginService {
}
invalidate() {
this._oldCache = this._cache;
this._cache = undefined;
}
@@ -50,15 +71,31 @@ export default class PluginService {
return this._cache;
}
const descriptors = this._cache = this._createDescriptors(chart);
this._notifyStateChanges(chart);
return descriptors;
}
_createDescriptors(chart, all) {
const config = chart && chart.config;
const options = valueOrDefault(config.options && config.options.plugins, {});
const plugins = allPlugins(config);
// options === false => all plugins are disabled
const descriptors = options === false ? [] : createDescriptors(plugins, options);
return options === false && !all ? [] : createDescriptors(plugins, options, all);
}
this._cache = descriptors;
return descriptors;
/**
* @param {Chart} chart
* @private
*/
_notifyStateChanges(chart) {
const previousDescriptors = this._oldCache || [];
const descriptors = this._cache;
const diff = (a, b) => a.filter(x => !b.some(y => x.plugin.id === y.plugin.id));
this._notify(diff(previousDescriptors, descriptors), chart, 'stop');
this._notify(diff(descriptors, previousDescriptors), chart, 'start');
}
}
@@ -84,20 +121,26 @@ function allPlugins(config) {
return plugins;
}
function createDescriptors(plugins, options) {
function getOpts(options, all) {
if (!all && options === false) {
return null;
}
if (options === true) {
return {};
}
return options;
}
function createDescriptors(plugins, options, all) {
const result = [];
for (let i = 0; i < plugins.length; i++) {
const plugin = plugins[i];
const id = plugin.id;
let opts = options[id];
if (opts === false) {
const opts = getOpts(options[id], all);
if (opts === null) {
continue;
}
if (opts === true) {
opts = {};
}
result.push({
plugin,
options: mergeIf({}, [opts, defaults.plugins[id]])
@@ -113,6 +156,30 @@ function createDescriptors(plugins, options) {
* @typedef {object} IPlugin
* @since 2.1.0
*/
/**
* @method IPlugin#install
* @desc Called when plugin is installed for this chart instance. This hook is called on disabled plugins (options === false).
* @param {Chart} chart - The chart instance.
* @param {object} args - The call arguments.
* @param {object} options - The plugin options.
* @since 3.0.0
*/
/**
* @method IPlugin#start
* @desc Called when a plugin is starting. This happens when chart is created or plugin is enabled.
* @param {Chart} chart - The chart instance.
* @param {object} args - The call arguments.
* @param {object} options - The plugin options.
* @since 3.0.0
*/
/**
* @method IPlugin#stop
* @desc Called when a plugin stopping. This happens when chart is destroyed or plugin is disabled.
* @param {Chart} chart - The chart instance.
* @param {object} args - The call arguments.
* @param {object} options - The plugin options.
* @since 3.0.0
*/
/**
* @method IPlugin#beforeInit
* @desc Called before initializing `chart`.
@@ -338,8 +405,16 @@ function createDescriptors(plugins, options) {
*/
/**
* @method IPlugin#destroy
* @desc Called after the chart as been destroyed.
* @desc Called after the chart has been destroyed.
* @param {Chart} chart - The chart instance.
* @param {object} args - The call arguments.
* @param {object} options - The plugin options.
*/
/**
* @method IPlugin#uninstall
* @desc Called after chart is destroyed on all plugins that were installed for that chart. This hook is called on disabled plugins (options === false).
* @param {Chart} chart - The chart instance.
* @param {object} args - The call arguments.
* @param {object} options - The plugin options.
* @since 3.0.0
*/

View File

@@ -653,12 +653,14 @@ export default {
*/
_element: Legend,
beforeInit(chart) {
start(chart) {
const legendOpts = resolveOptions(chart.options.plugins.legend);
createNewLegendAndAttach(chart, legendOpts);
},
if (legendOpts) {
createNewLegendAndAttach(chart, legendOpts);
}
stop(chart) {
layouts.removeBox(chart, chart.legend);
delete chart.legend;
},
// During the beforeUpdate step, the layout configuration needs to run

View File

@@ -184,11 +184,17 @@ export default {
*/
_element: Title,
beforeInit(chart, options) {
start(chart, _args, options) {
createTitle(chart, options);
},
beforeUpdate(chart, args, options) {
stop(chart) {
const titleBlock = chart.titleBlock;
layouts.removeBox(chart, titleBlock);
delete chart.titleBlock;
},
beforeUpdate(chart, _args, options) {
if (options === false) {
removeTitle(chart);
} else {

View File

@@ -1383,44 +1383,49 @@ describe('Chart', function() {
});
describe('plugin.extensions', function() {
var hooks = {
install: ['install'],
uninstall: ['uninstall'],
init: [
'beforeInit',
'resize',
'afterInit'
],
start: ['start'],
stop: ['stop'],
update: [
'beforeUpdate',
'beforeLayout',
'afterLayout',
'beforeDatasetsUpdate',
'beforeDatasetUpdate',
'afterDatasetUpdate',
'afterDatasetsUpdate',
'afterUpdate',
],
render: [
'beforeRender',
'beforeDraw',
'beforeDatasetsDraw',
'beforeDatasetDraw',
'afterDatasetDraw',
'afterDatasetsDraw',
'beforeTooltipDraw',
'afterTooltipDraw',
'afterDraw',
'afterRender',
],
resize: [
'resize'
],
destroy: [
'destroy'
]
};
it ('should notify plugin in correct order', function(done) {
var plugin = this.plugin = {};
var sequence = [];
var hooks = {
init: [
'beforeInit',
'resize',
'afterInit'
],
update: [
'beforeUpdate',
'beforeLayout',
'afterLayout',
'beforeDatasetsUpdate',
'beforeDatasetUpdate',
'afterDatasetUpdate',
'afterDatasetsUpdate',
'afterUpdate',
],
render: [
'beforeRender',
'beforeDraw',
'beforeDatasetsDraw',
'beforeDatasetDraw',
'afterDatasetDraw',
'afterDatasetsDraw',
'beforeTooltipDraw',
'afterTooltipDraw',
'afterDraw',
'afterRender',
],
resize: [
'resize'
],
destroy: [
'destroy'
]
};
Object.keys(hooks).forEach(function(group) {
hooks[group].forEach(function(name) {
@@ -1447,13 +1452,17 @@ describe('Chart', function() {
chart.destroy();
expect(sequence).toEqual([].concat(
hooks.install,
hooks.start,
hooks.init,
hooks.update,
hooks.render,
hooks.resize,
hooks.update,
hooks.render,
hooks.destroy
hooks.destroy,
hooks.stop,
hooks.uninstall
));
done();
@@ -1461,6 +1470,55 @@ describe('Chart', function() {
chart.canvas.parentNode.style.width = '400px';
});
it ('should notify initially disabled plugin in correct order', function() {
var plugin = this.plugin = {id: 'plugin'};
var sequence = [];
Object.keys(hooks).forEach(function(group) {
hooks[group].forEach(function(name) {
plugin[name] = function() {
sequence.push(name);
};
});
});
var chart = window.acquireChart({
type: 'line',
data: {datasets: [{}]},
plugins: [plugin],
options: {
plugins: {
plugin: false
}
}
});
expect(sequence).toEqual([].concat(
hooks.install
));
sequence = [];
chart.options.plugins.plugin = true;
chart.update();
expect(sequence).toEqual([].concat(
hooks.start,
hooks.update,
hooks.render
));
sequence = [];
chart.options.plugins.plugin = false;
chart.update();
expect(sequence).toEqual(hooks.stop);
sequence = [];
chart.destroy();
expect(sequence).toEqual(hooks.uninstall);
});
it('should not notify before/afterDatasetDraw if dataset is hidden', function() {
var sequence = [];
var plugin = this.plugin = {

View File

@@ -760,7 +760,7 @@ describe('Legend block tests', function() {
expect(chart.legend.weight).toBe(42);
});
xit ('should remove the legend if the new options are false', function() {
it ('should remove the legend if the new options are false', function() {
var chart = acquireChart({
type: 'line',
data: {

View File

@@ -295,7 +295,7 @@ describe('Title block tests', function() {
expect(chart.titleBlock.weight).toBe(42);
});
xit ('should remove the title if the new options are false', function() {
it ('should remove the title if the new options are false', function() {
var chart = acquireChart({
type: 'line',
data: {

37
types/core/index.d.ts vendored
View File

@@ -567,6 +567,30 @@ export const layouts: {
export interface Plugin<O = {}> {
id: string;
/**
* @desc Called when plugin is installed for this chart instance. This hook is also invoked for disabled plugins (options === false).
* @param {Chart} chart - The chart instance.
* @param {object} args - The call arguments.
* @param {object} options - The plugin options.
* @since 3.0.0
*/
install?(chart: Chart, args: {}, options: O): void;
/**
* @desc Called when a plugin is starting. This happens when chart is created or plugin is enabled.
* @param {Chart} chart - The chart instance.
* @param {object} args - The call arguments.
* @param {object} options - The plugin options.
* @since 3.0.0
*/
start?(chart: Chart, args: {}, options: O): void;
/**
* @desc Called when a plugin stopping. This happens when chart is destroyed or plugin is disabled.
* @param {Chart} chart - The chart instance.
* @param {object} args - The call arguments.
* @param {object} options - The plugin options.
* @since 3.0.0
*/
stop?(chart: Chart, args: {}, options: O): void;
/**
* @desc Called before initializing `chart`.
* @param {Chart} chart - The chart instance.
@@ -772,11 +796,20 @@ export interface Plugin<O = {}> {
*/
resize?(chart: Chart, args: { size: { width: number, height: number } }, options: O): boolean | void;
/**
* Called after the chart as been destroyed.
* Called after the chart has been destroyed.
* @param {Chart} chart - The chart instance.
* @param {object} args - The call arguments.
* @param {object} options - The plugin options.
*/
destroy?(chart: Chart, options: O): boolean | void;
destroy?(chart: Chart, args: {}, options: O): boolean | void;
/**
* Called after chart is destroyed on all plugins that were installed for that chart. This hook is also invoked for disabled plugins (options === false).
* @param {Chart} chart - The chart instance.
* @param {object} args - The call arguments.
* @param {object} options - The plugin options.
* @since 3.0.0
*/
uninstall?(chart: Chart, args: {}, options: O): void;
}
export declare type ChartComponentLike = ChartComponent | ChartComponent[] | { [key: string]: ChartComponent };