react-jsonschema-form icon indicating copy to clipboard operation
react-jsonschema-form copied to clipboard

Material-ui - field type `data-url`

Open hoetmaaiers opened this issue 3 years ago • 10 comments

Prerequisites

Description

I am using the material-ui theme, which I love to have available. Only no proper data-url field Widget is present in Material-ui.

Because of this the title property is not rendered for material-ui, type: data-url. Is this on purpose and am I missing some functionality?

Version

├─ @rjsf/[email protected] └─ @rjsf/[email protected]

hoetmaaiers avatar Aug 20 '20 09:08 hoetmaaiers

Any feedback possible on this issue?

hoetmaaiers avatar Sep 16 '20 10:09 hoetmaaiers

I forgot to provide playground link:

In the playground https://rjsf-team.github.io/react-jsonschema-form/ when choosing the example named 'Files' and 'material-ui' as theme, the problem is visible.

Also semantic-ui and fluent-ui experience this problem.

The screenshots show Material-UI and Bootstrap 4. In Bootstrap the tile field is rendered, in Material-UI nog title field is present. h7oun sN1uS

hoetmaaiers avatar Sep 24 '20 12:09 hoetmaaiers

Any feedback on how to solve this?

hoetmaaiers avatar Oct 01 '20 10:10 hoetmaaiers

I've been having this problem as well, I've been able to use the "ui:help" helper text as a work around in some places, but it would be great to have the label show up for file inputs.

OliverLeighC avatar Oct 07 '20 17:10 OliverLeighC

Hello @hoetmaaiers did you solve this issue. Also do you know how to change 'choose file' and 'no files selected' ?? I am planning to use 'Select your photo' instead

jonra1993 avatar Oct 16 '20 04:10 jonra1993

No sorry, I went with using the basic HTML form and add some basic css to this form to match the application.

hoetmaaiers avatar Oct 19 '20 05:10 hoetmaaiers

A true solution, update on the material-theme or some hints on how to solve the bug would still be great.

hoetmaaiers avatar Oct 19 '20 05:10 hoetmaaiers

I ended up creating a custom widget using the functions from the core package FileWidget

import React, { useState, useRef, useEffect } from "react";
import { WidgetProps } from "@rjsf/core";
import FormLabel from "@material-ui/core/FormLabel";
import List from "@material-ui/core/List";
import Input from "@material-ui/core/Input";
import ListItemText from "@material-ui/core/ListItemText";
import ListItemIcon from "@material-ui/core/ListItemIcon";
import ListItem from "@material-ui/core/ListItem";
import DescriptionIcon from "@material-ui/icons/Description";
import { dataURItoBlob } from "../../../core/src/utils";

interface FileInfo {
  name: string;
  size: number;
  type: string;
}

interface Props {
  filesInfo?: FileInfo[];
}

function addNameToDataURL(dataURL: any, name: any) {
  return dataURL.replace(";base64", `;name=${encodeURIComponent(name)};base64`);
}

function processFile(file: any) {
  const { name, size, type } = file;
  return new Promise((resolve, reject) => {
    const reader = new window.FileReader();
    reader.onerror = reject;
    reader.onload = event => {
      resolve({
        dataURL: addNameToDataURL(event.target?.result, name),
        name,
        size,
        type,
      });
    };
    reader.readAsDataURL(file);
  });
}

function processFiles(files: any) {
  return Promise.all([].map.call(files, processFile));
}

function extractFileInfo(dataURLs: any[]) {
  return dataURLs
    .filter(dataURL => typeof dataURL !== "undefined")
    .map(dataURL => {
      const { blob, name } = dataURItoBlob(dataURL);
      return {
        name: name,
        size: blob.size,
        type: blob.type,
      };
    });
}

const FilesInfo = ({ filesInfo }: Props) => {
  if (!filesInfo || filesInfo.length === 0) {
    return null;
  }
  return (
    <List id="file-info">
      {filesInfo.map((fileInfo: any, key: any) => {
        const { name, size, type } = fileInfo;
        return (
          <ListItem key={key}>
            <ListItemIcon>
              <DescriptionIcon />
            </ListItemIcon>
            <ListItemText primary={name} secondary={`${type}, ${size} bytes`} />
          </ListItem>
        );
      })}
    </List>
  );
};

const FileWidget = ({
  id,
  schema,
  options,
  value,
  required,
  disabled,
  readonly,
  label,
  multiple,
  autofocus,
  onChange,
}: WidgetProps) => {
  const [state, setState] = useState<FileInfo[]>();
  const inputRef = useRef();

  useEffect(() => {
    const values = Array.isArray(value) ? value : [value];
    const initialFilesInfo: FileInfo[] = extractFileInfo(values);
    if (initialFilesInfo.length > 0) {
      setState(initialFilesInfo);
    }
  }, [value]);

  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    processFiles(event.target.files).then((filesInfo: any) => {
      setState(filesInfo);
      const values = filesInfo.map((fileInfo: any) => fileInfo.dataURL);
      if (multiple) {
        onChange(values);
      } else {
        onChange(values[0]);
      }
    });
  };

  return (
    <>
      <FormLabel required={required} htmlFor={id}>
        {label || schema.title}
      </FormLabel>
      <Input
        ref={inputRef.current}
        id={id}
        type="file"
        disabled={readonly || disabled}
        onChange={handleChange}
        autoFocus={autofocus}
        multiple={multiple}
        accept={options.accept as string}
      />
      <FilesInfo filesInfo={state} />
    </>
  );
};

export default FileWidget;

was going to make a PR but was having issues installing packages for this repo (might be too much for my laptop)

OliverLeighC avatar Oct 19 '20 13:10 OliverLeighC

My custom FileWidget

import React, { Component } from "react"; import PropTypes from "prop-types";

import AttachFile from "@material-ui/icons/AttachFile"; import { Fab, Button } from "@material-ui/core";

import { dataURItoBlob, shouldRender } from "@rjsf/core/lib/utils";

function addNameToDataURL(dataURL, name) { return dataURL.replace(";base64", ;name=${encodeURIComponent(name)};base64); }

function processFile(file) { const { name, size, type } = file; return new Promise((resolve, reject) => { const reader = new window.FileReader(); reader.onerror = reject; reader.onload = event => { resolve({ dataURL: addNameToDataURL(event.target.result, name), name, size, type, }); }; reader.readAsDataURL(file); }); }

function processFiles(files) { return Promise.all([].map.call(files, processFile)); }

function FilesInfo(props) { const { filesInfo } = props; if (filesInfo.length === 0) { return null; } return ( <ul className="file-info"> {filesInfo.map((fileInfo, key) => { const { name, size, type } = fileInfo; return (

  • {name} ({type}, {size} bytes)
  • ); })} ); }

    function extractFileInfo(dataURLs) { return dataURLs .filter(dataURL => typeof dataURL !== "undefined") .map(dataURL => { const { blob, name } = dataURItoBlob(dataURL); return { name: name, size: blob.size, type: blob.type, }; }); }

    class FileWidget extends Component { constructor(props) { super(props); const { value } = props; const values = Array.isArray(value) ? value : [value]; this.state = { values, filesInfo: extractFileInfo(values) }; }

    shouldComponentUpdate(nextProps, nextState) { return shouldRender(this, nextProps, nextState); }

    onChange = event => { const { multiple, onChange } = this.props; processFiles(event.target.files).then(filesInfo => { const state = { values: filesInfo.map(fileInfo => fileInfo.dataURL), filesInfo, }; this.setState(state, () => { if (multiple) { onChange(state.values); } else { onChange(state.values[0]); } }); }); };

    render() { const { multiple, id, readonly, disabled, autofocus, options, label } = this.props; console.log('this.props', this.props) const { filesInfo } = this.state; return (

          <label htmlFor={id}>
            <input
              style={{ display: "none" }}
              ref={ref => (this.inputRef = ref)}
              id={id}
              name={id}
              type="file"
              disabled={readonly || disabled}
              onChange={this.onChange}
              defaultValue=""
              autoFocus={autofocus}
              multiple={multiple}
              accept={options.accept}
            />
            <Button color="primary" variant="contained" component="span">
              <AttachFile /> {label}
          </Button>{" "}
          </label>
        </p>
        <FilesInfo filesInfo={filesInfo} />
      </div>
    );
    

    } }

    FileWidget.defaultProps = { autofocus: false, };

    if (process.env.NODE_ENV !== "production") { FileWidget.propTypes = { multiple: PropTypes.bool, value: PropTypes.oneOfType([ PropTypes.string, PropTypes.arrayOf(PropTypes.string), ]), autofocus: PropTypes.bool, }; }

    export default FileWidget;

    MedinaGitHub avatar Nov 02 '20 18:11 MedinaGitHub

    This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Please leave a comment if this is still an issue for you. Thank you.

    stale[bot] avatar Aug 03 '22 19:08 stale[bot]

    Still an issue for me after installing latest react-jsonschema-form through npm. gh-pages showcase also affected.

    JannikGM avatar Aug 12 '22 12:08 JannikGM

    @heath-freenome regarding "needs playground link":

    It was already mentioned by @hoetmaaiers in their 3rd post in this issue:

    I forgot to provide playground link:

    In the playground https://rjsf-team.github.io/react-jsonschema-form/ when choosing the example named 'Files' and 'material-ui' as theme, the problem is visible.

    Currently, the playground URL gives 404, but I assume it will still be a problem when it comes back online, as no fix was referenced yet.

    JannikGM avatar Sep 22 '22 08:09 JannikGM