marmelab / react-admin

A frontend Framework for single-page applications on top of REST/GraphQL APIs, using TypeScript, React and Material Design
http://marmelab.com/react-admin
MIT License
24.89k stars 5.23k forks source link

display array with arrayField or other #2383

Closed nacimgoura closed 6 years ago

nacimgoura commented 6 years ago

Hello, I want to display a list of items that are stored in a array ([1,3,4,5,7...]) capture d ecran 2018-10-03 a 15 47 46 With ArrayField I can't display the elements as I want because it expects an object array when I have a simple array. How to display the elements stored in a array ? Currently I am creating my own field for this :

const TagsFieldInstitutes = ({ record }) => (
  <div>
    {record.institutes.map(item => (
      <span key={item} className="chip">
        {item}
      </span>
    ))}
  </div>
);

But I think there is a better way. Thanks you for your help !

TomJannes commented 6 years ago

From my experience it takes less time to wrap the value server side in an object, is this an option? You could make your own chipfield

import React from 'react';
import PropTypes from 'prop-types';
import get from 'lodash/get';
import pure from 'recompose/pure';
import Chip from '@material-ui/core/Chip';
import { withStyles } from '@material-ui/core/styles';
import classnames from 'classnames';
import sanitizeRestProps from './sanitizeRestProps';

const styles = {
    chip: { margin: 4 },
};

export const ChipField = ({
    className,
    classes = {},
    source,
    record = {},
    ...rest
}) => {
    return (
        <Chip
            className={classnames(classes.chip, className)}
            label={source !== null && source !== undefined ? get(record, source) : record}
            {...sanitizeRestProps(rest)}
        />
    );
};

ChipField.propTypes = {
    className: PropTypes.string,
    classes: PropTypes.object,
    elStyle: PropTypes.object,
    sortBy: PropTypes.string,
    source: PropTypes.string,
    record: PropTypes.object,
};

const PureChipField = withStyles(styles)(pure(ChipField));

export default PureChipField;

Afterwards you can do something like

<ArrayField source="institutes">
    <SingleFieldList>
        <ChipField />
    </SingleFieldList>
</ArrayField>

(this code was not tested)

djhi commented 6 years ago

Hi, and thanks for your question. As explained in the react-admin contributing guide, the right place to ask a "How To" question, get usage advice, or troubleshoot your own code, is StackOverFlow.

This makes your question easy to find by the core team, and the developer community. Unlike Github, StackOverFlow has great SEO, gamification, voting, and reputation. That's why we chose it, and decided to keep GitHub issues only for bugs and feature requests.

So I'm closing this issue, and inviting you to ask your question at:

http://stackoverflow.com/questions/tagged/react-admin

And once you get a response, please continue to hang out on the react-admin channel in StackOverflow. That way, you can help newcomers and share your expertise!

afilp commented 5 years ago

@djhi I also had this problem and I lost some time doing conversions in the data provider (all 4 CRUD types need to be converted, otherwise the app gives errors when trying to show/update/delete).

It would be a good feature if both ArrayField and ArrayInput (along with its "siblings") supported the plain Array structure too: [1,2,3,4,5]

Could we mark this as a possible enhancement please? (I can create a new issue if you wish)

P.S. I could not find any related stack-overflow question/answer related to this.

fzaninotto commented 5 years ago

@afilp as explained in the documentation:

If you need to render a collection in a custom way, it's often simpler to write your own component:

const TagsField = ({ record }) => (
    <ul>
        {record.tags.map(item => (
            <li key={item.name}>{item.name}</li>
        ))}
    </ul>
)
TagsField.defaultProps = { addLabel: true };

We won't add a new field to replace a one liner in userland.

afilp commented 5 years ago

@fzaninotto If I may add, it was not a one liner because I had to make changes also for the "C"reate and "U"pdate, because their logic also require an array of objects (instead of a simple array), in their respective ArrayInput. I did not propose to add a new field.

I could put some little effort to create a PR for ArrayField and ArrayInput that also work for arrays with "non-object values", would you care for this? Thanks.

danlupascu commented 5 years ago

I used this solution: https://stackoverflow.com/a/53031676/6744320

paradoxxxzero commented 5 years ago

I ended with a more generic solution:

import { cloneElement } from 'react'

export const StringToLabelObject = ({ record, children, ...rest }) =>
  cloneElement(children, {
    record: { label: record },
    ...rest,
  })
<ArrayField source="institutes">
  <SingleFieldList>
    <StringToLabelObject>
      <ChipField source="label"/>
    </StringToLabelObject>
  </SingleFieldList>
</ArrayField>

but I agree this is quite a common case and should be supported by react-admin.

afilp commented 5 years ago

@paradoxxxzero @danlupascu How do you handle the Create and Update that use the respective input fields?

danlupascu commented 5 years ago

@afilp This is how I did it:


export const UsersEdit = props => (
    <Edit {...props}>
      <SimpleForm>
        <TextInput source="email" />
        <BooleanInput source="isVerified" />
        <TextInput source="verifyToken" />
        <ArrayInput source="roles"><SimpleFormIterator><TextInput/></SimpleFormIterator></ArrayInput>
      </SimpleForm>
    </Edit>
);

In my case I have a field 'roles' that is an array of roles (strings) that the user has.

dantman commented 5 years ago

I cannot comprehend why this has been essentially wontfixed. There is nothing at all strange about an API returning an array of strings like ['a', 'b', 'c']. And it's ridiculous to expect the API to be rewritten to return [{label: 'a'}, {label: 'b'}, {label: 'c'}]. That changes the API response that other clients uses from a sensible response to a nonsensical one.

ajhool commented 5 years ago

I would add that while the Documentation's assumption is that this is just a simple one line solution, the "complexity" of this issue was that I couldn't believe there was not a way to do it within the react admin ecosystem. It took a while to find this issue and be convinced that the React Admin team really does not think that an array ['cat', 'dog', 'man', 'bear'] deserves native support

This is a very surprising design choice IMO and that surprise should be considered when designing the API

fzaninotto commented 5 years ago

Let me reiterate: react-admin doesn't require that you change your API to return an array of objects instead of an array of strings.

For fields, the zero-documentation, pure react way of doing it is documented and simple enough:

const TextArrayField = ({ record, source }) => (
    <>
        {record[source].map(item => <Chip label={item} key={item} />)}
    </>
)
TextArrayField.defaultProps = { addLabel: true };

This must be an additional component, as the current ArrayField is designed to receive an iterator as child. So if we integrated this in the core, we'd have to explain the difference between ArrayField and TextArrayField. It would take you more time to learn about that than to write the component by hand, as above.

For inputs, the solution using <SimpleFormIterator> is also documented and simple enough.

<ArrayInput source="roles">
  <SimpleFormIterator>
    <TextInput/>
  </SimpleFormIterator>
</ArrayInput>      

So it all boils down to: Do you prefer to use react-admin for everything, or are you willing to use React whenever you can? We designed react-admin for the latter case, because we had many, many bad experiences with "frameworks" that force you to re-learn how to code.

We prefer spending our time implementing customer requirements rather than learning tools that partially implement our customer requirements, and that we'll maybe never be able to tweak completely.

dantman commented 5 years ago

I can definitely say that when I had this issue the documentation was not clear enough on what I had to do. I did not find the solution until after piles of source code digging and finding this bug report. If the "documentation" has anything to do with the "If you need to render a collection in a custom way, it's often simpler to write your own component" text – trying to get a chip field react-admin already has to use the array item instead of a property on the array item has nothing to do with "rendering a collection in a custom way" from the user's perspective.

I can also say that when I was reading source code and setting debug breakpoints and got to this line of the ChipField implementation I was incredibly annoyed to see that the component already had exactly the data it needed in record and all it needed was to not get a property from it and there was absolutely nothing I could pass to source to tell it not to read a property off the record. I had to add a multi-line hack to the project because a single line of code in react-admin made the component useless.

zhouhao27 commented 5 years ago

@fzaninotto Modified your code a bit. It works now.

import Chip from '@material-ui/core/Chip'

const TextArrayField = ({ record, source }) => {
  const array = record[source]
  if (typeof array === 'undefined' || array === null || array.length === 0) {
    return <div/>
  } else {
    return (
      <>
        {array.map(item => <Chip label={item} key={item}/>)}
      </>
    )    
  }
}
TextArrayField.defaultProps = { addLabel: true }

Usage:

      <TextArrayField source="tags">
        <SingleFieldList>
          <ChipField source="id" />
        </SingleFieldList>
      </TextArrayField>
Tanapruk commented 4 years ago

For the input field I use

<ArrayInput source="tags" helperText="for example, barcode">
            <SimpleFormIterator>
              <TextInput format={(value) => (typeof value === 'object' ? '' : value)} label="" />
            </SimpleFormIterator>
          </ArrayInput>
mtyurt commented 3 years ago

@zhouhao27 's solution fails for nested dictionaries, in that case you can use this:

import get from 'lodash/get';

const TextArrayField = ({ record, source }) => {
  const array = get(record, source)
...
}

And usage:

<TextArrayField source="dict.tags">
  <SingleFieldList>
    <ChipField />
  </SingleFieldList>
</TextArrayField>
thinnguyenqb commented 2 years ago

@fzaninotto Modified your code a bit. It works now.

import Chip from '@material-ui/core/Chip'

const TextArrayField = ({ record, source }) => {
  const array = record[source]
  if (typeof array === 'undefined' || array === null || array.length === 0) {
    return <div/>
  } else {
    return (
      <>
        {array.map(item => <Chip label={item} key={item}/>)}
      </>
    )    
  }
}
TextArrayField.defaultProps = { addLabel: true }

Usage:

      <TextArrayField source="tags">
        <SingleFieldList>
          <ChipField source="id" />
        </SingleFieldList>
      </TextArrayField>

good

abe-winter commented 2 years ago

might be late to the party here, but having a similar problem

as a design solution, is there any value in allowing source="." inside arrays, which would use the entire current list element?

I'm not totally familiar with this codebase, but could maybe do it here where SingleFieldList creates its child

something like Object.assign({}, record, {'.': record}), so the child can then ask for '.' field

zackha commented 2 years ago

If the json structure you use field fetches a one-to-many relationship and you want to bring them without the need for ArrayField, you can use the example mui table component code below.

import {useRecordContext} from 'react-admin';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Paper from '@mui/material/Paper';

export default function ProductsInOrder() {
const items = useRecordContext();
  return (
      <TableContainer component={Paper}>
          <Table sx={{ minWidth: 600 }} aria-label="simple table">
              <TableHead>
                  <TableRow>
                      <TableCell>Id</TableCell>
                      <TableCell>Name</TableCell>
                  </TableRow>
              </TableHead>
              <TableBody>
                  {items.line_items.map((item) => (
                     <TableRow>
                         <TableCell>{item.id}</TableCell>
                         <TableCell>{item.name}</TableCell>
                     </TableRow>
                  ))}
              </TableBody>
          </Table>
      </TableContainer>
  );
}
Ferlorin commented 6 months ago

For future readers (myself included), I guess react-admin now has TextArrayField included in "@react-admin/ra-form-layout/lib/stories/TextArrayField" (i think in react admin enterprise), but I could not find this documented anywhere, so here you go.