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

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...
}

How to use Server.MapPath in asp.net core

Server.MapPath is not available in asp.net core. Instead, you can use IWebHostEnvironment interface to access any physical contents kept in the specified path.

Below is the Controller code using the IWebHostEnvironment ContentRootPath property which gets the absolute path to the directory that contains the Application content files:

[ApiController]
public class DocumentController : ControllerBase
{
	private IDocumentService offService = null;
	private readonly IWebHostEnvironment _host;

	public DocumentController(IDocumentService offService, IWebHostEnvironment host)
	{
		this.offService = offService;
		this._host = host;
	}
	
	[HttpGet]
	[Route("some/route")]
	public IActionResult ExportToFile([FromQuery] int UserId)
	{
		var username = HttpContext.User.Identity.Name.Split('\\')[1].ToString();
		
		//get the memoryStream by passing the absolute path:
		MemoryStream memoryStream = offService.GetWordStream(_host.ContentRootPath);
		
		memoryStream.Position = 0;
		
		return File(memoryStream, "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "File.docx"); // returns a FileStreamResult
	}
}	

Use the path in the File IO methods, example below:

File.Copy(path + "\\DocPath\\File.docm", strFileName);

In the debug mode, this path is the WebAPI Project absolute path to the directory. During runtime, this path is the absolute path where the files are hosted on the Server.