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.

How to align array items in a row React Native

There could be a scenario where you’d want to display the elements in your array in a row spaced evenly.

I’ve imported the following object from another file that contains my Array:

export const keyArrays = {
    myArray : ["A", "B", "C", "D", "E", "F", "G"],
    ....
}

The following code snippet shows how the return method in the main App.js component uses a view. The rowContainer class used in the code below will align the array items in a row inside the view and will wrap the items if going out of View’s width.

The following is the stylesheet snippet used in the App.js component:

const styles = StyleSheet.create({
  rowContainer: {
    flexDirection: 'row',
    flexWrap: 'wrap',
    height: 30,
    paddingTop: 5,
  },
  sectionContainer: {
    marginTop: 32,
    paddingHorizontal: 24,
  },
  sectionTitle: {
    fontSize: 20,
    fontWeight: '600',
    fontFamily: 'AvenirNextCyr-Regular',
    color: Colors.white,
    backgroundColor: '#32879d',
  },
});

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.

How to create XML in JavaScript

Though this is pretty rarely used scenario, I tried to create XML document using the DOMParser. Since JavaScript handles XML with ‘XML DOM objects’, you can create such an object by parsing a string containing serialized XML.

Below code is just a method that I implemented which returns the XML object:

getXMLString() {
    var parser = new DOMParser();
    var xml = "<?xml version=\"1.0\" standalone=\"yes\" ?>";
    xml = xml + "<PlanInfo UserId=\"" + this.props.UserId + "\" UpdatedBy=\"" + Number(localStorage.getItem("userId")) + "\" UpdatedOn=\"" + this.formatDate(new Date()) + "\"><Plans>";
    for(var i=0; i<15; i++) {
      xml = xml + "<Plan ID=\"" + this.props.myColl[i].dataId + "\" Comment=\"" + this.props.myColl[i].comment + "\" />";
    }
    xml = xml + "</Plans></PlanInfo>";
    var xmlDoc = parser.parseFromString(xml, "application/xml");
    return xmlDoc;
}

The above code is an implementation in ReactJS that reads the props from redux and loop through the data to create the XML. Please note that XML tag names are case-sensitive.

Below is the sample output:

<?xml version="1.0" standalone="yes" ?>
<PlanInfo UserId="234" UpdatedBy="123" UpdatedOn="13 May 2020">
    <Plans>
        <Plan ID="1" Comment="test comments"/>
	<Plan ID="2" Comment="test comments2"/>
....
    </Plans>
</PlanInfo>

When you have obtained an XML DOM object (e.g. xmlDoc above), you can use methods to manipulate it as shown below:

var node = xmlDoc.createElement("Plan");
var elem = xmlDoc.getElementsByTagName("Plans");
elem[0].appendChild(node);

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.

Setup React-Native for iOS and Android on Mac with VS Code

The very first step to start development in React-Native is to install the right tools to setup the environment correctly. I’m working on MacOS Catalina and I already have Xcode 11 installed on my machine which is one of the pre-requisites to run the iOS App.

Install the following dependencies for React-Native:

NodeJS: https://nodejs.org/en/download/

The recommended way to install Node is using Homebrew from your Terminal:

brew install node

Watchman: In react-native, Watchman watches the source code for any changes and rebuilds them.

brew install watchman

Install XCode from App Store.

Cocoapods: This is the Dependency Manager for iOS & Mac projects.

sudo gem install cocoapods

This requires you to enter system password.

Install Visual Studio Code for Mac from here. You might face issues on Mac OS Catalina to run VS Code due to Notarization requirements. You can check out this post for more details.

Create the folder inside say your Documents folder on your Mac as below:

mkdir Apps

Open VS Code and open New Terminal and type the following command to create a new React-Native App:

react-native init MyApp
cd MyApp
cd ios
pod install

Come back to the MyApp folder and run the below command to run your App in the iOS simulator:

react-native run-ios

This will install and launch the App in your iOS simulator. The first time might be very slow. It might be helpful to launch your simulator beforehand.

Running on Android:

Install the Android Studio from here, which will install the Android SDK and the required emulator. I’m using the Android 10 emulator for Pixel XL.

react-native run-android

Open the MyApp folder in VS Code to make further changes.

Issues

Running the Android App causes issues while launching the Emulator. The error message is: React Native adb reverse ENOENT

Starting with macOS Catalina, your Mac uses zsh as the default login shell and interactive shell. You can make zsh the default in earlier versions of macOS as well.

So on your Mac:

1 – Open your .zshrc file:

open ~/.zshrc

2 – if .zshrc file doesn’t exist, you need to create one & open again(Step 1)

touch ~/.zshrc

3 – Add this to your .zshrc file

export ANDROID_HOME=/Users/<username>/Library/Android/sdk
export PATH=/Users/<username>/Library/Android/sdk/platform-tools:$PATH

4 – Save and close

5 – Compile your changes

source ~/.zshrc

& make sure to restart your terminal.

Run the command in the Terminal and you’ll see something like below if everything is fine:

adb

Android Debug Bridge version 1.0.41

Version 30.0.0-6374843

Installed as /Users/<username>/Library/Android/sdk/platform-tools/adb

You can also run the code in your VS Code Terminal and see the same output.

The path to Android SDK is available in the Configure button -> SDK Manager (at the bottom right) when you launch Android Studio.

Run VS Code on MacOS Catalina

Due to notarization requirements, currently you may face issue while opening Visual Studio code on MacOS Catalina. To resolve this, go to

System Preferences -> Security & Privacy. Click the lock to make changes -> Provide your password. You’ll get a notification at the bottom that the “App was blocked from opening because it is not from an identified developer”.

click ‘open anyway’.

Check the screen-shot below:

This would open Visual Studio code on your Mac.

Here is the Visual Studio Code Requirements Page:

https://code.visualstudio.com/docs/supporting/requirements

Here is the Apple notarising documentation:

https://developer.apple.com/documentation/xcode/notarizing_macos_software_before_distribution