React-admin Array input with array of strings/numbers - javascript

https://marmelab.com/react-admin/Inputs.html#arrayinput
Examples cover cases where you have an array of objects:
backlinks: [
{
date: '2012-08-10T00:00:00.000Z',
url: 'http://example.com/foo/bar.html',
},
{
date: '2012-08-14T00:00:00.000Z',
url: 'https://blog.johndoe.com/2012/08/12/foobar.html',
}
]
is it possible to have it work with just an array of strings?
backlinks: ['a', 'b', 'c']

I was able to execute the inputs variant, as opposed to the fields variant, by simply not providing a source attribute for the inner TextField, and sourcing the array in the actual ArrayField. Then of course just use a SimpleFormIterator. Clearly React favors the use of keys, treating array types like maps, for the most part.
<ArrayInput source="my-source">
<SimpleFormIterator>
<TextInput />
</SimpleFormIterator>
</ArrayInput>

Here is my working code based on #fzaninotto's post in react-admin Issues:
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>

Maybe you can create your own Field component which can able to take source and record as props.
function populateList(numbers) {
return numbers.map((number) =>
<li key={number.toString()}>
{number}
</li>
);
}
const SimpleArray = ({source, record = {}}) =>
<ul>
{
populateList(record[source])
}
</ul>;
SimpleArray.defaultProps = {
addLabel: true,
label: 'List'
};
SimpleArray.propTypes = {
label: PropTypes.string,
record: PropTypes.object,
source: PropTypes.string
};
export default SimpleArray;
And easily use it inside any form element like :
<SimpleShowLayout>
<TextField source="id"/>
<TextField label="Title" source="title" className={classes.name}/>
<TextField source="title"/>
<NumberField source="defaultItemCount"/>
<RichTextField source="description"/>
<NumberField source="priceInNumber"/>
<SimpleArray source="attributeArray" label="Product Attributes" />
</SimpleShowLayout>

My solution expands a bit on the answer from #kcrawford
In my case, I needed to output the array of URLs. Simplified version of the code
const MassMessageEdit: FC<any> = (props) => (
<Edit {...props}>
<SimpleForm {...props}>
...
<ArrayField source="onesignalUrls">
<URLs />
</ArrayField>
</CreateEditForm>
</Edit>
)
const URLs: React.FC<{ ids?: string[] }> = (props) => {
if (!props["ids"]) return null
return (
<ul>
{props["ids"].map((link, key) => (
<li key={key}>
<a href={JSON.parse(link) as string} rel="noopener noreferrer" target="_blank">
Notification {key + 1}
</a>
</li>
))}
</ul>
)
}
ArrayField passes the values array as ids, which can later be parsed and rendered

Related

React: Data within a map is appearing different even though the code is written the same way for both instances where used

Apologies if this is poorly written (first time posting here so feedback on how to better write posts welcome!)
I am using react map to iterate through lists of data.
{level1Folders.level2Folders.map(
(level2Folders, index) => {
return (
<li
id={level2Folders.folderLevel2Name}
key={level2Folders.folderLevel2Name + index}
>
<div
className="menu-item-folder-level-2"
onClick={() =>
hideMenuItem(
level2Folders.folderLevel2Name
)
}
>
<FaIcons.FaCaretRight />
{level2Folders.folderLevel2Name}
</div>
<ul
className="manuals d-none"
id={level2Folders.folderLevel2Name}
>
{level2Folders.manuals.map(
(manual, index) => {
return (
<li key={manual + index} id={manual}>
<div
onClick={() =>
handleExplorerItemClick(manual)
}
className="menu-item-manual"
>
{manual}
</div>
</li>
);
}
)}
I have a method hideMenuItem(menuItemId) which will hide items based on their id's, so the idea is to set the id = to the name of the item, so when the parent item is clicked the child elements will be hidden.
function hideMenuItem(menuItemId) {
console.log(menuItemId);
let x = document.getElementById(menuItemId);
if (x.classList.contains('d-block')) {
x.classList.add('d-none');
x.classList.remove('d-block');
} else {
x.classList.add('d-block');
x.classList.remove('d-none');
}
}
I have 5 uses of this - level2Folders.folderLevel2Name, the only one that won't work is when trying to enter this as a parameter in hideMenuItem(menuItemId), the value here is returned as the index of the item.
The point here is you want to toggle the item's child by using the element's classlist. It might be better if you change your approach and use react ways to achieve your goals. One of them is conditional styling where you can read here for the details.
For your case, let me show you one of many approach which using state in show and hide elements. Try absorp the concept and implement it at yours.
First, the data should have name which is the name of folder, showSubFolders which is property that will keep the boolean value, and subFolders that contains the sub folders details.
const folderData = [
{
name: "My Documents",
showSubFolders: false,
subFolders: [
{
name: "My Music",
icon: faMusic
},
{
name: "My Images",
icon: faImage
}
]
},
...
}
Then, set a folders state that will keep the folder's data in our component:
export default function OurComponent() {
const [folders, setFolders] = useState(folderData);
...
}
Since we use <FontAwesomeIcon icon={...} />, so we can render the main folder icon by using conditional icon selection which depends on folder's showSubFolders property value:
return (
...
<ul>
{folders.map((folder, index) => {
return (
<li>
<FontAwesomeIcon
icon={folder.showSubFolders ? faFolderOpen : faFolder}
/>
</li>
...
)
})}
</ul>
...
)
The next is toggle subFolders section by creating a toggleFolder method that use useCallback hooks and depends on folders state. We negate the value of showSubFolders property if the folder name equal to the argument name supplied.
const toggleFolder = useCallback(
(name) => {
const toggledFolders = folders.map((folder) => {
if (folder.name === name) {
folder.showSubFolders = !folder.showSubFolders;
}
return folder;
});
setFolders(toggledFolders);
},
[folders]
);
And we can call the toggleFolder method from our list item as follow:
return (
...
{folders.map((folder, index) => {
return (
<li key={index} style={{ cursor: "pointer" }}>
<span onClick={() => toggleFolder(folder.name)}>
<FontAwesomeIcon
icon={folder.showSubFolders ? faFolderOpen : faFolder}
/>
{folder.name}
</span>
...
</li>
...
)
})}
...
)
And when the folders state change, the component will re-render and we can use conditional render technique {folder.showSubFolders && ( ... ) here:
<li>
...
{folder.showSubFolders && (
<ul>
{folder.subFolders.map((subFolder, index) => {
return (
<li key={index}>
<FontAwesomeIcon icon={subFolder.icon} />
{subFolder.name}
</li>
);
})}
</ul>
)}
</li>
Of course, this is not the only way to achieve your goal, but it is more React Way in doing so.
And lastly, this is the final code:

Passing an object Id by submit button in order to find and change value in array after submiting (react)

I want to update a value of an object nested in array after choosing new date from select button and then submiting it in order to change it.
The button (select) is nested with a rendered array object invoice in Accordion from material ui and is supposed to change a date (for now a year only) and save it while comparing its Id number.
I have two components : sideBar and simpleForm
const SideBar = ({ className }) => {
const [invoices, setInvoice] = useState([
{ label: 'Test', invoiceDate: '2021', id: 0 },
{ label: 'Test', invoiceDate: '2022', id: 1 },
{ label: 'Test', invoiceDate: '', id: 2 },
])
const addInvoiceDate = (date, invoice) => {
setInvoice(
invoices.map((x) => {
if (x.id === invoice.id)
return {
...x,
invoiceDate: date,
}
return x
})
)
}
return (
<>
<Wrapper>
<S.MainComponent>
<div>
{invoices.map((invoice) => {
return (
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<div key={invoice.id}>
{invoice.label}
{invoice.invoiceDate}
</div>
</AccordionSummary>
<AccordionDetails>
<SimpleForm addInvoiceDate={addInvoiceDate} />
</AccordionDetails>
</Accordion>
)
})}
</div>
</S.MainComponent>
</Wrapper>
</>
)
}
Simple Form :
const SimpleForm = ({ addInvoiceDate }) => {
const [date, setDate] = useState('')
const handleSubmit = (e) => {
e.preventDefault()
addInvoiceDate(date)
}
function range(start, end) {
return Array(end - start + 1)
.fill()
.map((_, placeholder) => start + placeholder)
}
const Years = range(2021, 4000)
const Options = []
Years.forEach(function (element) {
Options.push({ label: element, value: element })
})
return (
<form onSubmit={handleSubmit}>
<select value={date} required onChange={(e) => setDate(e.target.value)}>
{Options.map((option) => (
<option value={option.value}>{option.label}</option>
))}
</select>
<input type='submit' value='Save' />
</form>
)
}
My problem is, i have no clue how can i succesfully pass an id number of array object to addInvoiceDate (change invoice date)in order to find it in orginal array, compare it and then submit new value. I was testing adding a new object with a new year value and it worked, but in that case i dont have to find an id of object. Its a little bit harder if i want actually find a previous one and update it through comparing id.
Any ideas how it should be done ? Probably i overlooked something or dont have enough expierience yet :)
How about this
<SimpleForm addInvoiceDate={(date) => addInvoiceDate(date, invoice)} date={invoice.invoiceDate}/> in SideBar's return
Remove the state from SimpleForm as we now have a date prop instead
<select value={date} required onChange={(e) => addInvoiceDate(e.target.value)}> in SimpleForm's return
Please leave a comment if there are questions

How to change property of one object in an array of objects in React state?

I have an array of JavaScript objects that I holding in React State, and on a click, I change the property of one of the objects in the array.
I got the following to work without mutating state, but my current setState() syntax also adds the same object to the end of the array again.
How can I simply change the state of one of the objects in my array of objects in state, without adding another object and without mutating state?
import React, { useState } from 'react';
interface IFlashcard {
noun: string;
article: string;
show: boolean;
}
const initialFlashcards = [
{
noun: 'Dependency',
article: 'die Dependency, die Dependencys',
show: false
},
{
noun: 'Kenntnis',
article: 'die Kenntnis, die Kenntnisse',
show: false
},
{
noun: 'Repository',
article: 'das Repository, die Repositorys',
show: false
},
{
noun: 'Kenntnis',
article: 'die Kenntnis, die Kenntnisse',
show: false
}
];
function LanguageFlashcards() {
const [flashcards, setFlashcards] = useState(initialFlashcards);
const toggleFlashcard = (flashcard: IFlashcard) => {
flashcard.show = !flashcard.show;
setFlashcards([...flashcards, flashcard]);
}
return (
<>
<h2>Language Flashcards</h2>
<ul>
{flashcards.map((flashcard: IFlashcard) => {
return (
<>
<li><span onClick={() => toggleFlashcard(flashcard)}>{flashcard.noun}</span>
{flashcard.show && (
<>
: {flashcard.article}
</>
)}
</li>
</>
)
})}
</ul>
</>
);
}
export default LanguageFlashcards;
Your example is in fact mutating state here:
flashcard.show = !flashcard.show;
At this point, flashcard refers directly to an object in state, so altering one of its properties is a mutation.
You need a way to identify the objects in state so that you can extract one, clone it individually, and then insert it back into a cloned state array in its original position. Without changing any of your data, you could do this by passing the array position of the flashcard when you call toggleFlashcard.
{flashcards.map((flashcard: IFlashcard, i: number) => {
return (
<>
<li><span onClick={() => toggleFlashcard(i)}>{flashcard.noun}</span>
{flashcard.show && (
<>
: {flashcard.article}
</>
)}
</li>
</>
)
})}
Now the toggleFlashcard event handler should look something like this:
const toggleFlashcard = (i: number) => {
const clonedCard = {...flashcards[i]};
clonedCard.show = !clonedCard.show;
const clonedState = [...flashcards];
clonedState[i] = clonedCard;
setFlashcards(clonedState);
}
If you don't want to mutate anything, please try this solution.
const toggleFlashcard = (flashcard: IFlashcard) => {
const flashcardIndex = flashcards.findIndex(f => f === flashcard);
const newFlashcards = [...flashcards];
newFlashcards[flashcardIndex]= { ...flashcard, show: !flashcard.show };
setFlashcards(newFlashcards);
};
And this is not related to the main topic but the key attribute is missing here.
{flashcards.map((flashcard: IFlashcard, index: number) => {
...
<li key={index}><span onClick={() => toggleFlashcard(flashcard)}>{flashcard.noun}</span>
If you don't specify the key attribute, you will see React warnings.
To do what you want you could do something like this
const toggleFlashcard = (flashcardIndex: number) => {
const flashcardsDeepCopy: IFlashcard[] = JSON.parse(JSON.stringify(flashcards));
flashcardsDeepCopy[flashcardIndex].show = !flashcard.show;
setFlashcards(flashcardsDeepCopy);
}
In jsx you need to pass the index
<ul>
{flashcards.map((flashcard: IFlashcard, index) => {
return (
<>
<li><span onClick={() => toggleFlashcard(index)}>{flashcard.noun}</span>
{flashcard.show && (
<>
: {flashcard.article}
</>
)}
</li>
</>
)
})}
</ul>
Your problem is with your toggleFlashcard function you think you aren't mutating the original state but you are given that javascript passes your object by reference. If you were to just do
const toggleFlashcard = (flashcard: IFlashcard) => {
flashcard.show = !flashcard.show;
setFlashcards([...flashcards]);
}
It would work however that isn't really react practice. What you would need to do is return a brand new array without modifying the original object. youll need some sort of identifier to filter out which flash card is which. maybe appending the index of the item to the renderer or pass in something from the backend.

Deleting list Item in react keep older items instead

I would like to delete selected item from list.
When I click on delete the right item get deleted from the list content but on UI I get always the list item fired.
I seems to keep track of JSX keys and show last values.
Here's a demo
const Holidays = (props) => {
console.log(props);
const [state, setState] = useState({ ...props });
useEffect(() => {
setState(props);
console.log(state);
}, []);
const addNewHoliday = () => {
const obj = { start: "12/12", end: "12/13" };
setState(update(state, { daysOffList: { $push: [obj] } }));
};
const deleteHoliday = (i) => {
const objects = state.daysOffList.filter((elm, index) => index != i);
console.log({ objects });
setState(update(state, { daysOffList: { $set: objects } }));
console.log(state.daysOffList);
};
return (
<>
<Header as="h1" content="Select Holidays" />
<Button
primary
icon={<AddIcon />}
text
content="Add new holidays"
onClick={() => addNewHoliday(state)}
/>
{state?.daysOffList?.map((elm, i) => {
console.log(elm.end);
return (
<Flex key={i.toString()} gap="gap.small">
<>
<Header as="h5" content="Start Date" />
<Datepicker
defaultSelectedDate={
new Date(`${elm.start}/${new Date().getFullYear()}`)
}
/>
</>
<>
<Header as="h5" content="End Date" />
<Datepicker
defaultSelectedDate={
new Date(`${elm.end}/${new Date().getFullYear()}`)
}
/>
</>
<Button
key={i.toString()}
primary
icon={<TrashCanIcon />}
text
onClick={() => deleteHoliday(i)}
/>
<span>{JSON.stringify(state.daysOffList)}</span>
</Flex>
);
})}
</>
);
};
export default Holidays;
Update
I'm trying to make a uniq id by adding timeStamp.
return (
<Flex key={`${JSON.stringify(elm)} ${Date.now()}`} gap="gap.small">
<>
<Header as="h5" content="Start Date" />
<Datepicker
defaultSelectedDate={
new Date(`${elm.start}/${new Date().getFullYear()}`)
}
/>
</>
<>
<Header as="h5" content="End Date" />
<Datepicker
defaultSelectedDate={
new Date(`${elm.end}/${new Date().getFullYear()}`)
}
/>
</>
<Button
primary
key={`${JSON.stringify(elm)} ${Date.now()}`}
icon={<TrashCanIcon />}
text
onClick={() => deleteHoliday(i)}
/>{" "}
</Flex>
);
I was hoping that the error disappear but still getting same behaviour
Issue
You are using the array index as the React key and you are mutating the underlying data array. When you click the second entry to delete it, the third element shifts forward to fill the gap and is now assigned the React key for the element just removed. React uses the key to help in reconciliation, if the key remains stable React bails on rerendering the UI.
You also can't console log state immediately after an enqueued state update and expect to see the updated state.
setState(update(state, { daysOffList: { $set: objects } }));
console.log(state.daysOffList);
React state updates are asynchronous and processed between render cycles. The above can, and will, only ever log the state value from the current render cycle, not the update enqueued for the next render cycle.
Solution
Use a GUID for each start/end data object. uuid is a fantastic package for this and has really good uniqueness guarantees and is incredibly simple to use.
import { v4 as uuidV4 } from 'uuid';
// generate unique id
uuidV4();
To specifically address the issues in your code:
Add id properties to your data
const daysOffList = [
{ id: uuidV4(), start: "12/12", end: "12/15" },
{ id: uuidV4(), start: "12/12", end: "12/17" },
{ id: uuidV4(), start: "12/12", end: "12/19" }
];
...
const addNewHoliday = () => {
const obj = {
id: uuidV4(),
start: "12/12",
end: "12/13",
};
setState(update(state, { daysOffList: { $push: [obj] } }));
};
Update handler to consume id to delete
const deleteHoliday = (id) => {
const objects = state.daysOffList.filter((elm) => elm.id !== id);
setState(update(state, { daysOffList: { $set: objects } }));
};
Use the element id property as the React key
{state.daysOffList?.map((elm, i) => {
return (
<Flex key={elm.id} gap="gap.small">
...
</Flex>
);
})}
Pass the element id to the delete handler
<Button
primary
icon={<TrashCanIcon />}
text
onClick={() => deleteHoliday(elm.id)}
/>
Use an useEffect React hook to log any state update
useEffect(() => {
console.log(state.daysOffList);
}, [state.daysOffList]);
Demo
Note: If you don't want (or can't) install additional 3rd-party dependencies then you can roll your own id generator. This will work in a pinch but you should really go for a real proven solution.
const genId = ((seed = 0) => () => seed++)();
genId(); // 0
genId(); // 1

How to render a list of formatted messages using React-Intl

I have a navigation with standard items like: contact, services, prices, etc... I render it like this:
const menuItemList = menuItems.map((item, index) => {
return (
<li key={index}>
<NavLink to={item.url}>{item.title}</NavLink>
</li>
);
});
It works fine. But now I need to translate this navigation and I use react-intl library for this purpose. Accordingly to react-intl doc I have to use FormattedMessage like this:
<p>
<FormattedMessage id="mainText"/>
</p>
It works. But how I can use it for list rendering? I think it would work with this, but it doesn't.
const menuItemsList = menuItems.map((item, index) => {
return (
<li key={index}>
<NavLink to={item.url}>
<FormattedMessage id="mainText" values={item.title}/>
</NavLink>
</li>
);
});
Guys, help please. How to render a list with items in React using FormattedMessage from react-intl?
You need to have messages passed to intl. For example:
{
profile: 'Profile',
settings: 'Settings'
}
Also, I suppose that you have
const menuItems = [
{
url: '/profile',
title: 'profile'
},
{
url: '/settings',
title: 'settings'
}
]
So you can use it like this
const menuItemsList = menuItems.map((item, index) => {
return (
<li key={index}>
<NavLink to={item.url}>
<FormattedMessage id={item.title} />
</NavLink>
</li>
);
});
I recently had the same problem. React Intl doesn't allow to pass an array to <FormattedMessage />, So assuming you have a large array of translated items like in this english messages object:
{
"page.title": "Test Page",
"page.menuItems": ["First english Item", "second english Item", ... , "100th english item"]
}
You'd need a workaround. The idea is that we want to access those items via a nested index instead of a global id.
Now i found two solutions to get this working:
directly access the nested messages in the intl messages via their index (hacky & fast):
import React from "react"
import {useIntl} from 'react-intl'
import NavLink from 'somewhere'
const Menu = ({menuItems}) => {
const intl = useIntl();
const menuItemsIntl = intl.messages["page.menuItems"];
return (<div>
{menuItems.map((item, index) =>
<li key={index}>
<NavLink to={item.url}>
{menuItemsIntl[index]}
</NavLink>
</li>}
</div>);
});
export default Menu;
This has the drawback that you don't have the fallback functionality of React Intl in case you don't have a translation available. You'd need to write your custom FormatMessage component around menuItemsIntl[index] that handles missing keys.
Flatten out the messages object before injecting it into the provider and then access it via a computed unique key (Safe & robust):
We would need the messages object to look like this:
{
"page.title": "Test Page",
"page.menuItems.1": "First english Item",
"page.menuItems.2": "Second english Item",
...
"page.menuItems.100": "100th english item"
}
In order to have that we'd need to flatten the original messages object, for example with the following mutating function:
import translations from "./translations.json";
const Root = ()=> {
const [lan, setLan]= useState("en");
const messages = translations[lan]
const flattenMessages = (obj, parKey = "") => {
Object.keys(obj).forEach((key) => {
const currKey = parKey + (parKey.length > 0 ? "." : "") + key;
if (Array.isArray(obj[key]) || typeof obj[key] === 'object') {
flattenMessages (obj[key], currKey)
} else if (typeof obj[key] === "string") {
messages[currKey] = obj[key]
}
})
}
flattenMessages({...messages})
return <IntlProvider
key={lan}
messages={messages}
locale={lan}
defaultLocale="en"
>
<App />
</IntlProvider>
};
And later you you just use FormattedMessage with an id like you are used to. You simply need to compute the id string based on the iterator in jsx.
const Menu = ({menuItems}) => {
return (<div>
{menuItems.map((item, index) =>
<li key={index}>
<NavLink to={item.url}>
<FormattedMessage id={`page.menuItems.${index}.title`} />
</NavLink>
</li>}
</div>);
});
I wrote the first solution in case somebody didn't know that you can directly access the messages object. For most cases however i would reccommend the second solution.
You can use defineMessages
const menuItemsList = menuItems.map((item, index) => {
return (
<li key={index}>
<NavLink to={item.url}>
<FormattedMessage {...MSG[`item_${index}`]} />
</NavLink>
</li>
);
});
const MSG = defineMessages({
item_0: {
id: 'items.service',
defaultMessage: 'Service'
},
item_1: {
id: 'items.contact',
defaultMessage: 'Contact'
}
})

Categories