React Virtualized: Table rows get cut off when autoHeight is on - javascript

I am currently trying to implement React Virtualized to replace an laggy table but am running into an issue. I am using WindowScroller, AutoSizer, Table, and Column from React Virtualized,
In my 400 row table, about 30 rows appear before they stop showing up (as in those DOM elements have not been rendered). However, the table appears to be the correct height. Here is a picture to help visualize:
From what I can tell, the culprit is (or related to) autoHeight on the <Table /> element. When I remove it, I can scroll through all the row elements within the Table. However, this breaks the desired functionality of being able to scroll the page, not the Table.
Things I have tested so far:
It occurred to me that the issue might be with the scrollElement on WindowScroller since the container element for my table has overflow: scroll; height: 100vh. When I tried setting the scrollElement property to this element none of my rows would render. For testing purposes, I also tried removing this container so that window would handle scrolling, but this didn't solve the bug either.
I have tried replicating this example as closely as possible, but no dice so far.
Finally, here is a simplified code snippet:
import React, { Component } from "react";
import { Table, Column, WindowScroller, AutoSizer } from "react-virtualized";
import "react-virtualized/styles.css";
import "./style.scss";
class AnalyticsResponsesReportTable extends React.Component {
constructor(props) {
...
}
//Methods...
render() {
const data = this.props.values.data;
return (
<div className="AnalyticsResponsesReportTable">
{data.length && (
<WindowScroller>
{({
height,
isScrolling,
registerChild,
onChildScroll,
scrollTop
}) => (
<div>
<AutoSizer disableHeight>
{({ width }) => (
<Table
ref="Table"
headerHeight={40}
height={height}
width={width}
autoHeight
rowCount={data.length}
rowHeight={40}
rowGetter={({ index }) => data[index]}
className="AnalyticsResponsesReportTable__table"
onRowClick={this.handleRowClick}
>
//Columns rendering here
</Table>
)}
</AutoSizer>
</div>
)}
</WindowScroller>
)}
</div>
);
}
}
export default AnalyticsResponsesReportTable;

After trying out many different combinations, the root issue was that I had not set scrollTop={scrollTop} on Table. I also needed to set scrollElement to the appropriate container. The issue took so long to solve since both are needed, and I guess I had just tested one without the other

Related

React: Get DOM data from sibling elements

(I am new to React and TypeScript so apologies ahead of time)
I am trying to build a bar that I can fill between 2 elements and I think the best way might be to figure out where those two elements are after the component is built and then do some logic in order to size the bar properly.
For reference here is what im trying to build:
How / is it possible to get the DOM using something like getBoundingClientRect() in order to know how big and where the bar should start and stop?
Here is the component set up so far:
import React from "react";
import './CardLineItem.css';
import CardLineItemImage from "./CardLineItemImage";
import { FaChevronRight } from 'react-icons/fa';
import CardLineItemProgressBar from "./CardLineItemProgressBar";
export default class CardLineItem extends React.Component {
render() {
return (
<div className="CardLineItem">
<CardLineItemImage cardUrl="platinum-card.png" altText="platinum-card"/>
<FaChevronRight className="CardLineItemChevron" size="25%"/>
<CardLineItemProgressBar />
</div>
)
}
}
I looked into componentDidMount and this.children.toArray but that never returns anything. Suggestions on how I could best do this? CardLineItemProgressBar is the element I need to be flexible depending on the browser size.
Thanks!
Rather than reading the size and position of the other items, and making adjustments before the DOM paints, it's much better for performance to rely on CSS to do something like this.
I would highly recommend seeing if flex or grid can fulfill what you're trying to do here.
By bringing the progress bar component between the two other components in the JSX tree like this ...
<div className="CardLineItem">
<CardLineItemImage cardUrl="platinum-card.png" altText="platinum-card"/>
<CardLineItemProgressBar className="CardLineItemProgress" />
<FaChevronRight className="CardLineItemChevron" size="25%"/>
</div>
... you can add the flex style to the .CardLineItem and a .CardLineItemProgress class to have your progress bar grow accordingly to fill the space.
.CardLineItem {
// ... your other styles
display: flex;
gap: 10px;
}
.CardLineItemProgress {
flex-grow: 1;
align-self: flex-end;
}
That said, if you really would prefer to add JS calculations to manually calculate and set the size, getSnapshotBeforeUpdate() should be what you're looking for.
And if you ever switch to React functional components with hooks (standardized after React 16.8), then the hook you'd be looking for is useLayoutEffect()

How can I use react-zoom-pan-pinch to allow users to view all of a chart that changes size based on user input?

I've designed and built an interactive Org Chart that allows users to view business organizations in a hierarchical format. By default only the first row below the root node of the chart is visible, but users can click to expand the chart further, thereby changing the size of the chart. Users can also drag and drop nodes to simulate a reorganization of the business.
I'm currently using react-zoom-pan-pinch to allow users to zoom and pan the chart. It works very well when the org chart has not been expanded too much, but becomes problematic at larger chart scales.
The problem is that the organizations being represented by the chart are very broad in comparison to their depth, meaning a fully expanded chart is a horizontal rectangle, not a square. react-zoom-pan-pinch will only allow me to zoom out to the maximum vertical extent of the chart, meaning users can't view a fully expanded organization without scrolling from side to side. This is not an acceptable behavior.
This is for work, so I cannot post code without violating numerous agreements. Instead I have linked to the react-zoom-pan-pinch documentation and will go over what I have tried changing.
The first place I looked was the TransformWrapper Props section of the documentation.
There I found the inititalScale, minScale, and maxScale props.
I can set the initialScale prop to a value of less than 1, and obtain something close to the result I want at first. Setting it to 0.5 results in the chart being zoomed out further than normally possible, but when I zoom in to a value of 1 I am unable to zoom back out. This was expected, as the minScale prop was still set to 1.
Having checked that the props indeed work, I went ahead and set minScale to 0.5, assuming I would be able to zoom back out to the initial view seen when initialScale is set to 0.5. This seemed like it should work, but it did not. Even with the minScale prop set to 0.5, I am unable to zoom back out after zooming in to a value of 1. This is very strange to me, as the acceptance of 0.5 as the initialScale prop and subsequent rendering of the chart indicates that values below 1 are acceptable.
I am now messing around with the rest of the props listed in the documentation, but have yet to achieve the desired result (infinite zoomout).
I believe the root of the issue is that react-zoom-pan-pinch is meant for images, not things that change size and aspect ratio, but it is a good package and I would prefer to keep using it.
Is anyone familiar enough with this package to know the settings I should be using to allow infinite zoom out, and if so what are those settings?
I discovered the answer to my own question. It turns out the minScale, maxScale, and other props were not being passed to the component properly. Once they are passed properly the package works very well. Here's my explanation/fix
The documentation suggests doing this:
import { TransformWrapper, TransformComponent } from "react-zoom-pan-pinch";
class Example extends Component {
render() {
return (
<TransformWrapper
initialScale={1}
minScale={0.5}
maxScale={7}
initialPositionX={200}
initialPositionY={100}
>
{({ zoomIn, zoomOut, resetTransform, ...rest }) => (
<React.Fragment>
<div className="tools">
<button onClick={() => zoomIn()}>+</button>
<button onClick={() => zoomOut()}>-</button>
<button onClick={() => resetTransform()}>x</button>
</div>
<TransformComponent>
<img src="image.jpg" alt="test" />
<div>Example text</div>
</TransformComponent>
</React.Fragment>
)}
</TransformWrapper>
);
}
}
The above doesn't work, and the minScale and maxScale props aren't passed to the component. If you open the React dev tools in your browser and go to TransformWrapper, you'll see the default values of 1 for minScale and 8 for maxScale, not the values you entered in your code.
You can solve the problem by creating an object:
const transformOptions = {
initialScale: 1,
minScale: 0.5,
maxScale: 2
}
Then setting an options prop inside the TransformWrapper component equal to the object, like so:
import { TransformWrapper, TransformComponent } from "react-zoom-pan-pinch";
class Example extends Component {
render() {
return (
<TransformWrapper
initialScale={1}
options={transformOptions}
initialPositionX={200}
initialPositionY={100}
>
{({ zoomIn, zoomOut, resetTransform, ...rest }) => (
<React.Fragment>
<div className="tools">
<button onClick={() => zoomIn()}>+</button>
<button onClick={() => zoomOut()}>-</button>
<button onClick={() => resetTransform()}>x</button>
</div>
<TransformComponent>
<img src="image.jpg" alt="test" />
<div>Example text</div>
</TransformComponent>
</React.Fragment>
)}
</TransformWrapper>
);
}
}
The same thing applies to pan, wheel, and zoom options. They don't work if set directly in the component as suggested by the documentation, but do work if you create objects like I did above.

Conditionally Rendering Markup (JSX) vs. CSS `display: none` - Which is better practice?

Say I've got a component with identical content, but presented two totally different markup structures depending on the device (desktop viewports or mobile viewports).
In this situation when the viewport is below or above certain width or breakpoint (for this example 768px), I want to show one over the other.
A common situation for something like this might be the Navigation, where at Desktop views you have a simple navigation bar in the header of the page, whilst at Mobile views you have a more complex navigation menu that slides in and out:
import React from 'react';
import './Navigation.scss';
const Navigation = () => {
return (
<div className="navigation">
<div className="mobile-navigation-container">
<MobileNavigation />
</div>
<div className="desktop-navigation-container">
<DesktopNavigation />
</div>
</div>
);
};
Solution 1:
A simple solution to achieve this is to use CSS:
.navigation {
.mobile-navigation-container {
#media (min-width: 768px){
display: none;
}
}
.desktop-navigation-container {
#media (max-width: 767px){
display: none;
}
}
}
However, the issue here is that I still have both views in the DOM, even though one is not visible.
Solution #2:
Alternatively, I can use a resize listener and piece of state in my JSX component to conditionally render the correct component depending on the viewport width I can calculate using window.innerWidth:
import React, { Component } from 'react';
const isClient = typeof window !== 'undefined';
class Navigation extends Component {
state = {
viewportWidth: 0,
}
componentDidMount() {
if (isClient) {
this.updateWindowDimensions();
window.addEventListener('resize', this.updateWindowDimensions);
}
}
componentWillUnmount() {
if (isClient) window.removeEventListener('resize', this.updateWindowDimensions);
}
updateWindowDimensions = () => {
this.setState({ viewportWidth: window.innerWidth });
}
render() {
const { viewportWidth } = this.state;
return (
<div className="navigation">
{viewportWidth <= 768 && (
<div className="mobile-navigation-container">
<MobileNavigation />
</div>
)}
{viewportWidth > 768 && (
<div className="desktop-navigation-container">
<DesktopNavigation />
</div>
)}
</div>
);
}
This solves the issue of having duplicate content on the DOM. Which I'd guess is better for Search Engine Crawlers.
However, this somewhat makes my JSX more complicated, and I have the feeling that the CSS breakpoint is cleaner, smoother implementation in terms of performance, rather than using a JavaScript resize listener (though I can't find solid sources to advise one over the other).
My question is which of these two implementations is better practice and why?
The second approach Solution #2 is very good as compared to Solution #1. Because #1 has unnecessary and unwanted elements in DOM Object which is also confusing for react. Although it is not a good practice in any other languages as well. But in #2 you are not rendering unwanted contents this will improve smooth running of your code and debugging and designing is also easy in this approach.
Definitely the 2nd one even though it involves more lines of code, the overall performance outcome is much better because you don’t have extra pieces of DOM elements laying unnecessarily here and there in the page.
What’s more important is the flexibility provided by solution 2, what if you have to change the underlying markup on different screens in the future? (ex. hide some columns in smaller screens)

How to get Xterm.js resize properly?

tl;dr
I've created a React wrapper to render an array of log messages into a terminal but resizing is giving a weird output (see screenshot). (There is a React-Wrapper on NPM but that wasn't working for my use-case - caused screen flickering)
I'm working on a feature for Guppy where I'm adding Xterm.js for the terminal output.
The PR can be found here.
I've added xterm because of hyperlink scanning/parsing and that is working.
But I'm stuck with getting resize to work. If I'm starting the devServer in the app and wait for some text it will display with correct letter width.
If I reduce the size I'm getting an output with an incorrect letter width.
Like in the following screenshot:
It is always looking correct in the not resized state but after resize it will get the wrong display - so this will happen for enlarging & shrinking the screen width.
The output should look similar to the following screenshot (maybe with some wrapped lines):
I think this is caused by Fit addon or the way I'm handling resizing with the resize observer but I'm not sure.
The span style of xterm letter are getting a width of NaNpx like in the following screenshot:
Is this caused by a media query I'm using? I haven't seen that yet maybe I have to temporarily disable all media queries to see if that's causing the behaviour.
What I have tried so far:
Wrapped this.xterm.fit() into a setTimeout(func, 0) but with-out an effect
Modified some of the styles I'm using but I haven't found the cause.
Code
The code I'm using can be found on Github branch feature-terminal-links but here I'd like to extract the parts I've added to get Xterm to work with React:
I created a styled-component XtermContainer as a div so I can add Xterm styles and own styling. The following code is inside render and will be our xterm.js container (innerRef will be used later in ComponentDidMount to intialize Xterm with that container):
<XtermContainer
width={width}
height={height}
innerRef={node => (this.node = node)}
/>
Init xterm in componentDidMount with the container above:
componentDidMount() {
Terminal.applyAddon(webLinks);
Terminal.applyAddon(localLinks);
Terminal.applyAddon(fit);
this.xterm = new Terminal({
convertEol: true,
fontFamily: `'Fira Mono', monospace`,
fontSize: 15,
rendererType: 'dom', // default is canvas
});
this.xterm.setOption('theme', {
background: COLORS.blue[900],
foreground: COLORS.white,
});
this.xterm.open(this.node);
this.xterm.fit();
/* ... some addon setup code here (not relevant for the problem) ... */
}
Added react-resize-observer inside of the wrapper that is also containing the terminal container so I can trigger this.xterm.fit() if the size changes (in the repo there is a setTimeout wrapper around for testing).
<ResizeObserver onResize={() => this.xterm && this.xterm.fit()} />
Using componentDidUpdate(prevProps, prevState) to update the terminal and scroll the terminal to the bottom if the component is getting new logs:
componentDidUpdate(prevProps, prevState) {
if (prevProps.task.logs !== this.state.logs) {
if (this.state.logs.length === 0) {
this.xterm.clear();
}
for (const log of this.state.logs) {
/*
We need to track what we have added to xterm - feels hacky but it's working.
`this.xterm.clear()` and re-render everything caused screen flicker that's why I decided to not use it.
Todo: Check if there is a react-xterm wrapper that is not using xterm.clear or
create a wrapper component that can render the logs array (with-out flicker).
*/
if (!this.renderedLogs[log.id]) {
this.writeln(log.text);
this.xterm.scrollToBottom();
this.renderedLogs[log.id] = true;
}
}
}
}
Ideas I have to find the cause:
Check ResizeObserver code. (see update below)
Try to find why xterm css is getting a NaN width. Is Xterm.js using the style width of the container? If yes, maybe that's not correctly set.
Update
OK, the resize obeserver is probably not needed as I'm getting the same behaviour after commenting out the <ResizeObserver/> in render. So I think it's caused by xterm.js or the css in Guppy.
I have a fix for the issue. It's now working in the above mentioned feature branch. Not sure if there is a better solution but it's working for me.
I like to explain how I have fixed the resizing issue:
The problem was the OnlyOn component that was used in DevelopmentServerPane. It always rendered two TerminalOutput components. One terminal was hidden with display: none and the other was displayed with display: inline - the style change was handled with a media query inside a styled-component.
After replacing OnlyOn with React-responsive and using the render props to check mdMin breakpoint it was working as expected. React-responsive is removing the not displayed mediaquery component from DOM so only one terminal in DOM at the same time.
I still don't know why there was a problem with the letter width but probably the two instances collided somehow. I couldn't create a minimal reproduction. I tried to recreate the issue in this Codesandbox but I have only resized one Terminal at a time and so I haven't got the issue there.
The code that fixed the problem (simplified version from the above mentioned repo):
import MediaQuery from 'react-responsive';
const BREAKPOINT_SIZES = {
sm: 900,
};
const BREAKPOINTS = {
mdMin: `(min-width: ${BREAKPOINT_SIZES.sm + 1}px)`,
};
const DevelopmentServerPane = () => (
<MediaQuery query={BREAKPOINTS['mdMin']}>
{matches =>
matches ? (
<div>{/* ... render Terminal for matching mdMin and above */}</div>
) : (
<div> {/* ... render Terminal for small screens */}</div>
)
}
</MediaQuery>
);

How to add ripple effect when clicking Card in MUI

Is there a way I can add ripple effect to MUI Card component on click.
And I would also like to know, is it possible to make ripple effect to come on top of the content of Card component, rather that it showing as background.
I noticed that TouchRipple has been moved out of the internal directory.
It's now available in the ButtonBase folder.
Here is how I was able to add ripple effect by using the ButtonBase component -
Basically, you wrap your component, let's say <Card> inside the <ButtonBase> like so, and the ButtonBase takes care of the TouchRipple setting up for you -
<ButtonBase>
<Card>
....
</Card>
</ButtonBase>
Here is a Codesandbox link to working demo.
I know this is not the best way. You could directly use the TouchRipple/Ripple component, but I found this way to be very easy to use.
Hope this helps.
I can see this question was not answered, so I'll provide an up-to-date solution (writing this as material-ui is v. 0.18.7 (stable):
You need to import the ripple higher-order comp. (HOC) as:
import TouchRipple from '#material-ui/core/ButtonBase/TouchRipple';
Then you can wrap any component of you choice with TouchRipple, like:
<TouchRipple>
<div>
MY RIPPLING DIV
</div>
</TouchRipple>
Or, if you need a CSS class-based apporach, you can use materialize lib -> https://react-materialize.github.io/#/
In that case, it's as simple as adding a value to waves prop on a material-ui Button, like:
<Button waves='light'>EDIT ME<Icon left>save</Icon></Button>
2021 Update
The most idiomatic way to add the ripple effect when clicking the Card is using the CardActionArea. This component inherits the props of ButtonBase. It also changes the Card background color when hovered and focused (unlike ButtonBase):
<Card>
<CardActionArea>
<CardContent>
{...}
</CardContent>
</CardActionArea>
</Card>
The approach taken in #xiaofan2406 never worked for me, not to mention passing height, width and position seems easily breakable and might not be possible when using flexbox.
However I managed to make it work like:
<YourComponent>
<TouchRipple>
{children}
</TouchRipple>
</YourComponent>
Here is a Solution 2021 updated
simple You need wrap your custom components with component from material ui .
Then add style padding: 0 that solve.
Here I want my Image should react with ripple effect.
You can customize by wrapping with Grid and props container
import { Button } from "#material-ui/core";
function ImageCard(props){
return (
<Button style={{ padding: 0, borderRadius: "16px" }}>
{/*my custom component you can use any component even material ui component also*/}
<img
src={yourImageUrl}
alt="img"
style={{
height: 200,
width: 400,
borderRadius: "16px",//optional
}}
/>
</Button>
);
}
Using component attribute
Instead of wrapping, you can specify the component attribute as the desired component you want it to be. That is, for this use-case;
import ButtonBase from '#material-ui/core/ButtonBase';
...
<Card component = {ButtonBase}>
<CardContent>
...
</CardContent>
</Card>
If you have issues with height or width of the card, add the sx attribute;
<Card component={ButtonBase} sx={{height:'100%', width:'100%'}}>
...
</Card>
If ButtonBase messes up all other buttons on the page, it's better to use just the Button;
import Button from '#mui/material/Button';
...
<Card component = {Button}>
<CardContent>
...
</CardContent>
</Card>
Using MUI V5 (2022)
You can get the benefit of the component prop, use the <ButtonBase> component to get the ripple effect.
Create a React HOC or simply copy this code into a new file:
import React, { forwardRef } from 'react'
import { ButtonBase } from '#mui/material'
export default function WithTouchRipple(OriginalComponent) {
return (props) => {
const Wrapper = !props.component
? ButtonBase
: forwardRef((ButtonBaseProps, ref) => <ButtonBase component={props.component} {...ButtonBaseProps} ref={ref} />)
return <OriginalComponent {...props} component={Wrapper} />
}
}
Then use it as follows on any component you like:
import { Chip, Card, Stack } from '#mui/material'
import WithTouchRipple from '../WithTouchRipple'
const RippleChip = WithTouchRipple(Chip)
const RippleCard = WithTouchRipple(Card)
const RippleStack = WithTouchRipple(Stack)
<RippleChip component={Link} to={`/users/${id}`} {...chipProps} />
<RippleStack>...</RippleStack>
<RippleCard>...</RippleCard>
If you want the ripple effect to appear on the <Card> component, there is a built-in component for that called <CardActionArea>, and you can use it as follows:
<Card>
<CardActionArea>
<CardContent>
{...}
</CardContent>
</CardActionArea>
</Card>
The official api doesn't seem to support it.
But this is what I do, when I want to use material-ui ripple affect:
Use material-ui/internal/TouchRipple, have a look at its source code
Usage example:
<YourComponent>
<TouchRipple style={style}/>
{children}
</YourComponent>
You need to pass the inline style to specify its height, width and position that matches YourComponent's height, width and postion

Categories

Resources