Set focus on tabbing ReactJS

I was working on Material-UI ExpansionPanel and had come across a task where I had to set focus on tabbing through them using keyboard. So when you press space on Focus, the Panel would open/close.

The ExpansionPanel sample implementation would be as below inside render:

return(
	<div>
	  <div className={classes.root}>
		{this.state.panelTypes.map((item) => (
		  <ExpansionPanel key={item.userType} expanded={this.state.expanded === item.userType} onChange={this.handleChange(item.userType)}>
			<ExpansionPanelSummary
			  expandIcon={<ExpandMoreIcon />}
			  aria-controls="panel1bh-content"
			  id="panel1bh-header"
			  style={{backgroundColor: "#e6e6e6", border: '1px solid silver'}}
			  className={classes.heading}
			>
			  <Typography className={classes.heading}>Heading</Typography>
			</ExpansionPanelSummary>
			<ExpansionPanelDetails>
			... display data here...
			</ExpansionPanelDetails>
		  </ExpansionPanel>
		))}
	  </div>
	</div>
  );

The above code is mapping through an array of PanelTypes for creating multiple ExpansionPanels.

The class would require the pseudo-class :focus to be used for tabbing through the Expansion Panel heading:

const styles = (theme) => ({
	//There might be other classes here..
  heading: {
    fontSize: theme.typography.pxToRem(15),
    flexBasis: '33.33%',
    flexShrink: 0,
    fontFamily: "Verdana,Arial,sans-serif",
    '&:focus': {
      outline: "-webkit-focus-ring-color auto 1px"
    }
  }
});

Similarly, other pseudo-classes can be used inside the styles e.g. ::after, ::before etc. as per their own usage.

Material-table example ReactJS

There are a lot of scenarios in Material-table that you might have difficulty in understanding from the documentation or may be scattered on different forums. I’ll try to cover up the scenarios which I faced while implementing a table in my Project with Actions like Add/Edit/Delete. Another scenario is to make it read-only i.e. remove all the Action buttons.

This is the sample courses React App over which I’ll develop further using Material-Table. You can get the code from the Github Project here. To get started, you’ll need the following packages:

npm i @material-ui/core
npm i @material-ui/icons
npm i @material-ui/lab
npm i material-table
npm i axios

The actions are managed through uncontrolled fields i.e. without managing through state directly. The API calls are as per my example and URL placeholder requires to be updated as per your API host name.

The columns and icons are set in the state. autoFocus property is set to the first TextField for edit mode.

The options prop which I’ve used has Paging, sorting and dragging of columns disabled. Sorting and draggable can simply be set to true and used.

To make the table read-only, the editable prop of Material Table can be set to null conditionally. I’ve used isReadOnly boolean to add the condition, you can have your own state here.

render() {
    const { classes } = this.props;
    return (
      <Fragment>
         <div
            id={this.props.id}
          >
            <MaterialTable
              components={{
                EditRow: (props) => {
                  return (
                    <MTableEditRow
                      {...props}
                      onEditingCanceled={(mode, rowData) => {
                        this.canceledClicked();
                        props.onEditingCanceled(mode);
                      }}
                    />
                  );
                },
              }}
              title=""
              columns={this.state.columns}
              data={this.state.StudentsEnrolled}
              icons={this.state.tableIcons}
              isLoading={this.state.showLoading}
              style={{
                border: "2px solid gray",
                maxWidth: "1450px",
                overflow: "scroll",
                marginTop: "10px",
                marginLeft: "20px",
              }}
              editable={
                isReadOnly
                ? null
                : 
                {
                onRowAdd: (newData) =>
                  new Promise((resolve, reject) => {
                    newData.studentName =
                      studentName_GRC === null ? "" : studentName_GRC;
                    
                    newData.grade = grade_GR === null ? "" : grade_GR;

                    newData.courseName = courseName_GR === null ? "" : courseName_GR;

                    newData.comments = comments_GR === null ? "" : comments_GR;

                    var errorMsg = " ";

                    if (newData.studentName === "") {
                      errorMsg = "Please insert Student Name.";
                    }
                    if (newData.grade === "") {
                      errorMsg = errorMsg + "\nPlease insert grade.";
                    }
                    if (newData.courseName === "") {
                      errorMsg = errorMsg + "\nPlease insert course Name.";
                    }

                    if (errorMsg !== " ") {
                      reject();
                      alert(errorMsg);
                    } else if (this.state.countGrid >= GridDataLimit) {
                      reject();
                      alert(
                        "You cannot add more than " +
                          GridDataLimit +
                          " Students to the list."
                      );
                    } else {
                      this.setState({ showLoading: true });
                      this.postStudentsEnrolledData(newData);
                      resolve();
                    }
                    // }, 600);
                  }),
                onRowUpdate: (newData, oldData) =>
                  new Promise((resolve, reject) => {
                    // setTimeout(() => {
                    //resolve();

                    if (oldData) {
                      newData.studentName =
                        studentName_GRC === null
                          ? oldData.studentName
                          : studentName_GRC === ""
                          ? ""
                          : studentName_GRC;

                      newData.grade =
                        grade_GR === null
                          ? oldData.grade
                          : grade_GR === ""
                          ? ""
                          : grade_GR;

                      newData.courseName =
                        courseName_GR === null
                          ? oldData.courseName
                          : courseName_GR === ""
                          ? ""
                          : courseName_GR;

                      newData.comments =
                        comments_GR === null
                          ? oldData.comments
                          : comments_GR === ""
                          ? ""
                          : comments_GR;

                      var errorMsg = " ";
                      if (newData.studentName === "") {
                        errorMsg = "Please insert Student Name.";
                      }
                      if (newData.grade === "") {
                        errorMsg = errorMsg + "\nPlease insert grade.";
                      }
                      if (newData.courseName === "") {
                        errorMsg = errorMsg + "\nPlease insert course Name.";
                      }
                      if (errorMsg !== " ") {
                        reject();
                        alert(errorMsg);
                      } else {
                        this.setState({ showLoading: true });
                        this.postStudentsEnrolledData(newData);
                        resolve();
                      }
                    }
                    // }, 600);
                  }),
                onRowDelete: (oldData) =>
                  new Promise((resolve) => {
                    // setTimeout(() => {
                    this.setState({ showLoading: true });
                    this.deleteStudentsEnrolledData(oldData);
                    resolve();
                    // }, 600);
                  }),
              }}
              options={{
                paging: false,
                sorting: false,
                draggable: false,
                addRowcourseName:"first",
                rowStyle: { backgroundColor: "#fff" },
              }}
            />
          </div>
      </Fragment>
    );
  }

The isLoading prop manages the hide/show of loader when an action is performed.

There is one more scenario where clicking outside the + icon, doesn’t trigger the onAddRowClick event. This is handled in componentDidMount method.

Render elements dynamically using map Reactjs

Here I’m using a component that needs to render multiple ExpansionPanel Material UI components based on data fetched in an array. The map is using Arrow function with () for an implicit return.

Below is the sample code for the Component:

import React, { Component } from "react";
import Header from "./Header";
import { withStyles } from '@material-ui/core/styles';
import ExpansionPanel from '@material-ui/core/ExpansionPanel';
import ExpansionPanelDetails from '@material-ui/core/ExpansionPanelDetails';
import ExpansionPanelSummary from '@material-ui/core/ExpansionPanelSummary';
import Typography from '@material-ui/core/Typography';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import loadingImg from "../images/LoadingImg_EZ.gif";

const styles = (theme) => ({
  root: {
    width: '100%',
  },
  heading: {
    fontSize: theme.typography.pxToRem(15),
    flexBasis: '33.33%',
    flexShrink: 0,
    fontWeight: 'bold',
  },
  secondaryHeading: {
    fontSize: theme.typography.pxToRem(15),
    color: theme.palette.text.secondary,
  },
  OfflineText: {
    fontSize: "20px",
    color: "red",
  },
});

class MyComponent extends Component {
  constructor(props) {
    super(props);
    this.state = {
      expanded: false,
      panelTypes: null,
      summary: null
    };
  }

  componentWillMount() {
    //get data here and set state.
  }

  handleChange = (panel) => (event, isExpanded) => {
    this.setState({ expanded: isExpanded? panel: false});
  };

  render() {
      const { classes } = this.props;
      
      return(
        <div>
        <Header />
        {this.state.panelTypes !=null ? (
        <>
          <h3 style={{fontSize: "15px", paddingLeft: "4px"}}>Summary</h3>
          <div className={classes.root}>
            {this.state.panelTypes.map((item) => (
              <ExpansionPanel expanded={this.state.expanded === item.types} onChange={this.handleChange(item.types)}>
                <ExpansionPanelSummary
                  expandIcon={<ExpandMoreIcon />}
                  aria-controls="panel1bh-content"
                  id="panel1bh-header"
                  style={{backgroundColor: "#e6e6e6"}}
                >
                  <Typography className={classes.heading}>{this.state.panelTypes !=null ? `${item.types} (Year ${item.year})`:''}</Typography>
                </ExpansionPanelSummary>
                <ExpansionPanelDetails>
                  <Typography>
                    Nulla facilisi. Phasellus sollicitudin nulla et quam mattis feugiat. Aliquam eget
                    maximus est, id dignissim quam.
                  </Typography>
                </ExpansionPanelDetails>
              </ExpansionPanel>
            ))}
          </div>
          </>) : (
          <div id="loading">
            <img id="loading-image" src={loadingImg} alt="Loading..." />
          </div>
        )}
        </div>
      );
  }

}

export default withStyles(styles, { withTheme: true })((MyComponent));

Check Application is Online ReactJS

It is a very common scenario to check whether the user is online while using your Web Application to make it robust. May be you need to just show a message to the user that the Internet connection is disrupted.

There is a very cool react package react-detect-offline available on npm that helps with this. It uses a default polling url to check whether you’re online. You can however change a few settings including the ping URL, enabled boolean and interval for ping duration. You can also change the timeout setting but I’ve not used it here in the polling object.

//say this code is inside constants.js file under common folder..
export const APIUrl = "https://testapi";
export const pingUrl = APIUrl + "/ping";
export const polling = {enabled: true, url: pingUrl, interval: 10000};
export const OfflineText = "You're Offline. Please check your Internet connection.";

In your component, import these objects and use it as below:

import { Offline, Online } from "react-detect-offline";
import { OfflineText, polling } from "../Common/Constants";

const styles = (theme) => ({
  OfflineText: {
    fontSize: "20px",
    color: "red",
  },
});

class MyComponent extends Component {
	render() {
		const { classes } = this.props;
		return (
		  <div id="loading">
            <Online polling={polling}>
              <img id="loading-image" src={loadingImg} alt="Loading..." />
            </Online>
            <Offline polling={polling}>
              <div className={classes.OfflineText}>{OfflineText}</div>
            </Offline>
		  </div>
		)
	}
}

The above example will ping every 10 seconds to check https://testapi for online status. This is called using http HEAD GET request. So please make sure both http verbs are allowed in your API.

For .net core users, the WebAPI should have the [HEAD] attribute set on the action and the method should be allowed in the Startup.cs file and Web.config.

Example of handlers in web.config as below:

<handlers>
            <remove name="ExtensionlessUrlHandler-ISAPI-4.0_64bit" />
            <remove name="ExtensionlessUrlHandler-ISAPI-4.0_32bit" />
            <add name="ExtensionlessUrlHandler-ISAPI-4.0_32bit" path="*." verb="GET,HEAD,POST,DEBUG,PUT,DELETE" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll" resourceType="Unspecified" requireAccess="Script" preCondition="classicMode,runtimeVersionv4.0,bitness32" responseBufferLimit="0" />
            <add name="ExtensionlessUrlHandler-ISAPI-4.0_64bit" path="*." verb="GET,HEAD,POST,DEBUG,PUT,DELETE" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" resourceType="Unspecified" requireAccess="Script" preCondition="classicMode,runtimeVersionv4.0,bitness64" responseBufferLimit="0" />
        </handlers>

Prompt user with beforeunload on browser tab close React App

You might have a form created in your React App that user might try to close by closing the tab, reloading the page or close the browser directly. You might want to prompt the user in this case to alert them that there might be unsaved changes that might be lost.

The following example binds the “beforeunload” event to the App Component and the unbinds it on the componentwillunmount event. This could apply to form component and not the Parent App component.

export default class App extends React.Component {
  constructor(props) {
    super(props)
    this.handleUnload = this.handleUnload.bind(this);
  }

  componentDidMount() {
    window.addEventListener('beforeunload', this.handleUnload);
  }

  componentWillUnmount() {
    window.removeEventListener('beforeunload', this.handleUnload);
  }

  handleUnload(e) {
    var message = "\o/";

    (e || window.event).returnValue = message; //Gecko + IE
    return message;
  }

}

You can even call an API to make the necessary changes required for your App at the back-end before the browser unloads the Page. But please be careful in case the user decides to cancel and stays on the page. I’ve tested the above code on Google Chrome and Edge Chromium.

I haven’t really found a way that let’s you customize the prompt message in the modern browsers. And there is no way to capture the Leave/Cancel (stay) button click events. Let me know if you find a way to capture these events. However, I believe the browser has given control to the user more than the Page in case of unload.

Make React App compatible with IE11

IE11 is still among the most popular browsers in the world. Though Microsoft is promoting Edge Browser on Chromium engine. Many organizations have still not moved on to new offerings or taking their own time. Meanwhile, if you need to make your React Application compatible with IE11, check the steps below:

  1. Create the app, follow this post, if not already done.
  2. Add import 'react-app-polyfill/ie11'; as the first line in index.js file.
  3. For common standard functions like Array.find() that are not supported in IE 11 add import 'react-app-polyfill/stable'. This should be the 2nd line in index.js file.
  4. In package.json copy the production browserlist to development so you can test in IE11 and all other browsers that will work in production. Just add "ie 11" at the end of both production and development, browserlist section into package.json
  5. Delete the node_modules/.cache directory.
//index.js file:
import "react-app-polyfill/ie11";
import "react-app-polyfill/stable";
//browsers list in package.json file in reference to #4:
"browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all",
      "IE 11"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version",
      "IE 11"
    ]
  }

If you are also using arrow functions so you’ll also need babel to update that for you. The babel-plugin-transform-arrow-functions is your best solution for this. Please check the documentation here.

you might still face some issues in your functionality like additional spaces added by Material UI date control which fails in IE11 but not other browsers. This might need to be taken care of individually on operations using that date value.

Format Date in javascript to dd mmm yyyy

There is a simple way to write a function in Javascript to format the date as dd mmm yyyy. I’m using this method in my ReactJS code to simply format the date and set the state with the returned string wherever required.

formatDate(date) {
    if (date !== undefined && date !== "") {
      var myDate = new Date(date);
      var month = [
        "Jan",
        "Feb",
        "Mar",
        "Apr",
        "May",
        "Jun",
        "Jul",
        "Aug",
        "Sep",
        "Oct",
        "Nov",
        "Dec",
      ][myDate.getMonth()];
      var str = myDate.getDate() + " " + month + " " + myDate.getFullYear();
      return str;
    }
    return "";
  }

You can call this method from your JS code like this:

formatDate(new Date());

How to create Material UI Dialog with OK Cancel

Install the latest Material UI core package from npm:

npm install @material-ui/core @latest

Import the following components required for the Dialog box:

import React from "react";
import { withStyles } from "@material-ui/core/styles";
import Checkbox from "@material-ui/core/Checkbox";
import Dialog from "@material-ui/core/Dialog";
import DialogContent from "@material-ui/core/DialogContent";
import DialogActions from '@material-ui/core/DialogActions';
import { DialogContentText } from "@material-ui/core";
import MuiDialogTitle from "@material-ui/core/DialogTitle";
import CloseIcon from '@material-ui/icons/Close';
import Typography from '@material-ui/core/Typography';
import Button from "@material-ui/core/Button";
import IconButton from '@material-ui/core/IconButton';

Add the following styles for the Dialog content and buttons:

const styles = theme => ({
    root: {
      margin: 0,
      padding: theme.spacing(2),
    },
    closeButton: {
      position: 'absolute',
      right: theme.spacing(1),
      top: theme.spacing(1),
      color: theme.palette.grey[500],
    },
    dialogText: {
      color: theme.palette.blue
    }
  });

The following component will be used in the Dialog Title bar:

const DialogTitle = withStyles(styles)(props => {
    const { children, classes, onClose, ...other } = props;
    return (
      <MuiDialogTitle disableTypography className={classes.root} {...other}>
        <Typography variant="h6">{children}</Typography>
        {onClose ? (
          <IconButton aria-label="close" className={classes.closeButton} onClick={onClose}>
            <CloseIcon />
          </IconButton>
        ) : null}
      </MuiDialogTitle>
    );
  });

Add the class Component that will render the Dialog box:

class MyForm extends React.Component {
    constructor(props) {
      super(props);
	  this.handleChange = this.handleChange.bind(this);
	  this.handleDialogClose = this.handleDialogClose.bind(this);
	  this.handleDialogOK = this.handleDialogOK.bind(this);
	  this.state = {
		dialogText: '',
        isDialogOpen: false,
		isChecked: false
	  }
	}
	
	handleDialogOK() {
      console.log('Clicked OK!');
      this.setState({
        isDialogOpen: false
      });
    }
	
	handleDialogClose() {
      this.setState({
        isDialogOpen: false
      });
    }
	
	handleChange(e) {
      const target = e.target;
	  const value = target.checked;
	  
	  this.setState({
		isChecked: value,
		isDialogOpen: true
	  }, ()=> {console.log('Open Dialog')});
	}
	
	render() {
      const { classes } = this.props;
      return (
		<div>
			<Checkbox id="chkOpenDialog" onChange={this.handleChange} checked={this.state.isChecked}></Checkbox>
			<Dialog
                open={this.state.isDialogOpen}
                onClose={this.handleDialogClose}
                aria-labelledby="alert-dialog-title"
                aria-describedby="alert-dialog-description"
            >
              <DialogTitle id="customized-dialog-title" onClose={this.handleDialogClose}>
                  {"Message from Application"}
              </DialogTitle>
              <DialogContent>
                <DialogContentText id="alert-dialog-description" className={classes.dialogText}>
                  {this.state.dialogText}
                </DialogContentText>
                <DialogActions>
                  <Button color="primary" onClick={this.handleDialogOK}>
                      OK
                  </Button>
                  <Button color="primary" onClick={this.handleDialogClose}>
                      Cancel
                  </Button>
                </DialogActions>
              </DialogContent>  
          </Dialog>
		</div>
	  );
    }
}

export default withStyles(styles, { withTheme: true })(MyForm);

Setting the isDialogOpen to true on clicking checkbox will open the Dialog box with OK and Cancel Action buttons. Each Action button has it’s own handler.

Upload file in ReactJS to WebAPI

Create a new React App using create-react-app as explained in this Post. The sample ReactJS code is available here.

In the index.js file change the code as below:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.render(<App />, document.getElementById('root'));

For the App.js file, change the code as below:

import React from 'react';       
import './App.css';
import Fileupload from './FileUpload'
function App() {    
  return (    
    <div className="App">    
      <Fileupload></Fileupload>
    </div>    
  );    
}
export default App;

Now comes the FileUpload component as FileUpload.js:

import React from 'react';
import axios from 'axios';

class Fileupload extends React.Component {

    constructor(props) {    
            super(props);    
            this.state = {    
                    file: '',    
        };    
    }

    async submit(e) {    
        e.preventDefault();    
        const url = `http://url`;    
        const formData = new FormData();    
        formData.append('body', this.state.file);    
        const config = {    
            headers: {    
                'content-type': 'multipart/form-data',
                'token': 'xxx'                
            },
        };
        const HTTP = axios.create({
            withCredentials: true
        });

        //return post(url, formData, config);
        return HTTP.post(url, formData, config);
    }
    setFile(e) {    
        this.setState({ file: e.target.files[0] });    
    }

    render() {    
        return (    
                <div className="container-fluid">    
                        <form onSubmit={e => this.submit(e)}>    
                                <div className="col-sm-12 btn btn-primary">    
                                        File Upload    
                        </div>    
                                <h1>File Upload</h1>    
                                <input type="file" onChange={e => this.setFile(e)} />    
                                <button className="btn btn-primary" type="submit">Upload</button>    
                        </form>    
                </div>    
        )    
    }
}
export default Fileupload

Here I’m using axios to call the API via Post request. Passing the credentials in the call with create.
The token part is optional in the Headers, you can add/remove headers based on your requirement.

From the WebAPI end, I’m using .Net core. You can receive the file in the Action method as below:

[HttpPost]
[Route("upload")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public IActionResult UploadForm([FromQuery] string username)
{
	var request = HttpContext.Request;
            
	if (request.Form.Files.Count > 0)
	{
		filename = request.Form.Files[0].FileName;
		extension = filename.Substring(filename.LastIndexOf(".")).ToLower();
		if (!string.IsNullOrEmpty(extension) && extension.Equals(".docx"))
		{
			using (var memoryStream = new MemoryStream())
			{
				request.Form.Files[0].CopyTo(memoryStream);
				
				// Upload the file if less than 2 MB
				if (memoryStream.Length < 2097152)
				{
					string check = Service.CheckifValid(username);
					if (check.Equals("ok"))
					{
						//Process the stream and pass the data to the back-end in the Service Layer.
						result = Service.UploadStream(memoryStream, filename, extension, username);
					}
					else
					{
						return Ok(check);
					}
				}
				else
				{
					return BadRequest("File size too large");
				}
			}
		}
		else
		{
			return BadRequest("File format not recognised.");
		}
	}
	//Other ToDos...
}

Download word file using axios get from WebAPI

The below code is using axios get method with Headers token to be passed with credentials which in this case would be Windows Authentication to the WebAPI.

The response returned by the Promise contains the header “content-disposition” which has the name of the file being downloaded.

The content-disposition header value is as below:

attachment; filename="SomeFile.docm"; filename*=UTF-8''SomeFile.docm

The content-type required for this file from the API is:

application/vnd.openxmlformats-officedocument.wordprocessingml.document

The returned response has the blob in the data property which then requires to be converted to a downloadble link. The filename is being extracted from the content-disposition header.

const HTTP1 = axios.create({
                  withCredentials: true
                });
const response = HTTP1.get('http://path/worddoc?userid=<someid>', {
		        headers: {
		          'token': 'xxxxx'
		        },
				responseType: 'blob'
		      }).then((response) => {
			
				const headerval = response.headers['content-disposition'];
				var filename = headerval.split(';')[1].split('=')[1].replace('"', '').replace('"', '');
				
				const downloadUrl = window.URL.createObjectURL(new Blob([response.data]));
				const link = document.createElement('a');
				link.href = downloadUrl;
				link.setAttribute('download', filename); //any other extension
				document.body.appendChild(link);
				link.click();
				link.remove();
			  });

For IE11 browser, downloading blob response type directly does not work. A workaround for this is to recognize the browser via User Agent modify the above code in Javscript for IE:

var ua = window.navigator.userAgent;
var msie = ua.indexOf(".NET ");

if(msie>0) {
          window.navigator.msSaveBlob(new Blob([response.data]), filename);
}