azure-devops-ui
Version:
React components for building web UI in Azure DevOps
210 lines (209 loc) • 8.53 kB
JavaScript
import "../../CommonImports";
import "../../Core/core.css";
import * as React from "react";
import { ObservableLike } from '../../Core/Observable';
/**
* Handles subscription to properties that are IObservableValues, so that components don't have to handle on their own.
*
* Usage:
*
* <Observer myObservableValue={observableValue}>
* <MyComponent myObservableValue='' />
* </Observer>
*
* Your component will get re-rendered with the new value of myObservableValue whenever that value changes.
* Additionally, any additional props set on the Observer will also get passed down.
*/
class ObserverBase extends React.Component {
constructor(props) {
super(props);
this.subscriptions = {};
// Initialize the state with the initial value of the observable.
const state = { values: {}, oldProps: {} };
for (const propName in props) {
state.values[propName] = getPropValue(props[propName]);
}
this.state = state;
}
static getDerivedStateFromProps(props, state) {
const newState = updateSubscriptionsAndState(state.oldProps, props, state);
if (newState != null) {
return Object.assign(Object.assign({}, newState), { oldProps: props });
}
return { oldProps: props };
}
render() {
const newProps = {};
// Copy over any properties from the observable component to the children.
for (const key in this.state.values) {
if (key !== "children") {
newProps[key] = this.state.values[key];
}
}
if (typeof this.props.children === "function") {
const child = this.props.children;
return child(newProps);
}
else {
const child = React.Children.only(this.props.children);
return React.cloneElement(child, Object.assign(Object.assign({}, child.props), newProps), child.props.children);
}
}
componentDidMount() {
this.updateSubscriptionsAndStateAfterRender();
}
componentDidUpdate() {
this.updateSubscriptionsAndStateAfterRender();
if (this.props.onUpdate) {
this.props.onUpdate();
}
}
componentWillUnmount() {
// Unsubscribe from any of the observable properties.
for (const propName in this.subscribedProps) {
this.unsubscribe(propName, this.subscribedProps);
}
}
subscribe(propName, props) {
if (propName !== "children") {
let observableExpression;
let observableValue = props[propName];
let action;
// If this is an observableExpression, we need to subscribe to the value
// and execute the filter on changes.
if (observableValue && observableValue.observableValue !== undefined) {
observableExpression = observableValue;
observableValue = observableExpression.observableValue;
action = observableExpression.action;
}
if (ObservableLike.isObservable(observableValue)) {
const delegate = this.onValueChanged.bind(this, propName, observableValue, observableExpression);
ObservableLike.subscribe(observableValue, delegate, action);
this.subscriptions[propName] = { delegate, action };
}
}
}
unsubscribe(propName, props) {
if (propName !== "children") {
const observableValue = getObservableValue(props[propName]);
if (ObservableLike.isObservable(observableValue)) {
const subscription = this.subscriptions[propName];
ObservableLike.unsubscribe(observableValue, subscription.delegate, subscription.action);
delete this.subscriptions[propName];
}
}
}
updateSubscriptionsAndStateAfterRender() {
const newState = updateSubscriptionsAndState(this.subscribedProps, this.props, this.state, this);
if (newState != null) {
this.setState(newState);
}
this.subscribedProps = Object.assign({}, this.props);
}
onValueChanged(propName, observableValue, observableExpression, value, action) {
let setState = true;
if (!(propName in this.subscriptions)) {
return;
}
// If this is an ObservableExpression we will call the filter before setting state.
if (observableExpression && observableExpression.filter) {
setState = observableExpression.filter(value, action);
}
if (setState) {
this.setState((prevState, props) => {
return {
values: Object.assign(Object.assign({}, prevState.values), { [propName]: observableValue.value || value })
};
});
}
}
}
function getObservableValue(propValue) {
if (propValue && propValue.observableValue !== undefined) {
return propValue.observableValue;
}
return propValue;
}
function getPropValue(propValue) {
return ObservableLike.getValue(getObservableValue(propValue));
}
function updateSubscriptionsAndState(oldProps, newProps, state, component) {
// We need to unsubscribe from any observable values on old props and
// subscribe to any observable values on new props.
// In addition, if any of the values of the observables on the new props
// differ from the value on the state, then we need to update the state.
// This is possible if the value of the observable changed while the value
// was being rendered, but before we had set up the subscription.
// If we want to unsubscribe/resubscribe, then a component should be passed,
// since this method is always called statically.
const newState = Object.assign({}, state);
let stateChanged = false;
if (oldProps) {
for (const propName in oldProps) {
const oldValue = getObservableValue(oldProps[propName]);
const newValue = getObservableValue(newProps[propName]);
if (oldValue !== newValue) {
component && component.unsubscribe(propName, oldProps);
if (newValue === undefined) {
delete newState.values[propName];
stateChanged = true;
}
}
}
}
for (const propName in newProps) {
const oldValue = oldProps && getObservableValue(oldProps[propName]);
const newValue = getObservableValue(newProps[propName]);
if (oldValue !== newValue) {
component && component.subscribe(propName, newProps);
// Look for changes in the observables between creation and now.
if (state.values[propName] !== getPropValue(newValue)) {
newState.values[propName] = getPropValue(newValue);
stateChanged = true;
}
}
}
// If any state updates occurred update the state now.
if (stateChanged) {
return newState;
}
return null;
}
/**
* Handles subscription to properties that are IObservableValues, so that components don't have to handle on their own.
*
* Usage:
*
* <Observer myObservableValue={observableValue}>
* {(props: {myObservableValue: string}) =>
* <MyComponent myObservableValue={props.myObservableValue} />
* }
* </Observer>
*
* Your component will get re-rendered with the new value of myObservableValue whenever that value changes.
*/
export class Observer extends ObserverBase {
}
/**
* UncheckedObserver is like Observer, except that it performs less (no) typechecking on the child observer function,
* and allows child React elements.
*
* Usage:
*
* <Observer myObservableValue={observableValue}>
* {(props: {myObservableValue: string}) =>
* <MyComponent myObservableValue={props.myObservableValue} />
* }
* </Observer>
*
* -or-
*
* <Observer myObservableValue={observableValue}>
* <MyComponent myObservableValue='' />
* </Observer>
*
* Your component will get re-rendered with the new value of myObservableValue whenever that value changes.
* Additionally, any additional props set on the Observer will also get passed down.
*/
export class UncheckedObserver extends ObserverBase {
}