[RN] Power to Dialog

* Implement disabling buttons (like Web had the ability).
* Use consistent colors (e.g. for the buttons) like the rest of the app
  (e.g. WelcomePage).
* Enable AbstractDialog to await a thenable from onSubmit and Dialog to
  render a LoadingIndicator in place of the OK/submit button text.
This commit is contained in:
Lyubo Marinov 2017-09-22 15:09:15 -05:00
parent 1ea62215f6
commit a12984ed6f
3 changed files with 195 additions and 55 deletions

View File

@ -37,8 +37,33 @@ export default class AbstractDialog extends Component {
constructor(props) {
super(props);
this.state = {
};
this._onCancel = this._onCancel.bind(this);
this._onSubmit = this._onSubmit.bind(this);
this._onSubmitFulfilled = this._onSubmitFulfilled.bind(this);
this._onSubmitRejected = this._onSubmitRejected.bind(this);
}
/**
* Implements React's {@link Component#componentWillMount()}. Invoked
* immediately before mounting occurs.
*
* @inheritdoc
*/
componentWillMount() {
this._mounted = true;
}
/**
* Implements React's {@link Component#componentWillUnmount()}. Invoked
* immediately before this component is unmounted and destroyed.
*
* @inheritdoc
*/
componentWillUnmount() {
this._mounted = false;
}
/**
@ -48,25 +73,79 @@ export default class AbstractDialog extends Component {
* @returns {void}
*/
_onCancel() {
const { onCancel } = this.props;
const { cancelDisabled, onCancel } = this.props;
if (!onCancel || onCancel()) {
if ((typeof cancelDisabled === 'undefined' || !cancelDisabled)
&& (!onCancel || onCancel())) {
this.props.dispatch(hideDialog());
}
}
/**
* Dispatches a redux action to hide this dialog when it's submitted.
* Submits this dialog. If the React <tt>Component</tt> prop
* <tt>onSubmit</tt> is defined, the function that is the value of the prop
* is invoked. If the function returns a <tt>thenable</tt>, then the
* resolution of the <tt>thenable</tt> is awaited. If the submission
* completes successfully, a redux action will be dispatched to hide this
* dialog.
*
* @private
* @param {string} value - The submitted value if any.
* @returns {void}
*/
_onSubmit(value) {
const { onSubmit } = this.props;
const { okDisabled, onSubmit } = this.props;
if (!onSubmit || onSubmit(value)) {
this.props.dispatch(hideDialog());
if (typeof okDisabled === 'undefined' || !okDisabled) {
this.setState({ submitting: true });
// Invoke the React Compnent prop onSubmit if any.
const r = !onSubmit || onSubmit(value);
// If the invocation returns a thenable, await its resolution;
// otherwise, treat the return value as a boolean indicating whether
// the submission has completed successfully.
let then;
if (r) {
switch (typeof r) {
case 'function':
case 'object':
then = r.then;
break;
}
}
if (typeof then === 'function' && then.length === 2) {
then.call(r, this._onSubmitFulfilled, this._onSubmitRejected);
} else if (r) {
this._onSubmitFulfilled();
} else {
this._onSubmitRejected();
}
}
}
/**
* Notifies this <tt>AbstractDialog</tt> that it has been submitted
* successfully. Dispatches a redux action to hide this dialog after it has
* been submitted.
*
* @private
* @returns {void}
*/
_onSubmitFulfilled() {
this._mounted && this.setState({ submitting: false });
this.props.dispatch(hideDialog());
}
/**
* Notifies this <tt>AbstractDialog</tt> that its submission has failed.
*
* @private
* @returns {void}
*/
_onSubmitRejected() {
this._mounted && this.setState({ submitting: false });
}
}

View File

@ -1,12 +1,31 @@
import PropTypes from 'prop-types';
import React from 'react';
import { TextInput } from 'react-native';
import { StyleSheet, TextInput } from 'react-native';
import Prompt from 'react-native-prompt';
import { connect } from 'react-redux';
import { translate } from '../../i18n';
import { LoadingIndicator } from '../../react';
import { set } from '../../redux';
import AbstractDialog from './AbstractDialog';
import styles from './styles';
/**
* The value of the style property {@link _TAG_KEY} which identifies the
* OK/submit button of <tt>Prompt</tt>.
*/
const _SUBMIT_TEXT_TAG_VALUE = '_SUBMIT_TEXT_TAG_VALUE';
/**
* The name of the style property which identifies ancestors of <tt>Prompt</tt>
* such as its OK/submit button for the purposes of workarounds implemented by
* <tt>Dialog</tt>.
*
* XXX The value may trigger a react-native warning in the Debug configuration
* but, unfortunately, I couldn't find a value that wouldn't.
*/
const _TAG_KEY = '_TAG_KEY';
/**
* Implements <tt>AbstractDialog</tt> on react-native using <tt>Prompt</tt>.
@ -37,7 +56,6 @@ class Dialog extends AbstractDialog {
bodyKey,
cancelDisabled,
cancelTitleKey = 'dialog.Cancel',
children,
okDisabled,
okTitleKey = 'dialog.Ok',
t,
@ -45,30 +63,82 @@ class Dialog extends AbstractDialog {
titleString
} = this.props;
/* eslint-disable react/jsx-wrap-multilines */
const cancelButtonTextStyle
= cancelDisabled ? styles.disabledButtonText : styles.buttonText;
let submitButtonTextStyle
= okDisabled ? styles.disabledButtonText : styles.buttonText;
let element
= <Prompt
cancelText = { cancelDisabled ? undefined : t(cancelTitleKey) }
submitButtonTextStyle = {
...submitButtonTextStyle,
[_TAG_KEY]: _SUBMIT_TEXT_TAG_VALUE
};
// eslint-disable-next-line no-extra-parens
let element = (
<Prompt
cancelButtonTextStyle = { cancelButtonTextStyle }
cancelText = { t(cancelTitleKey) }
onCancel = { this._onCancel }
onSubmit = { this._onSubmit }
placeholder = { t(bodyKey) }
submitText = { okDisabled ? undefined : t(okTitleKey) }
submitButtonTextStyle = { submitButtonTextStyle }
submitText = { t(okTitleKey) }
title = { titleString || t(titleKey) }
visible = { true } />;
visible = { true } />
);
/* eslint-enable react/jsx-wrap-multilines */
// XXX The following implements workarounds with knowledge of
// react-native-prompt/Prompt's implementation.
if (React.Children.count(children)) {
// XXX The following implements a workaround with knowledge of the
// implementation of react-native-prompt.
element
= this._replaceFirstElementOfType(
// eslint-disable-next-line no-extra-parens, new-cap
(new (element.type)(element.props)).render(),
TextInput,
children);
}
// eslint-disable-next-line no-extra-parens, new-cap
element = (new (element.type)(element.props)).render();
let { children } = this.props;
children = React.Children.count(children) ? children : undefined;
// eslint-disable-next-line no-shadow
element = this._mapReactElement(element, element => {
// * If this Dialog has children, they are to be rendered instead of
// Prompt's TextInput.
if (element.type === TextInput) {
if (children) {
element = children; // eslint-disable-line no-param-reassign
children = undefined;
}
} else {
let { style } = element.props;
if (style
&& (style = StyleSheet.flatten(style))
&& _TAG_KEY in style) {
switch (style[_TAG_KEY]) {
case _SUBMIT_TEXT_TAG_VALUE:
if (this.state.submitting) {
// * If this Dialog is submitting, render a
// LoadingIndicator.
return (
<LoadingIndicator
color = { submitButtonTextStyle.color }
size = { 'small' } />
);
}
break;
}
// eslint-disable-next-line no-param-reassign
element
= React.cloneElement(
element,
/* props */ {
style: set(style, _TAG_KEY, undefined)
},
...React.Children.toArray(element.props.children));
}
}
return element;
});
return element;
}
@ -108,36 +178,6 @@ class Dialog extends AbstractDialog {
return mapped;
}
/**
* Replaces the first <tt>ReactElement</tt> of a specific type found in a
* specific <tt>ReactElement</tt> tree with a specific replacement
* <tt>ReactElement</tt>.
*
* @param {ReactElement} element - The <tt>ReactElement</tt> tree to search
* through and replace in.
* @param {*} type - The type of the <tt>ReactElement</tt> to be replaced.
* @param {ReactElement} replacement - The <tt>ReactElement</tt> to replace
* the first <tt>ReactElement</tt> in <tt>element</tt> of the specified
* <tt>type</tt>.
* @private
* @returns {ReactElement}
*/
_replaceFirstElementOfType(element, type, replacement) {
// eslint-disable-next-line no-shadow
return this._mapReactElement(element, element => {
if (replacement && element.type === type) {
/* eslint-disable no-param-reassign */
element = replacement;
replacement = undefined;
/* eslint-enable no-param-reassign */
}
return element;
});
}
}
export default translate(connect()(Dialog));

View File

@ -0,0 +1,21 @@
import { ColorPalette, createStyleSheet } from '../../styles';
/**
* The React <tt>Component</tt> styles of the feature base/dialog.
*/
export default createStyleSheet({
/**
* The style of the <tt>Text</tt> in a <tt>Dialog</tt> button.
*/
buttonText: {
color: ColorPalette.blue
},
/**
* The style of the <tt>Text</tt> in a <tt>Dialog</tt> button which is
* disabled.
*/
disabledButtonText: {
color: ColorPalette.darkGrey
}
});