Add refs to react component when know the id - reactjs

I want to make the table on ant design scroll but ant design table does not have the onScroll (event)
Problem:
https://github.com/ant-design/ant-design/issues/5904
And they suggest the solution like this
componentDidMount() {
...
var tableContent = document.querySelector('.ant-table-body')
tableContent.addEventListener('scroll', (event) => {
// checking whether a selector is well defined
console.log('yes, I am listening')
let maxScroll = event.target.scrollHeight - event.target.clientHeight
let currentScroll = event.target.scrollTop
if (currentScroll === maxScroll) {
// load more data
}
})
...
}
He adds the scroll event on element with the id '.ant-table-body';
And also someone said 'better use refs' instead
Could you show me how to solve the problem (add scroll event on ant table) by using refs?
How can I use the refs to refer to the component when I know the id '.ant-table-body'. Note that I cannot access the component '.ant-table-body'.
something like this: tableRef = refs.findById('.ant-table-body')? or tableRefs = refs.addRefToId('.ant-table-body')

First add a ref to your table, like this:
<Table ref={table => { this.table = table }}
Then in componentDidMount you can add the same event listener to it like this:
componentDidMount() {
if(this.table) {
this.table.addEventListener('scroll', (event) => {
// do your stuff here
})
}
}

Related

Make a momentary highlight inside a virtualized list when render is not triggered

I have an extensive list of items in an application, so it is rendered using a virtual list provided by react-virtuoso. The content of the list itself changes based on API calls made by a separate component. What I am trying to achieve is whenever a new item is added to the list, the list automatically scrolls to that item and then highlights it for a second.
What I managed to come up with is to have the other component place the id of the newly created item inside a context that the virtual list has access to. So the virtual list looks something like this:
function MyList(props) {
const { collection } = props;
const { getLastId } useApiResultsContext();
cosnt highlightIndex = useRef();
const listRef = useRef(null);
const turnHighlightOff = useCallback(() => {
highlighIndex.current = undefined;
}, []);
useEffect(() => {
const id = getLastId();
// calling this function also resets the lastId inside the context,
// so next time it is called it will return undefined
// unless another item was entered
if (!id) return;
const index = collection.findIndex((item) => item.id === if);
if (index < 0) return;
listRef.current?.scrollToIndex({ index, align: 'start' });
highlightIndex.current = index;
}, [collection, getLastId]);
return (
<Virtuoso
ref={listRef}
data={collection}
itemContent={(index, item) => (
<ItemRow
content={item}
toHighlight={highlighIndex.current}
checkHighlight={turnHighlightOff}
/>
)}
/>
);
}
I'm using useRef instead of useState here because using a state breaks the whole thing - I guess because Virtuouso doesn't actually re-renders when it scrolls. With useRef everything actually works well. Inside ItemRow the highlight is managed like this:
function ItemRow(props) {
const { content, toHighlight, checkHighligh } = props;
const highlightMe = toHighlight;
useEffect(() => {
toHighlight && checkHighlight && checkHighligh();
});
return (
<div className={highlightMe ? 'highligh' : undefined}>
// ... The rest of the render
</div>
);
}
In CSS I defined for the highligh class a 1sec animation with a change in background-color.
Everything so far works exactly as I want it to, except for one issue that I couldn't figure out how to solve: if the list scrolls to a row that was out of frame, the highlight works well because that row gets rendered. However, if the row is already in-frame, react-virtuoso does not need to render it, and so, because I'm using a ref instead of a state, the highlight never gets called into action. As I mentioned above, using useState broke the entire thing so I ended up using useRef, but I don't know how to force a re-render of the needed row when already in view.
I kinda solved this issue. My solution is not the best, and in some rare cases doesn't highlight the row as I want, but it's the best I could come up with unless someone here has a better idea.
The core of the solution is in changing the idea behind the getLastId that is exposed by the context. Before it used to reset the id back to undefined as soon as it is drawn by the component in useEffect. Now, instead, the context exposes two functions - one function to get the id and another to reset it. Basically, it throws the responsibility of resetting it to the component. Behind the scenes, getLastId and resetLastId manipulate a ref object, not a state in order to prevent unnecessary renders. So, now, MyList component looks like this:
function MyList(props) {
const { collection } = props;
const { getLastId, resetLastId } useApiResultsContext();
cosnt highlightIndex = useRef();
const listRef = useRef(null);
const turnHighlightOff = useCallback(() => {
highlighIndex.current = undefined;
}, []);
useEffect(() => {
const id = getLastId();
resetLastId();
if (!id) return;
const index = collection.findIndex((item) => item.id === if);
if (index < 0) return;
listRef.current?.scrollToIndex({ index, align: 'start' });
highlightIndex.current = index;
}, [collection, getLastId]);
return (
<Virtuoso
ref={listRef}
data={collection}
itemContent={(index, item) => (
<ItemRow
content={item}
toHighlight={highlighIndex.current === index || getLastId() === item.id}
checkHighlight={turnHighlightOff}
/>
)}
/>
);
}
Now, setting the highlightIndex inside useEffect takes care of items outside the viewport, and feeding the getLastId call into the properties of each ItemRow takes care of those already in view.

React testing library - how to test setstate incustom onchange method of child component which updates state of child in parent component

I am new to react testing. I have a child component (to which I have no edit access) which is a table along with search box option. It has a event onSearchChange which will update the state with text if value entered in search box. Then we have logic for search using that text. Both set state and search logic is in parent component on which child is rendered. Its working perfectly fine in real time, but in unit testing I am not sure why the onSearchChange is not updating the state(number of rows in table) when I change the value of search box.
This onSearchChange is of type
onSearchChange?: (e: React.SyntheticEvent, value: string) => void;
I have also tried firevents and userevent.type. But nothing works. I have code coverage problem because of this since I need to cover code of search logic.
Please help. Thank you !
Parent Component
const [searchText, setSearchText] = useState("");
//logic to perform search
return(<div className="patient-list">
<div className="table">
<SearchTable onSearchChange={(e) => {
setSearchText((e.target as HTMLInputElement).value)}} table={result} />
</div>
</div>
)
test file
it("should search the table", () => {
const { container } = render(<ParentComponent />);
let searchInputBox = screen.getByPlaceholderText ("Search the results") as HTMLInputElement
act(() => {
searchBoxInput.value = "Age";
ReactTestUtils.Simulate.change(searchBoxInput);
const rows = container.getElementsByClassName("class1")
expect(rows).toHaveLength(6);
});
});

get access to selected row data from outside table component

I have a common component for the table which is being used in different pages through the app. Now the selection of rows is being saved inside the table component itself. I want to access the selected rows of data from its parent component whenever button pressed
Here is an example
https://codesandbox.io/s/naughty-pond-3e5jp
What you can do is pass maintain some state for selectedRows in you parent component then pass a callback to Table component that'll be called every time a row is selected or unselected. Here I have made some changes to your sandbox, hope it helps :)
https://codesandbox.io/s/naughty-sun-3nhue?file=/src/App.js
#jiltender
i fix my problem by doing this to get row data outside the table
rowProps={row =>(console.log(row)
)}
add this on parent components where u call the Table
<Table
sortByProps={[]}
columns={columns}
rowProps={row =>(setSelectedItems(row))} /// pass row props
/>
and the table side
add this in
function props
` `rowProps = () => ({}) })
function Table({ columns, data, showSpinnerProp, hiddenColumnsProps,getCellProps, getRowProps, height, sortByProps, source,
setSelectedItems,rowProps = () => ({}) }) {
this will return u all the select data you can call it before return
useEffect(() => {
setSelectedItems = selectedFlatRows;
rowProps(selectedFlatRows)
}, [selectedFlatRows]);
}
Create a state variable outside of the component, something like
const [selectedRows, setSelectedRows] = useState([]);
And then pass this down to the component through the props. then on the button you can have some code like this
onPress = { () => props.setSelectedRows(selectedRows) }
Connect the parent component with the Table with useEffects. A pseudo example below.
function TableComponent ({columns, data.., setSelectedRows}){
const { ..., selectedFlatRows, state:{selectedRowIds} } = useTable({..})
useEffect(() =>{
setSelectedRows(selectedFlatRows)
}, [selectedFlatRows])
......
}
function ParentComponent (props){
const [selectedRows, setSelectedRows = useState ([])
useEffect(() =>{
// do your custom handle of the selection
},[selectedTickers])
return (
......
<Table
columns={columns}
....
setSelectedRows={setSelectedTickers}>
</Table>
......
)
}

How to catch remove event in react-select when removing item from selected?

first of all, thanks for awesome work. It makes a lot easier for me. Then here i want to catch remove event, how can I do that? I read the documentation and could not find remove event
I don't think they have an event for that. They just have the onChange.
So, to detect if some option was removed, you would have to compare the current state with the new values the onChange emitted.
Example:
handleOnChange(value) {
let difference = this.state.selected.filter(x => !value.includes(x)); // calculates diff
console.log('Removed: ', difference); // prints array of removed
this.setState({ selected: value });
}
render() {
return (
<div>
<Select
multi={this.state.multi}
options={this.state.options}
onChange={this.handleOnChange.bind(this)}
value={this.state.selected}
showNewOptionAtTop={false}
/>
</div>
);
}
Demo: https://codesandbox.io/s/7ymwokxoyq
React Select exposes a variety of eventListeners to you via props. The onchange function prop now has the following signature. (value: ValueType, action: ActionType).
Accordingly, you can get these events:
select-option, deselect-option, remove-value, pop-value, set-value, clear, create-option, through which you can handle creation and deletion.
Please refer to documentation
They don't have any event for that. I was facing the same problem but in my case, I was needing both the added and removed item. In case someone wants the both values
import 'difference' from 'lodash/difference'
this.currentTags = []
handleChange = (options) => {
const optionsValue = options.map(({ value }) => value)
if (this.currentTags.length < options.length) {
const addedElement = difference(optionsValue, this.currentTags)[0]
this.currentTags.push(addedElement)
console.log("addedElement",addedElement)
//call custom add event here
}
else {
let removedElement = difference(this.currentTags, optionsValue)[0]
console.log("removedElement", removedElement)
// call custom removed event here
let index = this.currentTags.indexOf(removedElement)
if (index !== -1) {
this.currentTags.splice(index, 1)
}
}
}
This code of snippet is working for me to know if any item is removed
I am calling handle change function on onChange event in reat-select
const [selectedGroups, setSelectedGroups] = useState([]);
const handleChange = (value) => {
const diifer = value.length <= selectedGroups.length;
if(diifer){
// item is removed
}
var arr = value.map((object) => object.value);
setSelectedGroups(arr);
};

how do I programmatically create instance of react component?

I have a table row component that checks to see if a custom component is specified and is supposed to programmatically take that React component (column.customComponent) and render the data using it. However, I cannot seem to instantiate the components.. all I get is the data as a string.
Here is what I have:
const cellContent = typeof(column.customComponent === 'undefined') ?
rowData[column.columnName] :
{
type: column.customComponent,
props: {
data: rowData[column.columnName],
rowData
}
};
console.log(cellContent); displays something like:
the function representation of the React component
or
function DateCell() ....
I'm not sure if I'm just using the wrong syntax when assigning cellContent or if I should be doing something with factories or what.
How should I do this?
I use this helper method to create components and return the instance.
static async renderComponentAt(componentClass, props, parentElementId){
let componentId = props.id;
if(!componentId){
throw Error('Component has no id property. Please include id:"...xyz..." to component properties.');
}
let parentElement = document.getElementById(parentElementId);
return await new Promise((resolve, reject) => {
props.ref = (component)=>{
if(component){
resolve(component);
}
};
let element = React.createElement(componentClass, props, null);
ReactDOM.render(element, parentElement);
});
}

Resources