Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

<Edit /> submit button is offering to submit while the form is still in pristine state and hasn't been edited #4733

Closed
kopax opened this issue Apr 28, 2020 · 10 comments · Fixed by #4904

Comments

@kopax
Copy link
Contributor

kopax commented Apr 28, 2020

What you were expecting:

I expect the submit button to be disabled if the form is still in pristine state

What happened instead:

The submit button is always enabled

Steps to reproduce:

https://marmelab.com/react-admin-demo/#/customers/129

Related code:

This is how a react-final-form submit button can solve the issue

<Button
-  disabled={submitting}
+  disabled={submitting || pristine}
/>

Environment

  • React-admin version: 3.4.3
  • Last version that did not exhibit the issue (if applicable):
  • React version: 16.13.1
  • Browser: chrone
  • Stack trace (in case of a JS error):
@fzaninotto
Copy link
Member

Nice idea for an enhancement. Feel free to open a PR to implement it.

@kopax
Copy link
Contributor Author

kopax commented Apr 28, 2020

The render method of <Form /> from React Final Form is actually passing the submitting and pristine in the same render method so that shouldn't be too hard.

I am currently reading the Button.tsx and I found that this may do that trick, but since I am less familiar with the source, I'd like to get your opinion first:

import React, { FC, ReactElement, SyntheticEvent, ReactNode } from 'react';
import PropTypes from 'prop-types';
import {
    Button as MuiButton,
    Tooltip,
    IconButton,
    useMediaQuery,
    makeStyles,
    PropTypes as MuiPropTypes,
} from '@material-ui/core';
import { ButtonProps as MuiButtonProps } from '@material-ui/core/Button';
import { Theme } from '@material-ui/core';
import classnames from 'classnames';
import { Record, RedirectionSideEffect, useTranslate } from 'ra-core';

/**
 * A generic Button with side icon. Only the icon is displayed on small screens.
 *
 * The component translates the label. Pass the icon as child.
 * The icon displays on the left side of the button by default. Set alignIcon prop to 'right' to inverse.
 *
 * @example
 *
 * <Button label="Edit" color="secondary" onClick={doEdit}>
 *   <ContentCreate />
 * </Button>
 *
 */
const Button: FC<ButtonProps> = props => {
    const {
        alignIcon = 'left',
        children,
        classes: classesOverride,
        className,
        color,
        disabled,
        label,
        size,
        ...rest
    } = props;
    const translate = useTranslate();
    const classes = useStyles(props);
    const isXSmall = useMediaQuery((theme: Theme) =>
        theme.breakpoints.down('xs')
    );
-    const restProps = sanitizeButtonRestProps(rest);
+    const { pristine, ...restProps } = sanitizeButtonRestProps(rest);

    return isXSmall ? (
        label && !disabled ? (
            <Tooltip title={translate(label, { _: label })}>
                <IconButton
                    aria-label={translate(label, { _: label })}
                    className={className}
                    color={color}
                    {...restProps}
                >
                    {children}
                </IconButton>
            </Tooltip>
        ) : (
            <IconButton
                className={className}
                color={color}
                disabled={disabled}
                {...restProps}
            >
                {children}
            </IconButton>
        )
    ) : (
        <MuiButton
            className={classnames(classes.button, className)}
            color={color}
            size={size}
            aria-label={label ? translate(label, { _: label }) : undefined}
-            disabled={disabled}
+            disabled={disabled || pristine}
            {...restProps}
        >
            {alignIcon === 'left' &&
                children &&
                React.cloneElement(children, {
                    className: classes[`${size}Icon`],
                })}
            {label && (
                <span
                    className={classnames({
                        [classes.label]: alignIcon === 'left',
                        [classes.labelRightIcon]: alignIcon !== 'left',
                    })}
                >
                    {translate(label, { _: label })}
                </span>
            )}
            {alignIcon === 'right' &&
                children &&
                React.cloneElement(children, {
                    className: classes[`${size}Icon`],
                })}
        </MuiButton>
    );
};

const useStyles = makeStyles(
    {
        button: {
            display: 'inline-flex',
            alignItems: 'center',
        },
        label: {
            paddingLeft: '0.5em',
        },
        labelRightIcon: {
            paddingRight: '0.5em',
        },
        smallIcon: {
            fontSize: 20,
        },
        mediumIcon: {
            fontSize: 22,
        },
        largeIcon: {
            fontSize: 24,
        },
    },
    { name: 'RaButton' }
);

interface Props {
    alignIcon?: 'left' | 'right';
    children?: ReactElement;
    classes?: object;
    className?: string;
    color?: MuiPropTypes.Color;
    component?: ReactNode;
    to?: string | { pathname: string; search: string };
    disabled?: boolean;
    label?: string;
    size?: 'small' | 'medium' | 'large';
    icon?: ReactElement;
    onClick?: (e: MouseEvent) => void;
    redirect?: RedirectionSideEffect;
    variant?: string;
    // May be injected by Toolbar
    basePath?: string;
    handleSubmit?: (event?: SyntheticEvent<HTMLFormElement>) => Promise<Object>;
    handleSubmitWithRedirect?: (redirect?: RedirectionSideEffect) => void;
    invalid?: boolean;
    onSave?: (values: object, redirect: RedirectionSideEffect) => void;
    saving?: boolean;
    submitOnEnter?: boolean;
    pristine?: boolean;
    record?: Record;
    resource?: string;
    undoable?: boolean;
}

export type ButtonProps = Props & MuiButtonProps;

export const sanitizeButtonRestProps = ({
    // The next props are injected by Toolbar
    basePath,
    handleSubmit,
    handleSubmitWithRedirect,
    invalid,
    onSave,
-    pristine,
    record,
    redirect,
    resource,
    saving,
    submitOnEnter,
    undoable,
    ...rest
}: any) => rest;

Button.propTypes = {
    alignIcon: PropTypes.oneOf(['left', 'right']),
    children: PropTypes.element,
    classes: PropTypes.object,
    className: PropTypes.string,
    color: PropTypes.oneOf(['default', 'inherit', 'primary', 'secondary']),
    disabled: PropTypes.bool,
    label: PropTypes.string,
    size: PropTypes.oneOf(['small', 'medium', 'large']),
};

Button.defaultProps = {
    color: 'primary',
    size: 'small',
};

export default Button;

Do you think that would be enough? I am unsure if pristine should be directly picked from props or from rest...

I am also not seing the submitting from react-final-form.

@WiXSL
Copy link
Contributor

WiXSL commented Apr 28, 2020

@kopax,
Be careful, sanitizeButtonRestProps is exported and used in other components, if you remove pristine from sanitizeButtonRestProps it will end up in some components that are currently being properly sanitized.

I would take pristine from props directly

@kopax
Copy link
Contributor Author

kopax commented Apr 28, 2020

I have tried but the value is always undefined in the simple app. I also have checked the occurence of <Form\n and <Form in the source code and there's just one in FilterForm. I'm unsure from where it should get propagated. Do you have another clue?

@WiXSL
Copy link
Contributor

WiXSL commented Apr 28, 2020

I can't look into it right now. Sorry.

@kopax
Copy link
Contributor Author

kopax commented Apr 28, 2020

No worries, I can't tell how the button is created within the form nor where is the form, I've checked SimpleForm, it's a bit magical. I won't be surprised if that props was removed by a sanitizeRestProps up in the tree, I'll wait till I get some further advices. Thank you

@fzaninotto
Copy link
Member

Fixed by #4773

@kopax
Copy link
Contributor Author

kopax commented May 26, 2020

Yes, and I am grateful to @WiXSL and that you accepted with welcoming the feature.
I am since watching for the branch next next release, do you have a hint on when this may happen?

@fzaninotto
Copy link
Member

Minor versions are released monthly. See the Milestones page for a schedule of the release dates.

@kopax
Copy link
Contributor Author

kopax commented Jun 6, 2020

Now that 3.6.0 is out, I am testing it in our back office app, and unfortunately, this is not working out of the box. The pristine props you are passing to <SaveButton disabled={ submitting || pristine} /> is always undefined because the <Toolbar /> is not passing it to <SaveButton />,

I have submitted #4904 that fixes it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants