UNPKG

azure-devops-ui

Version:

React components for building web UI in Azure DevOps

210 lines (209 loc) 8.53 kB
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 { }