Change colspan based on breakpoint - reactjs

I'm pretty new to React and I have a table that I hide some columns with CSS media query when the the screen is below certain breakpoint.
#media screen and (max-width: 766px) {
.tdcel {
display: none;
}
It works great, however, in the same table, in a row below I have a cell that's using colspan. Obviously when I hide columns with CSS I need to reduce the colspan as well.
I have an element that's calling a function on click. I was thinking of adding some sort of a check to that function and perhaps count visible cells in the row where the click happens. If I was using jQuery I could do something like this:
$(".clickEm").click(function() {
var ths = $(this),
par = ths.closest("tr"),
col = par.find("td:visible").length;
par.next("tr td").attr("colspan", col);
}
Unfortunately I'm still trying to wrap my mind around React components. Is there a way to accomplish this? Or perhaps I'm overthinking this. Perhaps I can have conditional colspan value based on a viewport size?
<td colSpan={5}>

The simplest solution would be to use css-grid or flexbox instead of tables, since it sounds like you have a layout issue here. Those would be purely css solutions.
However, if you need to set the colspan in response to a viewport change, you can always call react functions from window callbacks.
class MyComponent(props) extends React.Component {
state = {
colspan: 1
};
componentDidMount(){
window.addEventListener('resize', this.resize);
}
componentWillUnmount(){
window.removeEventListener('resize', this.resize);
}
resize(){
// set the state of your component here:
this.setState({
colspan: // whatever
});
}
render(){
return (
<Table>
<tr>
<td colSpan={this.state.colspan}>Blah blah blah</td>
</tr>
</Table>
);
}
}
You may need to change the syntax depending on your variant or flavour of javascript (typescript, classes, whatever.)
React component class functions are just regular functions that can be called from other functions, like window events. Make sure to remove the function when the component is unmounted, or else you could be left with a memory leak.
Additionally, don't forget that window events like resize can be called multiple times per frame, and react can batch up render calls, so make sure your update and render logic is simple.
Alternatively, there are react components on NPM that respond to window size changes for you. Search for npm react resize aware.

Related

What happens when React rerenders?

I have the following code and you can view it on codeSandbox:
import "./styles.css";
import lottie from "lottie-web";
import { useEffect, useRef } from "react";
import logo from "pic";
export default function App() {
const el = useRef(null);
useEffect(() => {
if (el != null) {
lottie.loadAnimation({
container: el,
renderer: "svg",
loop: true,
autoplay: true,
animationData: logo
});
}
}, []);
return (
<div className="App">
<div ref={el}></div>
</div>
);
}
Lottie is just some library to render animation. The problem I have with it is that while I am in the developing phase, if I make some modification to the second div tag, then React rerenders without destroying the previous animation and in the meanwhile create a new animation below the original one. I am aware that one has to add some effect clearing logic in useEffect but I just do not see why.
I am new to React and only have a very basic understanding of how React works in the background, here is what I think: basically when I change the second div tag, the diff algorithm notices that it has been altered, so it goes ahead and tries to modify the dom associated with it. In my opinion it can 1). either update the current dom 2). delete it and append a new dom. In the first situation, shouldn't it leave the first animation unchanged without adding a new animation? In the second situation, if it gets rid off the current dom, why would the original animation be kept?
React can do both scenarios you mentioned: update existing and destroy & re-create. And React uses its own algorithm to decide when to do which.
You can force React to use destroy & re-create strategy using a special props called key.
Here is an example: https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html#recommendation-fully-uncontrolled-component-with-a-key
Quoted from the link above:
When a key changes, React will create a new component instance rather than update the current one
I couldn't run the example you provided via code sandbox and just by reading your code, I can see that your App component has no props and no state. Thus, there is no way for you to update it.
Your code said: when componentDidMount use lottie to do some animation inside the second div. So, chances are the animation issue you're having is probably coming from lottie not React.

Mimic React custom component

I have a custom Reactjs component to display Pagination with next/previous buttons at the bottom of a grid. Now, the business needs to display the same component on top of the grid as well. How to display the previous /next button events based on the input provided in prev/next buttons at the bottom of the grid?
I tried using javascript innerHTML to mimic the behaviour. It works only with the display. It does not attach the event listener of the buttons. I tried even with
document.querySelector.addEventListener('click', ()=>{console.log('test')})
It does not work. Is there a better way to do with react.
I am going to just add some more content to Shmili Breuer answer.
If i understood you correctly you have 2 navigations, one at the top one at the bottom. The way you connect them would be through a state of you component, or a parent component if you are using functional component to render pagination stuff. So if you change the state it will reflect on both of your navigations. Also you can use only one function here, by passing a parameter, im gonna copy a code from before mentioned answer.
// first create a function
nextFunction = (condition) => {
if(condition){
this.setState(prevState=>({
page: prevState.page-1
}))
} else {
this.setState(prevState=>({
page: prevState.page+1
}))
}
}
// then use it in your button
<button onClick={() => this.nextFunction(some condition)}>Next</button>
Just put that component on top and bottom
<Grid>
<Pagination />
{...someOtherComponents}
<Pagination />
</Grid>
it's ok in react. Optimization that you want to do is overhead.
In react you would add an onClick attribute to an element you want to handle a click on.
Something like this
// first create a function
nextFunction = () => {
do next functionality....
}
// then use it in your button
<button onClick={() => this.nextFunction()}>Next</button>
This way you can have both top and bottom pagination call the same function.
Hope this helps

React dropzone - dragLeave event fired when dragging file over dropzone

I am using React dropzone for file upload
<DropZone
accept='.pdf,.pptx,.ppt,.docx,.doc,.xls,.xlsx,.xslx,.png,.xsl,.jpg,.jpeg,.gif,.zip'
onDrop={ files => {
this.handleFileDrop(files);
this.dragLeaveHandler();
} }
onDragEnter={ this.dragOverHandler }
onDragLeave={ this.dragLeaveHandler }
multiple={ false }
style={ { height: '100%' } }
>
dragOverHandler = () => {
console.log('enter');
this.setState({
isDragOver: true,
});
};
dragLeaveHandler = () => {
console.log('exit');
this.setState({
isDragOver: false,
});
};
When a file is moving above the drop zone onDragLeave event fires simultaneously.
Should I use some other events?
How can I fix this issue?
You could use pointer-events: none; on the element(s) that are firing the drag leave. That should still allow the dropped event and getting the accepted file though would stop overriding the dropzone events.
The problem you're facing is most likely caused by the DOM events dragEnter and dragLeave getting messed up instead of any flaw in the react-dropzone package. Some elements may cause hovering over them in certain positions not to register as hovering over their parent element. For example, there is a thin sliver at the top edge of any plain string rendered inside a block displayed element. Most commonly this happens inside a <p> tag.
Without seeing the children rendered inside your dropzone, it is impossible to give a specific fix. Generally, you will have to mess with the styling of the children, though. <p> tags for example will not be a problem if their size is set to 0 pixels or if they're replaced with <span> tags. Both options will disrupt the displaying of the children, which is unfortunatley unavoidable.
As for using other events, you're out of luck. The DropZone component relies on the onDragEnter and onDragLeave HTML DOM events. Therefore any fix you might come up with won't fix the component itself.
All in all, it's an unfortunate issue that just has to be dealt with. The simplest way to deal with it is to just have at most one piece of text inside the dropzone and to set its size to 0 pixels with css: height: 0px;. Regular <div> elements won't cause issues, so you can craft an intricate dropzone using them.

React - How to notify popper to reposition my popover whenever react updates any DOM element

Relevant versions: React 16.4.2, Bootstrap 4.1.3, popper.js 1.14.4, Typescript 3.0.3
I use the Bootstrap Popover functionality in my react app.
The Popover works well if the rest of the page is static. When the page is changed (at the browser level), the Popover gets repositioned very quickly and smoothly so it stays visible while the content it's anchored to is visible:
when scrolling if it bumps up against the windows edges
if the screen is rotated on a phone
if the window is resized
This all works well because popper.js is apparently watching the window.scroll and window.resize events, as per this answer: Bootstrap 4 - how does automatic Popover re-positioning work?
The problem comes when my react application starts showing/hiding DOM elements. Because popper.js doesn't know about react, it doesn't know the DOM changed, so it doesn't know that the Popovers might need to be repositioned.
I know calling popover("update") on each Popover anchor works, because I've added code like this to do it intermittently:
window.setInterval(()=> $(this.selfRef).popover("update"), 100);
But that's yucky and wasteful, and a little janky.
Is there a way to have react tell me when it updates any node in the DOM, so I can then tell popper.js to update the position of the popovers?
Note that the react component that causes the DOM change isn't necessarily located near the component that uses the Popover. It could be something in a completely separate part of the hierarchy that happens to be displayed before the component with the popover - so the I don't think the solution is componentWillReceiveProps() or methods like that on the Popover component, because it's probably not the component that's causing the movement.
Note that I'm aware of projects like react-bootstrap, reactstrap or react-popper - but I don't want to use them.
EDIT: it seems like MutationObserver might be a non-react way to do this. I just figured since React is already doing all that reconciliation work, maybe there's a way to get it to notify me when it actually does edit the DOM.
"The react Component that causes the DOM change isn't necessarily
located near the Component that uses the Popover. It could be
something in a completely separate part of the hierarchy"
If both the Component that changes the DOM, and the Component that creates the Popover are in the same parent, you could share a method in the parent that does the .popover('update'). The Component that changes the DOM would need to trigger this event, but it doesn't need to be specifically "aware" of the Popover Component. The Popover Component doesn't need to be aware of the DOM changing Component.
class ChangeDom extends React.Component {
constructor(props) {
super(props);
this.changeDom = this.changeDom.bind(this);
}
changeDom () {
this.props.domChanged();
}
render() {
return (
<div>
<button className="ml-2 btn btn-primary" onClick={this.changeDom}>Change Dom
</button>
</div>)
}
}
class Pop extends React.Component {
constructor(props) {
super(props);
this.togglePopover = this.togglePopover.bind(this);
}
togglePopover() {
$('[data-toggle="popover"]').popover('toggle');
}
render() {
return (
<div class="position-relative">
<button className="mt-4 btn btn-primary" onClick={this.togglePopover} data-toggle="popover"
</button>
</div>)
}
}
class Parent extends React.Component {
domChanged(){
$('[data-toggle="popover"]').popover("update");
}
render() {
return (
<div>
<ChangeDom domChanged={this.domChanged} />
<Pop />
</div>)
}
}
Demo: https://www.codeply.com/go/NhcfE8eAEY
This is my current attempt at a MutationObserver based solution.
UserApp is a component placed toward the top of the application hierarchy.
The Popover class is (over) used in various places in my application for a bunch of stuff.
The possibility of infinite recursion caused by firing popover("update") from a MutationObserver event makes me wary of using this solution long term.
It seems to do the job for now, but this is one of the things uni-directional binding is meant to avoid.
On the plus side, this works even when you have non-react components in your application (like for example, the Bootstrap navbar).
export class UserApp extends React.Component<any, AppState> {
public domChangeObservers = $.Callbacks();
public mutationObserver = new MutationObserver(
(mutations: MutationRecord[])=>{
// premature optimisation?
// I figure I don't care about each individual change, if the browser
// batched em up, just fire on the last one.
// But is this a good idea given we have to inspect the mutation in order
// to avoid recursive loops?
this.domChangeObservers.fire(mutations[mutations.length-1]);
}
);
constructor(props: any) {
super(props);
this.mutationObserver.observe(document.documentElement, {
attributes: true,
characterData: true,
childList: true,
subtree: true,
attributeOldValue: true,
characterDataOldValue: true
});
}
componentWillUnmount(){
this.mutationObserver.disconnect();
}
...
}
const DefaultTrigger = "click";
export interface PopoverProps{
popoverTitle: string | Element | Function;
popoverContent: string | Element | Function;
/** Set to "focus" to get "dismiss on next click anywhere" behaviour */
popoverTrigger?: string;
/** Leaving it empty means that the popover gets created
* as a child of the anchor (whatever you use as the child of the popover).
* Setting this to "body" means the popover gets created out on the body
* of the document.
* "body" can help with stuff like when the popover ends up
* being clipped or "under" other components (because of stuff like
* `overflow:hidden`).
*/
container?: string;
allowDefaultClickHandling?: boolean;
ignoreDomChanges?: boolean;
id?: string;
}
export class Popover
extends PureComponent<PopoverProps, object> {
// ! to hack around TS 2.6 "strictPropertyInitialization"
// figure out the right way... one day
selfRef!: HTMLSpanElement;
onDomChange = (mutation:MutationRecord)=>{
/*
- popover("update") causes DOM changes which fire this handler again,
so we need to guard against infinite recursion of DOM change events.
- popover("update") is async, so we can't just use an "if not currently
handling a mutation" flag, because the order of events ends up being:
onDomChange() -> flag=true -> popover("update") -> flag=false ->
popper.js changes DOM -> onDomChange() called again -> repeat forever
- Can't just detect *this* popover. If DOM event occurs because popovers
overlay each other they will recurse alternately - i.e. pop1 update
call makes DOM changes for pop2, pop2 update makes changes for pop1,
repeat forever.
*/
if( Popover.isPopoverNode(mutation) ){
return;
}
/*
- tell popper.js to reposition the popover
- probably not necessary if popover is not showing, but I duuno how to tell
*/
$(this.selfRef).popover("update");
};
private static isPopoverNode(mutation: MutationRecord){
/*
Had a good attempt that used the structure of the mutation target to
see if it's parent element was defined as `data-toggle="popover"`; but
that fails when you set the `container` prop to some other element -
especially, "body", see the comments on the Props .
*/
if( mutation.target.nodeType != 1 ){
return false;
}
// Is Element
let element = mutation.target as Element;
/*
Is the mutation target a popover element?
As defined by its use of the Bootstrap "popover" class.
This is dodgy, it relies on Bootstrap always creating a container
element that has the "popover" class assigned.
BS could change their classname, or they could
change how they structure their popover, or some other
random widget could use the name.
Actually, this can be controlled by overriding the popover template,
which I will do... later.
*/
let isPopoverNode = element.classList.contains("popover");
// very helpful when debugging - easy to tell if recursion is happening
// by looking at the log
// console.log("target", isPopoverNode, mutation, mutation.target );
return isPopoverNode;
}
componentDidMount(): void{
// the popover() method is a "JQuery plugin" thing,
// that's how Bootstrap does its stuff
$(this.selfRef).popover({
container: this.props.container || this.selfRef,
placement: "auto",
title: this.props.popoverTitle,
content: this.props.popoverContent,
trigger: this.props.popoverTrigger || DefaultTrigger,
});
if( !this.props.ignoreDomChanges ){
UserApp.instance.domChangeObservers.add(this.onDomChange);
}
}
componentWillUnmount(): void {
if( !this.props.ignoreDomChanges ){
UserApp.instance.domChangeObservers.remove(this.onDomChange);
}
// - without this, if this component or any parent is unmounted,
// popper.js doesn't know that and the popover content just becomes
// orphaned
$(this.selfRef).popover("dispose");
}
stopClick = (e: SyntheticEvent<any>) =>{
if( !this.props.allowDefaultClickHandling ){
// without this, if the child element is an <a> or similar, clicking it
// to show/dismiss the popup will scroll the content
e.preventDefault();
e.stopPropagation();
}
};
render(){
let popoverTrigger = this.props.popoverTrigger || DefaultTrigger;
// tabIndex is necessary when using "trigger=focus" to get
// "dismiss on next click" behaviour.
let tabIndex = popoverTrigger.indexOf("focus")>=0?0:undefined;
return <span id={this.props.id}
tabIndex={tabIndex}
ref={(ref)=>{if(ref) this.selfRef = ref}}
data-toggle="popover"
onClick={this.stopClick}
>{this.props.children}</span>;
}
}

React: empty placeholder div gets re-rendered and thereby removes children (asynchr attached chart)

I am working on a widget dashboard which has DC.JS charts as content for each widget.
Widgets are created/ removed using react-grid-layout which creates an empty placeholder node like this:
<div id={"content_" + this.props.id} className="widgetContent"> /* chart is later drawn here */ </div>)
DC.JS later selects the div by Id and attaches its SVG chart as a child.
The problem is that for some events (like toggling static or changing Ids of the widgets), react re-renders the widgets and thereby "overwrites" the existing charts (children) with a brand new empty placeholder div as above.
My question is if that issue can be solved by React-techniques (can I prevent a div from ever being re-rendered?) or if this is an issue with the library itself.
Very similar code can be found here. The code in action is here. Imagine the snippet line above (the empty chart placeholder where a chart is attached later) in line 44.
The common solution here is to wrap this chart in a component where shouldComponentUpdate is set to false. That way react will never alter the element which your charting library modifies. An example wrapper component can be found here (including below)
var React = require('react/addons');
var ReactIgnore = {
displayName: 'ReactIgnore',
shouldComponentUpdate (){
return false;
},
render (){
return React.Children.only(this.props.children);
}
};
module.exports = {
Class: ReactIgnore,
Component: React.createClass(ReactIgnore)
};

Resources