Skip to content
This repository has been archived by the owner on Feb 19, 2022. It is now read-only.

External events #324

Merged
merged 12 commits into from
Jan 2, 2018
17 changes: 17 additions & 0 deletions src/victory-legend/victory-legend.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,23 @@ class VictoryLegend extends React.Component {
]),
eventHandlers: PropTypes.object
})),
externalEventMutations: PropTypes.arrayOf(PropTypes.shape({
callback: PropTypes.function,
childName: PropTypes.oneOfType([
PropTypes.string,
PropTypes.array
]),
eventKey: PropTypes.oneOfType([
PropTypes.array,
CustomPropTypes.allOfType([CustomPropTypes.integer, CustomPropTypes.nonNegative]),
PropTypes.string
]),
mutation: PropTypes.function,
target: PropTypes.oneOfType([
PropTypes.string,
PropTypes.array
])
})),
groupComponent: PropTypes.element,
gutter: PropTypes.oneOfType([
CustomPropTypes.nonNegative,
Expand Down
42 changes: 37 additions & 5 deletions src/victory-shared-events/victory-shared-events.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { assign, isFunction, partialRight, defaults, fromPairs } from "lodash";
import { assign, isFunction, partialRight, defaults, isEmpty, fromPairs } from "lodash";
import React from "react";
import PropTypes from "prop-types";
import CustomPropTypes from "../victory-util/prop-types";
Expand Down Expand Up @@ -37,6 +37,23 @@ export default class VictorySharedEvents extends React.Component {
]),
target: PropTypes.string
})),
externalEventMutations: PropTypes.arrayOf(PropTypes.shape({
callback: PropTypes.function,
childName: PropTypes.oneOfType([
PropTypes.string,
PropTypes.array
]),
eventKey: PropTypes.oneOfType([
PropTypes.array,
CustomPropTypes.allOfType([CustomPropTypes.integer, CustomPropTypes.nonNegative]),
PropTypes.string
]),
mutation: PropTypes.function,
target: PropTypes.oneOfType([
PropTypes.string,
PropTypes.array
])
})),
groupComponent: PropTypes.node
};

Expand Down Expand Up @@ -96,11 +113,26 @@ export default class VictorySharedEvents extends React.Component {

setUpChildren(props) {
this.events = this.getAllEvents(props);
if (this.events) {
this.childComponents = React.Children.toArray(props.children);
const { externalEventMutations, container, children } = props;
if (this.events || !isEmpty(externalEventMutations)) {
this.childComponents = React.Children.toArray(children);
const childBaseProps = this.getBasePropsFromChildren(this.childComponents);
const parentBaseProps = props.container ? props.container.props : {};
const parentBaseProps = container ? container.props : {};
const childNames = Object.keys(childBaseProps);
this.baseProps = assign({}, childBaseProps, { parent: parentBaseProps });

if (!isEmpty(externalEventMutations)) {
const externalMutations = Events.getExternalMutationsWithChildren(
externalEventMutations, this.baseProps, this.state, childNames
);
const callbacks = externalEventMutations.reduce((memo, mutation) => {
memo = isFunction(mutation.callback) ? memo.concat(mutation.callback) : memo;
return memo;
}, []);
const compiledCallbacks = callbacks.length ?
() => { callbacks.forEach((c) => c()); } : undefined;
this.setState(externalMutations, compiledCallbacks);
}
}
}

Expand Down Expand Up @@ -147,7 +179,7 @@ export default class VictorySharedEvents extends React.Component {
getEventState: partialRight(this.getEventState, name)
};
return memo.concat(React.cloneElement(child, assign(
{ key: `events-${name}`, sharedEvents, eventKey },
{ key: `events-${name}`, sharedEvents, eventKey, name },
child.props
)));
} else {
Expand Down
53 changes: 42 additions & 11 deletions src/victory-util/add-events.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import React from "react";
import {
defaults, assign, keys, isFunction, partialRight, pick, without, isEmpty
} from "lodash";
import { defaults, assign, keys, isFunction, partialRight, pick, without, isEmpty } from "lodash";
import Events from "./events";
import Collection from "./collection";
import VictoryTransition from "../victory-transition/victory-transition";
Expand All @@ -23,12 +21,18 @@ export default (WrappedComponent, options) => {
this.getEventState = Events.getEventState.bind(this);
const calculatedValues = this.getCalculatedValues(this.props);
this.cacheValues(calculatedValues);
this.stateChanges = this.getStateChanges(this.props, calculatedValues);
this.applyExternalMutations(this.props, calculatedValues);
}

shouldComponentUpdate(nextProps) {
componentWillReceiveProps(nextProps) {
const calculatedValues = this.getCalculatedValues(nextProps);
this.applyExternalMutations(nextProps, calculatedValues);
}

// eslint-disable-next-line max-statements
shouldComponentUpdate(nextProps) {
const calculatedValues = this.getCalculatedValues(nextProps);
const { externalMutations } = calculatedValues;
// re-render without additional checks when component is animated
if (this.props.animate || this.props.animating) {
this.cacheValues(calculatedValues);
Expand All @@ -49,22 +53,44 @@ export default (WrappedComponent, options) => {
return true;
}

// check whether external mutations match
if (!Collection.areVictoryPropsEqual(this.externaMutations, externalMutations)) {
this.cacheValues(calculatedValues);
return true;
}

return false;
}

applyExternalMutations(props, calculatedValues) {
const { externalMutations } = calculatedValues;
if (!isEmpty(externalMutations)) {
const callbacks = props.externalEventMutations.reduce((memo, mutation) => {
memo = isFunction(mutation.callback) ? memo.concat(mutation.callback) : memo;
return memo;
}, []);
const compiledCallbacks = callbacks.length ?
() => { callbacks.forEach((c) => c()); } : undefined;
this.setState(externalMutations, compiledCallbacks);
}
}

// compile all state changes from own and parent state. Order doesn't matter, as any state
// state change should trigger a re-render
getStateChanges(props, calculatedValues) {
const { hasEvents, getSharedEventState } = calculatedValues;
if (!hasEvents) { return {}; }

options = options || {};
const components = options.components || defaultComponents;

const getState = (key, type) => {
const result = defaults({}, this.getEventState(key, type), getSharedEventState(key, type));
return isEmpty(result) ? undefined : result;
const baseState = defaults(
{}, this.getEventState(key, type), getSharedEventState(key, type)
);
return isEmpty(baseState) ? undefined : baseState;
};

options = options || {};
const components = options.components || defaultComponents;
return components.map((component) => {
if (!props.standalone && component.name === "parent") {
// don't check for changes on parent props for non-standalone components
Expand All @@ -78,7 +104,7 @@ export default (WrappedComponent, options) => {
}

getCalculatedValues(props) {
const { sharedEvents } = props;
const { sharedEvents, externalEventMutations } = props;
const components = WrappedComponent.expectedComponents;
const componentEvents = Events.getComponentEvents(props, components);
const getSharedEventState = sharedEvents && isFunction(sharedEvents.getEventState) ?
Expand All @@ -87,8 +113,13 @@ export default (WrappedComponent, options) => {
const dataKeys = keys(baseProps).filter((key) => key !== "parent");
const hasEvents = props.events || props.sharedEvents || componentEvents;
const events = this.getAllEvents(props);
const externalMutations = isEmpty(externalEventMutations) || sharedEvents ? undefined :
Events.getExternalMutations(
externalEventMutations, baseProps, this.state
);
return {
componentEvents, getSharedEventState, baseProps, dataKeys, hasEvents, events
componentEvents, getSharedEventState, baseProps, dataKeys,
hasEvents, events, externalMutations
};
}

Expand Down
114 changes: 113 additions & 1 deletion src/victory-util/events.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { assign, extend, merge, partial, isEmpty, isFunction, without } from "lodash";
import {
assign, extend, merge, partial, isEmpty, isFunction, without, pickBy, uniq, includes
} from "lodash";

export default {
/* Returns all own and shared events that should be attached to a single target element,
Expand Down Expand Up @@ -207,7 +209,117 @@ export default {
return state[childType] &&
state[childType][eventKey] &&
state[childType][eventKey][namespace];
},

/**
* Returns a set of all mutations for shared events
*
* @param {Array} mutations an array of mutations objects
* @param {Object} baseProps an object that describes all props for children of VictorySharedEvents
* @param {Object} baseState an object that describes state for children of VictorySharedEvents
* @param {Array} childNames an array of childNames
*
* @return {Object} a object describing all mutations for VictorySharedEvents
*/
getExternalMutationsWithChildren(mutations, baseProps, baseState, childNames) {
baseProps = baseProps || {};
baseState = baseState || {};

return childNames.reduce((memo, childName) => {
const childState = baseState[childName];
const mutation = this.getExternalMutations(
mutations, baseProps[childName], baseState[childName], childName
);
memo[childName] = mutation ? mutation : childState;
return pickBy(memo, (v) => !isEmpty(v));
}, {});

},

/**
* Returns a set of all mutations for a component
*
* @param {Array} mutations an array of mutations objects
* @param {Object} baseProps a props object (scoped to a childName when used by shared events)
* @param {Object} baseState a state object (scoped to a childName when used by shared events)
* @param {String} childName an optional childName
*
* @return {Object} a object describing mutations for a given component
*/
getExternalMutations(mutations, baseProps, baseState, childName) {
baseProps = baseProps || {};
baseState = baseState || {};

const eventKeys = Object.keys(baseProps);
return eventKeys.reduce((memo, eventKey) => {
const keyState = baseState[eventKey] || {};
const keyProps = baseProps[eventKey] || {};
if (eventKey === "parent") {
const identifier = { eventKey, target: "parent" };
const mutation = this.getExternalMutation(mutations, keyProps, keyState, identifier);
memo[eventKey] = typeof mutation !== "undefined" ?
assign({}, keyState, mutation) : keyState;
} else {
// use keys from both state and props so that elements not intially included in baseProps
// will be used. (i.e. labels)
const targets = uniq(Object.keys(keyProps).concat(Object.keys(keyState)));
memo[eventKey] = targets.reduce((m, target) => {
const identifier = { eventKey, target, childName };
const mutation = this.getExternalMutation(
mutations, keyProps[target], keyState[target], identifier
);
m[target] = typeof mutation !== "undefined" ?
assign({}, keyState[target], mutation) : keyState[target];
return pickBy(m, (v) => !isEmpty(v));
}, {});
}
return pickBy(memo, (v) => !isEmpty(v));
}, {});
},

/**
* Returns a set of mutations for a particular element given scoped baseProps and baseState
*
* @param {Array} mutations an array of mutations objects
* @param {Object} baseProps a props object (scoped the element specified by the identifier)
* @param {Object} baseState a state object (scoped the element specified by the identifier)
* @param {Object} identifier { eventKey, target, childName }
*
* @return {Object | undefined} a object describing mutations for a given element, or undefined
*/
getExternalMutation(mutations, baseProps, baseState, identifier) {

const filterMutations = (mutation, type) => {
if (typeof mutation[type] === "string") {
return mutation[type] === "all" || mutation[type] === identifier[type];
} else if (Array.isArray(mutation[type])) {
// coerce arrays to strings before matching
const stringArray = mutation[type].map((m) => `${m}`);
return includes(stringArray, identifier[type]);
} else {
return false;
}
};

mutations = Array.isArray(mutations) ? mutations : [mutations];
let scopedMutations = mutations;
if (identifier.childName) {
scopedMutations = mutations.filter((m) => filterMutations(m, "childName"));
}
// find any mutation objects that match the target
const targetMutations = scopedMutations.filter((m) => filterMutations(m, "target"));
if (isEmpty(targetMutations)) {
return undefined;
}
const keyMutations = targetMutations.filter((m) => filterMutations(m, "eventKey"));
if (isEmpty(keyMutations)) {
return undefined;
}
return keyMutations.reduce((memo, curr) => {
const mutationFunction = curr && isFunction(curr.mutation) ? curr.mutation : () => undefined;
const currentMutation = mutationFunction(assign({}, baseProps, baseState));
return merge({}, memo, currentMutation);
}, {});
},

/* Returns an array of defaultEvents from sub-components of a given component.
Expand Down