From 489abc82a4d48631e760b8637b4260c8ce1c346b Mon Sep 17 00:00:00 2001 From: David Bonnet Date: Mon, 4 Feb 2019 10:35:05 +0100 Subject: [PATCH] feat(tools): add `resilientProp` decorator BREAKING CHANGE: `promisedProp` and `promised` do not keep the last resolved value on promise update. Use `resilientProp` and `resilient` to enable this feature. --- src/tools.js | 51 ++++++++++++++++++++++++++++++++++++++++----------- src/values.js | 23 ++++++++++++++++++++++- 2 files changed, 62 insertions(+), 12 deletions(-) diff --git a/src/tools.js b/src/tools.js index a662ec3f..11c170c1 100644 --- a/src/tools.js +++ b/src/tools.js @@ -456,35 +456,39 @@ export function lazyProperty(object, propertyName, valueBuilder) { export function promisedProp(name) { /* - Takes the promise from the prop `[name]` and injects prop `[name]` with `{ done, error, value }`. + Replaces the promise at prop `[name]` with `{ done, error, value }`. Before the promise resolves, `done` is `false`, and becomes `true` afterwards. If an error occured in the promise, `error` is set to it. Otherwise, the `value` is set to the resolved value. - If a new promise is provided to `[name]`, the previously resolved `value` is kept until the new one resolves. + If the propmise at prop `[name]` changes, `done`, `error`, and `value` are reset and any previous promise is discarded. */ return Component => class promised extends BaseComponent { constructor(props) { super(props) - const promise = props[name] - this.state = this.constructor.getDerivedStateFromProps(props) + this.state = this.constructor.getDerivedStateFromProps( + props, + EMPTY_OBJECT, + ) this.mounted = false - this.attachPromise(promise) } attachPromise(promise) { + if (promise == null) { + return + } return Promise.resolve(promise).then( value => { if (!this.mounted || this.state.promise !== promise) { return } - this.setState({ resource: { done: true, error: null, value } }) + this.setState({ result: { done: true, error: null, value } }) }, error => { if (!this.mounted || this.state.promise !== promise) { return } this.setState({ - resource: { done: true, error, value: this.state.resource.value }, + result: { done: true, error, value: null }, }) }, ) @@ -492,19 +496,20 @@ export function promisedProp(name) { componentDidMount() { this.mounted = true + this.attachPromise(this.state.promise) } static getDerivedStateFromProps(props, state) { const promise = props[name] - if (state && state.promise === promise) { + if (promise === state.promise && state !== EMPTY_OBJECT) { return null } return { promise, - resource: { + result: { done: false, error: null, - value: state ? state.resource.value : null, + value: null, }, } } @@ -523,7 +528,31 @@ export function promisedProp(name) { render() { return $(Component, { ...this.props, - [name]: this.state.resource, + [name]: this.state.result, + }) + } + } +} + +export function resilientProp(name) { + /* + Keeps the last non-`nil` value of prop `[name]`. + */ + return Component => + class resiliant extends BaseComponent { + constructor(props) { + super(props) + this.state = { [name]: props[name] } + } + + static getDerivedStateFromProps(props, state) { + const value = props[name] + return value === state.value || value == null ? null : { value } + } + render() { + return $(Component, { + ...this.props, + [name]: this.state.value, }) } } diff --git a/src/values.js b/src/values.js index 02e12a02..a388301e 100644 --- a/src/values.js +++ b/src/values.js @@ -10,6 +10,8 @@ import { editableProp, cycledProp, promisedProp, + resilientProp, + EMPTY_OBJECT, } from './tools' export const defaultValue = Component => @@ -34,8 +36,16 @@ export const transformable = compose( hasProp('transformValue'), Component => class transformable extends BaseComponent { + constructor(props) { + super(props) + this.state = this.constructor.getDerivedStateFromProps( + props, + EMPTY_OBJECT, + ) + } + static getDerivedStateFromProps({ value, transformValue }, state) { - return state && value === state.value + return value === state.value && state !== EMPTY_OBJECT ? null : { transformedValue: transformValue(value, state), @@ -142,8 +152,19 @@ export const cyclable = branch( }), ) +/* +Replaces the promise at prop `value` with `{ done, error, value }`. +Before the promise resolves, `done` is `false`, and becomes `true` afterwards. +If an error occured in the promise, `error` is set to it. Otherwise, the `value` is set to the resolved value. +If the promise at prop `value` changes, `done`, `error`, and `value` are reset and any previous promise is discarded. +*/ export const promised = promisedProp('value') +/* +Keeps the last non-`nil` value of prop `value`. +*/ +export const resilient = resilientProp('value') + export const toggledEditing = branch( /* Sets the `editing` prop and enables its toggling through the `onToggleEditing()` prop.