Rendering comma separated list of links - reactjs

I'm trying to output a list of comma separated links and this is my solution.
var Item = React.createComponent({
render: function() {
var tags = [],
tag;
for (var i = 0, l = item.tags.length; i < l; i++) {
if (i === item.tags.length - 1) {
tag = <span><Tag key={i} tag={item.tags[i]} /></span>;
} else {
tag = <span><Tag key={i} tag={item.tags[i]} /><span>, </span></span>;
}
tags.push(tag);
}
return (
<tr>
<td>
{item.name}
</td>
<td>
{tags}
</td>
</tr>
);
}
});
I was just wondering if there was a better, more clean way to accomplish this?
Thanks

Simply
{tags.map((tag, i) => <span key={i}>
{i > 0 && ", "}
<Tag tag={tag} />
</span>)}
In React 16 it can be done even more simpler:
{tags.map((tag, i) => [
i > 0 && ", ",
<Tag key={i} tag={tag} />
])}

At Khan Academy we use a helper called intersperse for this:
/* intersperse: Return an array with the separator interspersed between
* each element of the input array.
*
* > _([1,2,3]).intersperse(0)
* [1,0,2,0,3]
*/
function intersperse(arr, sep) {
if (arr.length === 0) {
return [];
}
return arr.slice(1).reduce(function(xs, x, i) {
return xs.concat([sep, x]);
}, [arr[0]]);
}
which allows you to write code like:
var tags = item.tags.map(function(tag, i) {
return <Tag key={i} tag={item.tags[i]} />;
};
tags = intersperse(tags, ", ");

Or simply write the list items to an unordered list and use CSS.
var Item = React.createComponent({
render: function() {
var tags = this.props.item.tags.map(function(i, item) {
return <li><Tag key={i} tag={item} /></li>
});
return (
<tr>
<td>
{this.props.item.name}
</td>
<td>
<ul className="list--tags">
{tags}
</ul>
</td>
</tr>
);
}
});
And the CSS:
.list--tags {
padding-left: 0;
text-transform: capitalize;
}
.list--tags > li {
display: inline;
}
.list--tags > li:before {
content:',\0000a0'; /* Non-breaking space */
}
.list--tags > li:first-child:before {
content: normal;
}

import React from 'react';
import { compact } from 'lodash';
// Whatever you want to separate your items with commas, space, border...
const Separator = () => { ... };
// Helpful component to wrap items that should be separated
const WithSeparators = ({ children, ...props }) => {
// _.compact will remove falsey values: useful when doing conditional rendering
const array = compact(React.Children.toArray(children));
return array.map((childrenItem, i) => (
<React.Fragment key={`${i}`}>
{i > 0 && <Separator {...props} />}
{childrenItem}
</React.Fragment>
));
};
const MyPage = () => (
<WithSeparators/>
<div>First</div>
{second && (<div>Maybe second</div>)}
{third && (<div>Maybe third</div>)}
<div>Fourth</div>
</WithSeparators>
);

A function component that does the trick. Inspired by #imos's response. Works for React 16.
const Separate = ({ items, render, separator = ', ' }) =>
items.map((item, index) =>
[index > 0 && separator, render(item)]
)
<Separate
items={['Foo', 'Bar']}
render={item => <Tag tag={item} />}
/>

Here's a solution that allows <span>s and <br>s and junk as the separator:
const createFragment = require('react-addons-create-fragment');
function joinElements(arr,sep=<br/>) {
let frag = {};
for(let i=0,add=false;;++i) {
if(add) {
frag[`sep-${i}`] = sep;
}
if(i >= arr.length) {
break;
}
if(add = !!arr[i]) {
frag[`el-${i}`] = arr[i];
}
}
return createFragment(frag);
}
It filters out falsey array elements too. I used this for formatting addresses, where some address fields are not filled out.
It uses fragments to avoid the warnings about missing keys.

Simple one:
{items.map((item, index) => (
<span key={item.id}>
{item.id}
{index < items.length - 1 && ', '}
</span>
))}

To add to the great answers above Ramda has intersperse.
To comma separate a bunch of items you could do:
const makeLinks = (x: Result[]) =>
intersperse(<>,</>, map(makeLink, x))
Pretty succinct

The solution without extra tags
<p className="conceps inline list">
{lesson.concepts.flatMap((concept, i) =>
[concept, <span key={i} className="separator">•</span>]
, ).slice(-1)}
</p>
generates something like
Function • Function type • Higher-order function • Partial application

The easiest way to do
const elementsArr = ["a", "b", "c"];
let elementsToRender = [] ;
elementsArr.forEach((element, index) => {
let elementComponent = <TAG className="abc" key={element.id}>{element}</TAG>
elementsToRender.push(elementComponent);
if(index !== (elementsArr.length - 1)){
elementsToRender.push(", ");
}
});
render(){
return (
<div>{elementsToRender}</div>
)
}

Related

Issue with adding conditions to secret santa app

I'm working on creating secret santa app for my christmas party.
For now it works, but I need to add some conditions.
function App() {
var names = ["John", "Martha", "Adam", "Jane", "Michael"];
const shuffle = (arr: string[]) => {
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr;
};
const randomNames = shuffle(names);
const matches = randomNames.map((name, index) => {
return {
santa: name,
receiver: randomNames[index + 1] || randomNames[0],
};
});
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>Secret santa game</p>
<select>
<option>Select your name...</option>
{names.map((name) => (
<option> {name}</option>
))}
</select>
<div>
{matches.map((match) => {
return (
<div>
{match.receiver},{match.santa}
<br />
</div>
);
})}
</div>
</header>
</div>
);
}
export default App;
John and Martha are couple so they will buy themselves presents outside of the party anyway, so if one of them is receiver and another is santa, I want to generate results again so they'll be assigned to someone else.
I'm not sure how I can accomplish this.
You could define your relationships like this:
const couples = { John: "Martha" };
then have a function that verifies that your conditions are met:
const conditions_check = (santa: string, receiver: string) => {
if (couples[santa] === receiver) {
return false;
} else if (couples[receiver] === santa) {
return false;
}
return true;
};
and then finally you can check if your conditions are met while generating your matches and shuffle your pool of names again if needed:
let matches = randomNames.map((name, index) => {
let receiver = randomNames[(index + 1) % randomNames.length];
for (let i = 0; i < 25; i++) {
if (conditions_check(name, receiver)) {
break;
}
randomNames = shuffle(names);
}
return {
santa: name,
receiver: receiver
};
});
You can check out the whole implementation in this playground.

React Testing Library - Click all buttons returned by function

test("remove list when no entries are present", () => {
render(<ListContainer />);
seedEntries();
expect(screen.getByRole("list")).toBeInTheDocument;
screen.getAllByRole("button", { name: /\-/i }).forEach((entry) => {
userEvent.click(entry)
});
expect(screen.queryByRole("list")).toBeNull();
});
Clicking the button calls the following function:
const handleDeleteEntry = (id) => {
if (entries.length === 10) {
setLimitError(false);
}
let filteredEntries = entries.filter((entry) => entry.id !== id);
setEntries(filteredEntries);
};
and my render method looks like this:
{entries.length > 0 ? (
<ul>
{entries.map((entry, index) => {
return (
<li
key={index}
>
<div>
{entry.name}
</div>
<div>
{entry.address}
</div>
<div>
{entry.phoneNumber}
</div>
<button
type="button"
onClick={() => handleDeleteEntry(entry.id)}
>
-
</button>
</li>
);
})}
</ul>
) : null}
const seedEntries = () => {
for (let i = 0; i < 10; i++) {
userEvent.type(screen.getByTestId("input-name"), "testName" + i);
userEvent.type(screen.getByTestId("input-address"), "testAddress" + i);
userEvent.type(screen.getByTestId("input-phone"), "012345678" + i);
userEvent.click(screen.getByRole("button", { name: /\+/i }));
}
};
Is it possible to have my test run so that each of the deletion buttons are clicked reliably? Currently it appears to be missing every 2nd button (I think due to it being too quick, but I've tried playing around with settimeouts and async awaits to no success) my user-event is on v13.5

How to clear fields array in redux form

I am trying to clear checkbox array in fields array from the redux form. How can I do it?
I'm using redux form FieldArray in the format of members:[filterOption:{}, checkBoxOption:{}]. checkBoxOption value depends o filterOption dropdown. Whenever the user selects an option from filterOption in result they get a list of checkbox from which they have to select from the list of checkBoxOption.
Let's say if a user has selected a value from filterOption and checkBoxOption and now they change the value of filterOption in result they will get a new list of an array for checkBoxOption. The values are getting replaced by the new one but they are not getting uncheck.
I am able to clear checkbox array in values array by using fields.get(event).checkBoxOption = {} but unable to find the solution on how to empty fields array.
Can anyone help me out with this?
<ul className="list-group">
{
fields.map((member, index) => (
<li className="list-group filter-select-box" key={index}>
<SelectBox
name={`${member}.filterOption`}
label="Metadata Attribute"
options={attributes.map(attribute => ({
value: attribute.name,
label: attribute.name,
disabled: selectedAttributes.filter(value => value.name === attribute.name).length > 0,
}))}
isChange
handleSelectChange={opt => handleOptionChange(index, opt.value)}
/>
{checkStatus(index) && (
<div className="select-checkbox-option form-group">
{
getCheckboxList(index).map((checkboxItem, x) => (
<CheckBox
key={x}
type="checkbox"
name={`${member}.checkBoxOption.${checkboxItem}`}
label={checkboxItem}
value={`${member}.checkBoxOption.${checkboxItem}`}
id={checkboxItem}
/>
))
}
</div>
)}
</li>
))
}
<li className="list-group filter-select-box">
<button className="add-filter" type="button" onClick={() => fields.push({})}>
<img src={filterPlus} alt="" className="filterPlus" />
Add Filter
</button>
{touched && error && <span>{error}</span>}
</li>
</ul>
the function which is getting checkbox value
const handleOptionChange = (event, nameAttribute) => {
const value = {
index: event,
status: true,
name: nameAttribute,
};
let selectedAttributesStatus = false;
for (let i = 0; i < selectedAttributes.length; i += 1) {
if (value.index === selectedAttributes[i].index) {
selectedAttributes[i].name = value.name;
selectedAttributesStatus = true;
}
}
if (!selectedAttributes.length || !selectedAttributesStatus) {
setSelectedAttributes([...selectedAttributes, value]);
}
setShowOptions([...showOption, value]);
getCategoricalVar(extractorId, nameAttribute)
.then((resp) => {
const newAttributeValue = {
index: event,
value: resp,
};
fields.get(event).checkBoxOption = {};
setSelectedIndex(false);
console.log('fields.get(event).checkBoxOption: ', fields.get(event).checkBoxOption);
let attributeValuesStatus = false;
for (let i = 0; i < attributeValues.length; i += 1) {
if (newAttributeValue.index === attributeValues[i].index) {
attributeValues[i].value = newAttributeValue.value;
attributeValuesStatus = true;
}
}
if (!attributeValues.length || !attributeValuesStatus) {
setAttributeValues([...attributeValues, newAttributeValue]);
}
})
.catch(printError);
};
Function which is setting the value on checkbox
const getCheckboxList = (index) => {
for (let i = 0; i < attributeValues.length; i += 1) {
if (attributeValues[i].index === index) {
return attributeValues[i].value;
}
}
return [];
};

Adjust where HTML gets rendered in a map loop

I have an array of HTML elements that I want to render on a page, but depending on the element I'd like to adjust how they get wrapped.
const sections = [
{
id: 'top',
},
{
id: 'left',
},
{
id: 'right',
},
{
id: 'bottom',
}
]
const Element = (props) => {
return <div id={props.id}>hello</div>
}
const ArticleRightRail = (props) =>
<div>
<header>
</header>
<article>
{sections.map((section, i) => <Element key={i} {...section} >hello!</Element> )}
</article>
</div>
In the example above I want any id which is not top or bottom to be rendered within <article>, and anything that is top or bottom to be rendered within <header> tags. What is the best way of handling a situation like this with React?
Use ES6 array filter method to filter sections array as below:
const listForHeader = sections.filter((section, i) => {
return section === "top" || section === "bottom";
});
const listForArticle = sections.filter((section, i) => {
return section != "top" || section != "bottom";
})
Then use above 2 lists in HEADER and ARTICLE tags respectively using array map method.
check this
const listHeader = sections.filter((section, i) => {
return section === "top" || section === "bottom";
});
const listArticle = sections.filter((section, i) => {
return section != "top" || section != "bottom";
})
#JamesIves, the react code is:
const listForHeader = sections.filter((section, i) => {
return section === "top" || section === "bottom";
});
const listForArticle = sections.filter((section, i) => {
return section != "top" || section != "bottom";
})
const ArticleRightRail = (props) =>
<div>
<header>
{listForHeader.map((section, i) => <Element key={i} {...section} >hello!</Element> )}
</header>
<article>
{listForArticle.map((section, i) => <Element key={i} {...section} >hello!</Element> )}
</article>
</div>

is there a way to do array.join in react [duplicate]

This question already has answers here:
How to render react components by using map and join?
(18 answers)
Closed 10 months ago.
I'd like to inject a separator between each element of an array using an array.join-like syntax.
Something like the following:
render() {
let myArray = [1,2,3];
return (<div>
{myArray.map(item => <div>{item}</div>).join(<div>|</div>)}
</div>);
}
I've got it working using a lodash.transform approach, but it feels ugly, I'd like to just do .join(<some-jsx>) and have it insert a separator in between each item.
You can also use reduce to insert the separator between every element of the array:
render() {
let myArray = [1,2,3];
return (
<div>
{
myArray
.map(item => <div>{item}</div>)
.reduce((acc, x) => acc === null ? [x] : [acc, ' | ', x], null)
}
</div>
);
}
or using fragments:
render() {
let myArray = [1,2,3];
return (
<div>
{
myArray
.map(item => <div>{item}</div>)
.reduce((acc, x) => acc === null ? x : <>{acc} | {x}</>, null)
}
</div>
);
}
You can also do it by combining .reduce and React fragments.
function jsxJoin (array, str) {
return array.length > 0
? array.reduce((result, item) => <>{result}{str}{item}</>)
: null;
}
function jsxJoin (array, str) {
return array.length > 0
? array.reduce((result, item) => <React.Fragment>{result}{str}{item}</React.Fragment>)
: null;
}
const element = jsxJoin([
<strong>hello</strong>,
<em>world</em>
], <span> </span>);
ReactDOM.render(element, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.4.1/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.4.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Trying to .join with React Elements is probably not going out pan out for you. This would produce the result you describe needing.
render() {
let myArray = [1,2,3];
return (
<div>
{myArray.map((item, i, arr) => {
let divider = i<arr.length-1 && <div>|</div>;
return (
<span key={i}>
<div>{item}</div>
{divider}
</span>
)
})}
</div>
);
}
Using Array.map and React.Fragment you can join each array item with any JSX element you want. In the example below I'm using a <br /> tag but this can be substituted for whatever element you want.
const lines = ['line 1', 'line 2', 'line 3'];
lines.map((l, i) => (
<React.Fragment key={i}>{l}{i < (lines.length - 1) ? <br /> : ''}</React.Fragment>
));
The output would be
line 1 <br />
line 2 <br />
line 3
Btw. you can also supply functions to react. My approach would be .forEach pairs of push(value); push(glue);, and afterwards pop() the trailing glue...
function() {
joinLike = [];
originData.forEach(function(v,i) {
joinLike.push(v);
joinLike.push(<br>);
})
joinLike.pop();
return joinLike;
}
To produce the requested solution, in React join doesn't seem to work so with map and reduce you can do it like this:
render() {
let myArray = [1, 2, 3];
return (
<div>
{myArray
.map(item => <div>{item}</div>)
.reduce((result, item) => [result, <div>|</div>, item])}
</div>
);
}
You could also try flatMap(). Browser support is coming, but until then you could use a polyfill. Only downside is you'd have to lose the last element.
Eg.
{myArray.flatMap(item => [<div>{item}</div>, <div>|</div>]).slice(0, -1)}
Or
{myArray.flatMap((item, i) => [
<div>{item}</div>,
i < myArray.length - 1 ? <div>|</div> : null
])
You can use a function like this one
function componentsJoin(components, separator) {
return components.reduce(
(acc, curr) => (acc.length ? [...acc, separator, curr] : [curr]),
[]
);
}
Or can use a package, found this one
https://www.npmjs.com/package/react-join
If you are using TypeScript, you can simply copy this file and use jsxJoin:
import { Fragment } from "react";
/**
* Join together a set of JSX elements with a separator.
*
* #see https://stackoverflow.com/q/33577448/5506547
*/
function jsxJoin(components: JSX.Element[], separator: any) {
// Just to be sure, remove falsy values so we can add conditionals to the components array
const filtered = components.filter(Boolean);
return filtered.length === 0
? null
: (filtered.reduce(
(acc, curr, i) =>
acc.length
? [
...acc,
// Wrap the separator in a fragment to avoid `missing key` issues
<Fragment key={i}>{separator}</Fragment>,
curr
]
: [curr],
[]
) as JSX.Element[]);
}
export { jsxJoin };
// typescript
export function joinArray<T, S>(array: Array<T>, separator: S): Array<T | S> {
return array.reduce<(T | S)[]>((p, c, idx) => {
if (idx === 0) return [c];
else return [...p, separator, c];
}, []);
}
// javascript
export function joinArray(array, separator) {
return array.reduce((p, c, idx) => {
if (idx === 0)
return [c];
else
return [...p, separator, c];
}, []);
}
// example
console.log(joinArray(["1", "2", "3"], 2));
// -> ["1", 2, "2", 2, "3"]
// example
// privacyViews -> JSX.Element[]
const privacyViews = joinArray(
privacys.value.map(({ key, name }) => {
return (
<button onClick={() => clickPrivacy(key!)} class={Style.privacyBtn}>
{name}
</button>
);
}),
<span class={Style.privacyBtn}>、</span>
);
This will cover all cases
const items = []
if(x1) {
items.push(<span>text1</span>)
}
if(x2) {
items.push(<span>text3</span>)
}
if(x3) {
items.push(<span>text3</span>)
}
return <div>
<>{items.reduce((result, item) => result.length > 0 ? [...result, ', ', item] : [item], [])}</>
</div>

Resources