Passing a data attribute through a stateless component to its HTML output - reactjs

Say I have this List stateless component:
const List: React.StatelessComponent<ListProps> = props => {
const listClasses =
'atom-list uk-list ' +
props.className +
(props.striped ? ' uk-list-striped' : '')
return (
<ul className={listClasses}>
{props.items.map((item, index) => {
const itemClasses = [item.classes, props.childClasses]
.filter(prop => prop)
.join(' ')
return (
<li key={index} className={itemClasses}>
{item.content}
</li>
)
})}
</ul>
)
}
And I use the List component in another stateless component, SortableList:
const SortableList: React.StatelessComponent<SortableListProps> = props => {
const classes = 'molecule-sortableList ' + props.className
const items = props.items.map((item, index) => {
return { content: <div><div className="uk-sortable-handle" />{item.content}</div> }
})
return <List className={classes} items={items} data-uk-sortable="handle: .uk-sortable-handle" />
}
Because I'm using UIKit to make the list sortable, I need to apply a data attribute to the List element when I use it in SortableList, and have that data attribute output in the resulting ul.
Applying the data attribute directly to the ul in List works (the attribute is output in the resulting HTML), but that's not ideal, since it requires that List knows something irrelevant about how it's being used.
I really want to apply the data attribute directly to List's usage and have that output in the resulting ul. Unfortunately, doing something like this in SortableList doesn't seem to work:
return <List className={classes} items={items} data-uk-sortable="handle: .uk-sortable-handle" />
Why doesn't this work, and what do I need to do to pass the data attribute into List and have it output in the resulting ul?

You can do this by using approach similar to the following:
const List: React.StatelessComponent<ListProps> = (props) => {
return (
<div {...props}>List</div>
);
};
The example is overly simplified and you should not destruct props directly as they contain some extra properties you do not want to be rendered as attributes. Instead you can copy all of the props properties that start for example with "data-" into separate object and destruct it instead.

Related

Conditionally assign ref in react

I'm working on something in react and have encountered a challenge I'm not being able to solve myself. I've searched here and others places and I found topics with similar titles but didn't have anything to do with the problem I'm having, so here we go:
So I have an array which will be mapped into React, components, normally like so:
export default ParentComponent = () => {
//bunch of stuff here and there is an array called arr
return (<>
{arr.map((item, id) => {<ChildComponent props={item} key={id}>})}
</>)
}
but the thing is, there's a state in the parent element which stores the id of one of the ChildComponents that is currently selected (I'm doing this by setting up a context and setting this state inside the ChildComponent), and then the problem is that I have to reference a node inside of the ChildComponent which is currently selected. I can forward a ref no problem, but I also want to assign the ref only on the currently selected ChildComponent, I would like to do this:
export default ParentComponent = () => {
//bunch of stuff here and there is an array called arr and there's a state which holds the id of a selected ChildComponent called selectedObjectId
const selectedRef = createRef();
return (<>
<someContextProvider>
{arr.map((item, id) => {
<ChildComponent
props={item}
key={id}
ref={selectedObjectId == id ? selectedRef : null}
>
})}
<someContextProvider />
</>)
}
But I have tried and we can't do that. So how can dynamically assign the ref to only one particular element of an array if a certain condition is true?
You can use the props spread operator {...props} to pass a conditional ref by building the props object first. E.g.
export default ParentComponent = () => {
const selectedRef = useRef(null);
return (
<SomeContextProvider>
{arr.map((item, id) => {
const itemProps = selectedObjectId == id ? { ref: selectedRef } : {};
return (
<ChildComponent
props={item}
key={id}
{...itemProps}
/>
);
})}
<SomeContextProvider />
)
}
You cannot dynamically assign ref, but you can store all of them, and access by id
export default ParentComponent = () => {
//bunch of stuff here and there is an array called arr and theres a state wich holds the id of a selected ChildComponent called selectedObjectId
let refs = {}
// example of accessing current selected ref
const handleClick = () => {
if (refs[selectedObjectId])
refs[selectedObjectId].current.click() // call some method
}
return (<>
<someContextProvider>
{arr.map((item, id) => {
<ChildComponent
props={item}
key={id}
ref={refs[id]}
>
})}
<someContextProvider />
</>)
}
Solution
Like Drew commented in Medets answer, the only solution is to create an array of refs and access the desired one by simply matching the index of the ChildElement with the index of the ref array, as we can see here. There's no way we found to actually move a ref between objects, but performance cost for doing this should not be relevant.

useRef in a dynamic context, where the amount of refs is not constant but based on a property

In my application I have a list of "chips" (per material-ui), and on clicking the delete button a delete action should be taken. The action needs to be given a reference to the chip not the button.
A naive (and wrong) implementation would look like:
function MemberList(props) {
const {userList} = this.props;
refs = {}
for (const usr.id of userList) {
refs[usr.id] = React.useRef();
}
return <>
<div >
{
userList.map(usr => {
return <UserThumbView
ref={refs[usr.id]}
key={usr.id}
user={usr}
handleDelete={(e) => {
onRemove(usr, refs[usr.id])
}}
/>
}) :
}
</div>
</>
}
However as said this is wrong, since react expects all hooks to always in the same order, and (hence) always be of the same amount. (above would actually work, until we add a state/any other hook below the for loop).
How would this be solved? Or is this the limit of functional components?
Refs are just a way to save a reference between renders. Just remember to check if it is defined before you use it. See the example code below.
function MemberList(props) {
const refs = React.useRef({});
return (
<div>
{props.userList.map(user => (
<UserThumbView
handleDelete={(e) => onRemove(user, refs[user.id])}
ref={el => refs.current[user.id] = el}
key={user.id}
user={user}
/>
})}
</div>
)
}

react dynamically add children to jsx/component

Situation
I'm attempting to render a component based on template jsx pieces. Reason being because I have a few of these situations in my app but with subtle customizations and I'd rather leave the business logic for the customizations in the respective component, not the factory.
Example
Parent Template
<div></div>
Child Template
<p></p>
In the render function I want to add n child components to the parent. So if n=4 then I would expect output like
<div>
<p></p>
<p></p>
<p></p>
<p></p>
</div>
I tried using parentTemplate.children.push with no avail because the group template is still JSX at this point and not yet a rendered component. How can I accomplish this task using jsx + react ?
Extra
Here is what my actual code looks like so far
render() {
let groupTemplate = <ListGroup></ListGroup>
let itemTemplate = (item) => {
return <ListGroup.Item>{item.name}</ListGroup.Item>;
}
if (this.props.itemTemplate) itemTemplate = this.props.itemTemplate;
let itemsJsx;
this.props.navArray.forEach((item) => {
groupTemplate.children.push(itemTemplate(item))
});
return (
[groupTemplate]
);
}
From the parent:
const groupTemplate = <ListGroup className='custom-container-classes'></ListGroup>
const itemTemplate = <ListGroup.Item className='customized-classes'></ListGroup.Item>
<NavigationFactory groupTemplate={groupTemplate} itemTemplate={itemTemplate}>
</NavigationFactory>
You could use render props, which is the best way to achieve something like that:
The parent:
<NavigationFactory
itemTemplate={itemTemplate}
render={children => <ListGroup className='custom-container-classes'>
{children}
</ListGroup>}
/>
The Child:
render() {
this.props.render(this.props.navArray.map(item => <ListGroup.Item key={item.id}>
{item.name}
</ListGroup.Item>))
}
This will let you define the container on your calling function and enables you to create the chidlren as you want.
Hope this helps.
Unclear on your question but I have attempted to figure out based on your code on what you are trying to do, see if it helps, else I'll delete my answer.
The mechanism of below code working is that it takes in groupTemplate which in your case you'll be passing in as a prop which then takes children within it and returns them composed all together. There's default children group template which you can use if no related prop is passed and you have a mapper function within the render which determines which template to use and renders n number of times based on navArray length.
componentDidMount() {
// setup default templates
if (!this.props.navButtonContainerTemplate) {
this.prop.navButtonContainerTemplate = (items) => {
return <ListGroup>{items}</ListGroup>
}
}
if (!this.props.navButtonTemplate) {
this.props.navButtonTemplate = (item) => {
return (
<ListGroup.Item>
{item.name}
</ListGroup.Item>
)
}
}
}
render() {
const itemsJsx = this.props.navArray.map(obj => {
const itemJsx = this.props.navButtonTemplate(obj)
return itemJsx;
});
return this.props.navButtonContainerTemplate(itemsJsx);
}
Pass the contents as regular children to the ListGroup via standard JSX nesting:
render() {
let itemTemplate = (item) => {
return <ListGroup.Item>{item.name}</ListGroup.Item>;
}
if (this.props.itemTemplate) itemTemplate = this.props.itemTemplate;
return (
<ListGroup>
{this.props.navArray.map(item => (
itemTemplate(item)
))}
</ListGroup>
);
}

How can I get the cumulative width of a list of components?

I have a list of "selections" that are displayed using a component. I need to find the rendered width of all these selections. My template looks like this:
{props.selections.map((chip: SelectOptionType) => {
return (
<Chip text={chip.label} />
)
}
Typically, in a non-React application, I'd probably put a class on the <Chip /> and use jquery to select elements of that class name, then loop over them and just sum the widths together:
let sum: number = 0;
$(".someClassName").forEach(($el) => sum += $el.offsetWidth);
I know the suggested way of doing something similar to this is using refs, but it seems you cant create an array of refs. I tried doing something like this:
{props.selections.map((chip: SelectOptionType, index: number) => {
chipsRefs[index] = React.createRef<HTMLDivElement>();
return (
<div ref={chipsRefs[index]}>
<Chip text={chip.label} />
</div>
)
}
But as I quickly learned each Ref inside chipsRefs ended up with a null current.
Now I'm a bit at a loss for this and have tried finding examples of this use case but have come up empty.
Can you try this ?
ref={ref => {
chipsRefs[index] = ref
}}
Try doing something like this: https://codesandbox.io/s/awesome-haibt-zeb8m
import React from "react";
class Selections extends React.Component {
constructor(props) {
super(props);
this._nodes = new Map();
}
componentDidMount() {
this.checkNodes();
}
checkNodes = () => {
let totalWidth = 0;
Array.from(this._nodes.values())
.filter(node => node != null)
.forEach(node => {
totalWidth = totalWidth + node.offsetWidth;
});
console.log(totalWidth);
};
render() {
const { selections } = this.props;
return (
<div>
{selections.map((value, i) => (
<div key={i} ref={c => this._nodes.set(i, c)}>
{value}
</div>
))}
</div>
);
}
}
export default Selections;
The function we defined in the ref prop is executed at time of
render.
In the ref call-back function, ref={c => this._nodes.set(i, c)}
we pass in the index (i) provided by .map() and the html element
(c) that is provided by the ref prop, in this case the div itself.
this._nodes.set(i, c) will create a new key-value pair in our
this._nodes iterable, one pair for each div we created. Now we have recorded HTML elements (nodes) to work with that contain all the methods we need to calculate the totalWidth of your rendered list.
Lastly in checkNodes() we get the .offsetWidth of each node to get our totalWidth.

How do you pass index into rowRenderer?

https://codesandbox.io/s/qYEvQEl0
I try to render a list of draggables, everything seems fine only that I can't figure out how to pass 'index' into rowRenderer
If I do rowRenderer=props => <Row {...props}/>, index is passed in sucessfully.
But if I do:
const SortableRow = SortableElement(Row)
rowRenderer=props => <SortableRow {...props}/> ,
index is blocked somehow, failed to pass into <Row/>
Basically, I don't understand what can go wrong when you wrap your <Row/> component with a HOC? Why some props get to pass in, others not?
Copy the index into a different, custom prop...
rowRenderer = props => {
console.log(props.index);
return <SortableRow {...props} indexCopy={props.index} />;
};
Then, inside the child component, refer to this custom prop instead.
const Row = ({ indexCopy , style }) => {
console.log(indexCopy);
return (
<div style={style}>
<span>drag</span>
<input placeholder={'haha'} />
<span>index={indexCopy || 'undefined'}</span>
</div>
);
};
I'm not too familiar with HOCs, but I suspect that the react-sortable-hoc library is stripping out the implicit index and key values. However, as long as you copy them over into their own custom props, you should be fine.

Resources