React-native-ui-kitten: [feature request] A loading state and animation for buttons

Created on 5 Aug 2019  ยท  10Comments  ยท  Source: akveo/react-native-ui-kitten

Could be useful to have a laoding state for buttons.

Proposal Components

Most helpful comment

Hi,

This workaround, work for me:

I'm use the prop icon -> https://github.com/akveo/react-native-ui-kitten/blob/master/src/framework/ui/button/button.component.tsx#L274

import {
  Button,
} from 'react-native-ui-kitten';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import {
  ActivityIndicator,
} from 'react-native';

export default class ScButton extends Component {
  static get defaultProps() {
    return {
      loading: false,
      loadingColor: '#FFF',
      loadingSize: 'small',
      children: null,
    };
  }

  static get propTypes() {
    return {
      loading: PropTypes.bool,
      loadingColor: PropTypes.string,
      loadingSize: PropTypes.string,
      children: PropTypes.string,
    };
  }

  renderChildren() {
    const {
      loading,
      children,
    } = this.props;

    if (loading) {
      return null;
    }

    return children;
  }

  renderLoading() {
    const {
      loadingColor,
      loadingSize,
    } = this.props;

    return (
      <ActivityIndicator
        color={loadingColor}
        size={loadingSize}
      />
    );
  }

  render() {
    const {
      loading,
    } = this.props;

    const customProps = {};

    if (loading) {
      Object.assign(customProps, {
        icon: () => this.renderLoading(),
        disabled: true,
      });
    }

    return (
      <Button
        {...this.props}
        {...customProps}
      >
        {this.renderChildren()}
      </Button>
    );
  }
}
import ScButton from './ScButton';

<ScButton loading>
  ButtonText
</ScButton>

image

All 10 comments

We could do it with ActivityIndicator component, but eva design system rules does not allow to add anything other than string in buttons, as related in this issue #488

Hi โœ‹
Thanks for the proposal. This is already under discussion within our team :)
Let's keep it open. I'll share a status

Hi,

This workaround, work for me:

I'm use the prop icon -> https://github.com/akveo/react-native-ui-kitten/blob/master/src/framework/ui/button/button.component.tsx#L274

import {
  Button,
} from 'react-native-ui-kitten';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import {
  ActivityIndicator,
} from 'react-native';

export default class ScButton extends Component {
  static get defaultProps() {
    return {
      loading: false,
      loadingColor: '#FFF',
      loadingSize: 'small',
      children: null,
    };
  }

  static get propTypes() {
    return {
      loading: PropTypes.bool,
      loadingColor: PropTypes.string,
      loadingSize: PropTypes.string,
      children: PropTypes.string,
    };
  }

  renderChildren() {
    const {
      loading,
      children,
    } = this.props;

    if (loading) {
      return null;
    }

    return children;
  }

  renderLoading() {
    const {
      loadingColor,
      loadingSize,
    } = this.props;

    return (
      <ActivityIndicator
        color={loadingColor}
        size={loadingSize}
      />
    );
  }

  render() {
    const {
      loading,
    } = this.props;

    const customProps = {};

    if (loading) {
      Object.assign(customProps, {
        icon: () => this.renderLoading(),
        disabled: true,
      });
    }

    return (
      <Button
        {...this.props}
        {...customProps}
      >
        {this.renderChildren()}
      </Button>
    );
  }
}
import ScButton from './ScButton';

<ScButton loading>
  ButtonText
</ScButton>

image

@guilhermelcn good job :) But this will break your build if you'll try do this with TypeScript

I also have a workaround:

const [loading, setLoading] = useState(false);
const toggleLoading = useCallback(() => {
  setLoading(!loading);
}, [loading]);

<Button icon={loading ? () => <ActivityIndicator /> : null} onPress={toggleLoading}>
  Tap me to hide/show loading indicator
</Button>

Hi my approach is a custom button based on the original that have an ActivityIndicator passed from props (similar to react native elements approach). Just have a question @artyorsh how can I put a themed color for make it default color (sorry i'm a bit new in react and ui-kitten ^^)

color={this.props.loadingProps ? this.props.loadingProps.color : 'white'}

type IconProp = (style: ImageStyle) => IconElement;

export interface ButtonProps extends StyledComponentProps, TouchableOpacityProps {
    textStyle?: StyleProp<TextStyle>;
    loadingStyle?: StyleProp<ViewStyle>;
    loadingProps?: ActivityIndicatorProps;
    loading?: boolean;
    icon?: IconProp;
    status?: string;
    size?: string;
    children?: string;
}

export type ButtonElement = React.ReactElement<ButtonProps>;

export class ButtonComponent extends React.Component<ButtonProps> implements WebEventResponderCallbacks {

    static styledComponentName: string = 'Button';

    private webEventResponder: WebEventResponderInstance = WebEventResponder.create(this);

    // WebEventResponderCallbacks

    public onMouseEnter = (): void => {
        if (this.props.dispatch) {
            this.props.dispatch([Interaction.HOVER]);
        }
    };

    public onMouseLeave = (): void => {
        if (this.props.dispatch) {
            this.props.dispatch([]);
        }
    };

    public onFocus = (): void => {
        if (this.props.dispatch) {
            this.props.dispatch([Interaction.FOCUSED]);
        }
    };

    public onBlur = (): void => {
        if (this.props.dispatch) {
            this.props.dispatch([]);
        }
    };

    private onPress = (event: GestureResponderEvent): void => {
        if (this.props.onPress) {
            this.props.onPress(event);
        }
    };

    private onPressIn = (event: GestureResponderEvent): void => {
        if (this.props.dispatch) {
            this.props.dispatch([Interaction.ACTIVE]);
        }

        if (this.props.onPressIn) {
            this.props.onPressIn(event);
        }
    };

    private onPressOut = (event: GestureResponderEvent): void => {
        if (this.props.dispatch) {
            this.props.dispatch([]);
        }

        if (this.props.onPressOut) {
            this.props.onPressOut(event);
        }
    };

    private getComponentStyle = (source: StyleType): StyleType => {
        const {
            textColor,
            textFontFamily,
            textFontSize,
            textLineHeight,
            textFontWeight,
            textMarginHorizontal,
            iconWidth,
            iconHeight,
            iconTintColor,
            iconMarginHorizontal,
            ...containerParameters
        } = source;

        return {
            container: containerParameters,
            text: {
                color: textColor,
                fontFamily: textFontFamily,
                fontSize: textFontSize,
                lineHeight: textLineHeight,
                fontWeight: textFontWeight,
                marginHorizontal: textMarginHorizontal,
            },
            icon: {
                width: iconWidth,
                height: iconHeight,
                tintColor: iconTintColor,
                marginHorizontal: iconMarginHorizontal,
            },
        };
    };

    private renderTextElement = (style: TextStyle): TextElement => {
        return (
            <Text
                key={1}
                style={[style, styles.text, this.props.textStyle]}>
                {this.props.children}
            </Text>
        );
    };

    private renderActivityIndicatorElement = (style: ViewStyle) => {
        return (
            <ActivityIndicator
                style={StyleSheet.flatten([styles.loading, this.props.loadingStyle, style])}
                color={this.props.loadingProps ? this.props.loadingProps.color : 'white'}
                size={this.props.loadingProps ? this.props.loadingProps.size : 'small'}
                {...this.props.loadingProps}
            />
        )
    }

    private renderIconElement = (style: ImageStyle): IconElement | null => {
        if (this.props.icon) {
            const iconElement: IconElement = this.props.icon(style);

            return React.cloneElement(iconElement, {
                key: 2,
                style: [style, styles.icon, iconElement.props.style],
            });
        }
        return null;
    };

    private renderComponentChildren = (style: StyleType): React.ReactNodeArray => {
        const { icon, children } = this.props;

        return [
            icon && this.renderIconElement(style.icon),
            isValidString(children as any) && this.renderTextElement(style.text),
            this.renderActivityIndicatorElement(style.loading),
        ];
    };

    public render(): React.ReactElement<TouchableOpacityProps> {
        const { themedStyle, style, ...containerProps } = this.props;
        const { container, ...childStyles } = this.getComponentStyle(themedStyle as any);
        const [iconElement, textElement, activityElement] = this.renderComponentChildren(childStyles);

        return (
            <TouchableOpacity
                activeOpacity={1.0}
                {...containerProps}
                {...this.webEventResponder.eventHandlers}
                style={[container, styles.container, webStyles.container, style]}
                onPress={this.props.loading ? undefined : this.onPress}
                onPressIn={this.onPressIn}
                onPressOut={this.onPressOut}>
                {!this.props.loading && iconElement}
                {!this.props.loading && textElement}
                {this.props.loading && activityElement}
            </TouchableOpacity>
        );
    }
}

const styles = StyleSheet.create({
    container: {
        flexDirection: 'row',
        justifyContent: 'center',
        alignItems: 'center',
    },
    text: {},
    icon: {},
    loading: {
        marginVertical: 2,
    },
});

const webStyles = Platform.OS === 'web' && StyleSheet.create({
    container: {
        // @ts-ignore
        outlineWidth: 0,
    },
});

export const ButtonCustom = styled<ButtonProps>(ButtonComponent);

@anthowm you can access theme by using withStyles or styled

import { withStyles } from '@ui-kitten/components'

const ScreenComponent = (props) => (
  <ActivityIndicator style={props.themedStyle.indicator}/>
);

export const Screen = withStyles(ScreenComponent, theme => ({
  indicator: { backgroundColor: theme['color-primary-default'] },
}));

Woop. Soon in v5 ๐Ÿ˜„(sorry for fps)

https://i.imgur.com/qE5QHUq.gif

@artyorsh Cool I didn't read the v5 roadmap but looks excited ๐ŸŒŠ . thanks for the job ๐Ÿ’ฏ

Available in v5! ๐ŸŽ‰

Was this page helpful?
0 / 5 - 0 ratings

Related issues

gimli01 picture gimli01  ยท  3Comments

eyalyoli picture eyalyoli  ยท  3Comments

Gitldx picture Gitldx  ยท  3Comments

nonameolsson picture nonameolsson  ยท  3Comments

MScMechatronics picture MScMechatronics  ยท  3Comments