How can I use dataset with cloneelement? - reactjs

How can I set a data attribute on an element when using cloneelement?
I've tried it this way.
const clonedItem = React.cloneElement(item, {
dataset: { "data-test": 123 },
dataTest: 123,
})
But that doesn't set any attribute in my html...

May be you are storing a custom component in item variable i.e
let item = <ItemComponent />.
Just store html element in item variable i.e
let item = <div><h1>Hello</h1></div>
And that will set attribute in div element.
If you use custom component, you need to use the props passed in the component.
i.e ItemComponent will be as
function ItemComponent({dataTest}){
return (
<div dataTest={dataTest}>
<h1>Hello</h1>
</div>
);
}

The one that worked for me is by settings the key as string
const clonedItem = React.cloneElement(item, {
'data-test': 123
})

Related

How to get or filter a bunch of childNodes by their style class name in ReactJs

I am having trouble figuring out how to get or filter a bunch of childNodes by their style class name inside my useEffect. Using ReactJs v18.
Straight after the line with: const circleElements = launcherCircle!.childNodes; I would like to get/filter the div's with the class name 'launcherPos' so I can position them in a circle formation.
const LauncherComponent = () => {
const launcherCircleRef = useRef<HTMLDivElement>(null);
let modules: Module[] | null = GetModules();
const enableLauncher = (module: Module) => {
return !module.IsEnabled ? styles['not-active'] : null;
};
useEffect(() => {
const launcherCircle = launcherCircleRef.current;
const circleElements = launcherCircle!.childNodes;
let angle = 360 - 190;
let dangle = 360 / circleElements.length;
for (let i = 0; i < circleElements.length; i++) {
let circle = circleElements[i] as HTMLElement;
angle += dangle;
circle.style.transform = `rotate(${angle}deg) translate(${launcherCircle!.clientWidth / 2}px) rotate(-${angle}deg)`;
}
}, []);
if (modules == null){
return <Navigate replace to={'/noaccess'} />
} else {
return (
<div data-testid="Launcher" className={styles['launcherContainer']} >
<div className={styles['launcherCircle']} ref={launcherCircleRef}>
{modules.map(function (module: Module, idx) {
return (
<div key={idx} className={styles['launcherPos']} ><div className={`${styles['launcherButton']} ${enableLauncher(module)}`}><img src={module.ImagePath} alt={module.Prefix} /></div></div>
)
})}
<div className={styles['launcherTextDiv']}>
<span>TEST</span>
</div>
</div>
</div>
)
}
};
export default LauncherComponent;
From what I've read getElementsByClassName() is not advisable practise because of the nature of ReactJs and it's virtual DOM.
I tried the following filter but I think with React garburling the class name I didn't get anything back.
const launcherChildren = launcherCircle!.children;
const circleElements = [...launcherChildren].filter(element => element.classList.contains('launcherPos'));
Maybe there's a way to ref an array of the just the children with the launcherPos class???
There must be a couple of different ways, but, they are eluding me.
When you filter/map an array of HTMLElements, the results are in the form of objects, which contains properties like, props, ref etc.
Since className is a prop on the element, you should try looking for the class name by digging into the props key.
Simply put, all the props that you pass to the element, like onClick, onChange, value, className are stored under the props property.
You can filter the results by converting the class name into an array and further checking if it contains the target string (launcherPos in this case).
Your code should look something like this:
const circleElements = [...launcherChildren].filter(element=>element.props.className.split(' ').includes('launcherPos'))
The above method could be used when an array directly holds elements. E.g: [<div></div>,<div></div>...].
The approach that you've followed is correct, except for the way you are selecting the elements by their class names. I can see that you are using CSS modules in this component, meaning all the class names exist as properties on the imported object(styles in this case), so when you use contains('launcherPos') you are essentially checking for the presence of a string, but when using CSS modules, class names are available only as object properties, that's the reason you are getting an empty array. Simply update launcherPos to styles.launcherPos and that shall fix the issue.
All-in-all your useEffect function should look something like this:
useEffect(() => {
const launcherCircle = launcherCircleRef.current;
const launcherChildren = launcherCircle!.children;
const circleElements = [...launcherChildren].filter(element => element.classList.contains(styles.launcherPos)); //change launcherPos to styles.launcherPos
let angle = 360 - 190;
let dangle = 360 / circleElements.length;
for (let i = 0; i < circleElements.length; i++) {
let circle = circleElements[i] as HTMLElement;
angle += dangle;
circle.style.transform = `rotate(${angle}deg) translate(${launcherCircle!.clientWidth / 2}px) rotate(-${angle}deg)`;
}
}, []);

React state value is not retained when value is set using useState within useEffect with dep []

I am using Fluent UI Details List and trying to make row editable on icon click.
My code is as below. On first run, it shows grid with empty data. Then it goes to useEffect (I tried both useEffect and useLayoutEffect, same behaviour) and data is fetched and stored in state. It also fires the render and the grid shows all the rows per the data. All good so far.
When row > cell 1 is double-clicked on the grid, I turned the row to editable mode. That is also working.
For each editable column, I have a different onChange event attached. So, when any input text box/dropdown cell value changes, it fires the respective onChange event callback function.
Within this cell change callback event, it gets item id and final changed value as input aurguments. And using them, data array will be updated and stored in state. This is my expectation.
But when I read the current data array from state within the callback function, it is empty.
So, basically, problem is that state value stored from useEffect is not retained.
There is no other code line where data array state is updated. So, no chance that the data array is reset by code.
If anyone has any idea or faced, solved such issue, let me know. Thanks in advance.
Adding few things which I tried,
I tried using the class component and it worked. only difference is that instead of useEffect, I used componentDidMount and instead of useState, I used this.setState. Not sure what is other difference within class and function component?
The same code works in class component but not in function component.
I tried using the same function component and instead of async call within useEffect, I made direct sync fetch call before render and loaded data in state as initial value. Then, it worked.
So, it fails only when the data is fetched async mode within useEffect and set to state from there.
My problem is resolved after converting to class component.
but want to understand what is the issue within my function component code.
/** function component */
const [dataItems, setDataItems] = useState<IScriptStep[]>([]);
const [groups, setGroups] = useState<IGroup[]>([]);
/** Component Did Mount */
useLayoutEffect(() => {
props.telemetry.log(`useEffect - []`, LogLevel.Debug);
(async () => {
let scriptSteps = await props.dataHelper.retrieveScriptSteps();
setDataItems(scriptSteps);
let groups = getGroups(scriptSteps);
setGroups(groups);
props.telemetry.log(`Data updated in state`, LogLevel.Debug);
})();
}, []);
/** Render */
return (
<div className="SubgridMain">
{props.telemetry.log(`render`, LogLevel.Debug)}
<div className="List">
<DetailsList
componentRef={_root}
items={dataItems}
groups={groups}
columns={columns}
ariaLabelForSelectAllCheckbox="Toggle selection for all items"
ariaLabelForSelectionColumn="Toggle selection"
checkButtonAriaLabel="select row"
checkButtonGroupAriaLabel="select section"
onRenderDetailsHeader={_onRenderDetailsHeader}
onRenderRow={_onRenderRow}
groupProps={{ showEmptyGroups: true }}
onRenderItemColumn={_onRenderItemColumn}
onItemInvoked={_onItemInvoked}
compact={false}
/>
</div>
</div>
);
/** on change cell value callback function */
const _onChangeCellName = (entityId : string, fieldName:string, finalValue:string) => {
let currentItems = dataItems;
// create new data array
let toUpdateState: boolean = false;
let newItems = currentItems.map((item) => {
if (item.key === entityId) {
if (item.name !== finalValue) {
toUpdateState = true;
item.name = finalValue ?? null;
}
}
return item;
});
if (toUpdateState) setDataItems(newItems);
};
/** columns configuration is set as below */
let columns : IColumn[] = [
{
key: 'name',
name: 'Name',
fieldName: 'name',
minWidth: 300,
isResizable: true,
onRender: this._onRenderCellName,
},
..
..
]
/** Render Name cell */
private _onRenderCellName(item?: IScriptStep, index?: number, column?: IColumn) {
if (item) {
let stepName = item?.name ?? '';
if (item.isEditable) {
let propsNameTextEditor: ITextEditorWrapperProps = {
entityId: item.key,
fieldName: 'name',
initialText: stepName,
multiline: true,
required: true,
setFinalValue: this._onChangeCellName,
};
return <TextEditorWrapper {...propsNameTextEditor} />;
} else {
return (
<div className="ReadOnly">
<p>{stepName}</p>
</div>
);
}
} else return <></>;
};

How can I create a parent html element by appending sub element from an object?

In my react app I need to return a line which will be created based on a list.
Here is the object,
searchCriteria: {
op_company: "039",
doc_type: "ALL"
}
and in my UI, i need to show it as a paragraph with bold values. So the hard coded code would be like below
<p>Download request for op_company: <b>{searchCriteria.op_company}</b>, doc_type: <b>{searchCriteria.doc_type}</b></p>
But the object(searchCriteria) will be changed based on the user request. So I tried like below.
const getSearchCriteria = (criteria) => {
let searchCriteria = []
searchCriteria.push('Download request for')
Object.keys(criteria).forEach((key) => {
if(criteria[key] !== '') {
searchCriteria.push(` ${key}: ${criteria[key]},`)
}
});
return searchCriteria;
}
return (
<p>
{getSearchCriteria(searchCriteria).map((item) => <span key = {item}>{item}</span>)}
</p>
);
here i'm getting the expected output. But I can't get the value as bold (highlighted). Is there another way to directly deal with html elements?

Render some text if array.filter returns 0 in React?

I have a filter on an array in the render function in a React component:
someArray.filter(item => {
if (item.name.includes(searchText))return true
}).map(item=>{
return <h1>{item.name}</h1>
});
How can I elegantly display some text along the lines of "No search results" when no items are being returned by the map function?
There are a few ways you can do this. You can use a ternary operator (and also shorten your callbacks):
const filtered = someArray.filter(item =>
item.name.includes(searchText)
);
//Then, in your JSX:
{
filtered.length > 0 ?
filtered.map((item, key) =>
<h1 key={key}>{item.name}</h1>
)
:
<h1>No search results</h1>
}
This checks if there are any filtered results. If so, it will map them to h1s that have the name of the item. If not, then it will simply render a single h1 with the text 'No search results'.
One possible way is, instead of putting this code directly inside JSX render method, put it inside a method and call that method from render.
Like this:
_filterItem(){
const arr = someArray.filter(item => item.name.includes(searchText))
if(!arr.length) return <div>No data found</div>;
return arr.map(item => <h1 key={/*some unique value*/}>{item.name}</h1>)
}
render(){
return(
<div>{this._filterItem()}</div>
)
}
Suggestion:
With filter and map you can use concise body of arrow function instead of block body, for more details check MDN Doc.
Short and concise:
someArray.map(({name}) => name.includes(searchText) && <h1>{name}</h1>)

React JSX - dynamic html attribute

In JSX, we can indicate attribute value dynamically like:
<div className={this.state.className}>This is a div.</div>
Is it possible to indicate attribute (including attribute name and attribute value) dynamically? Like:
const value = emptyValue ? "" : "value='test'";
<input {value} />
That means, once emptyValue is true, "input" tag should not include "value" attribute (value="" is different from no value attribute, as one is show empty in input field, another is show existing text in input field).
ES6 object expansion only works for objects. Therefore to generate a dynamic attribute, try something like this:
const value = emptyValue ? {} : { value: 'test' }
<a {...value} ></a>
Note value will always be an object.
you can insert whole element in if statement in render function, but before return like this:
render() {
var input = (<input />);
if (!emptyValue) {
input = (<input value='test'/>)
}
return (
<div>
{input}
</div>
)
}

Resources