One place for hosting & domains

      Handle

      How to Handle File Uploads in Vue 2


      In this article, we will talk about how to handle file uploads with VueJs. We will create an images uploader that allow user to upload single or multiple images file by drag and drop or select file dialog.

      We will then upload the selected images and display them accordingly. We will also learn to filter the upload file type, for example, we only allow images, do not allow file type like PDF.

      Image uploader

      File Upload UI & API

      File upload consists of two parts: the UI (front-end) and the API (back-end). We will be using VueJs to handle the UI part. We need a backend application to accept the uploaded files. You may follow the backend tutorials or download and run either one of these server side application to handle file upload for your backend:-

      We will be using File upload with Hapi.js as our backend throughout this articles. We will also learn the tricks to enable fake upload on the front-end.

      Setup Project with Vue-Cli

      We will be using vue-cli to scaffold Vue.js projects. We will be using the webpack-simple project template.

      # install cli
      npm install vue-cli -g
      
      # then create project, with sass
      # follow the instructions to install all necessary dependencies
      vue init webpack-simple file-upload-vue
      

      Alright, all set. Let’s proceed to create our component.

      File Upload Component

      We will write our code in App.vue. Remove all the auto-generated code in the file.

      <!-- App.vue -->
      
      <!-- HTML Template -->
      <template>
        <div id="app">
          <div class="container">
            <!--UPLOAD-->
            <form enctype="multipart/form-data" novalidate v-if="isInitial || isSaving">
              <h1>Upload images</h1>
              <div class="dropbox">
                <input type="file" multiple :name="uploadFieldName" :disabled="isSaving" @change="filesChange($event.target.name, $event.target.files); fileCount = $event.target.files.length"
                  accept="image/*" class="input-file">
                  <p v-if="isInitial">
                    Drag your file(s) here to begin<br> or click to browse
                  </p>
                  <p v-if="isSaving">
                    Uploading {{ fileCount }} files...
                  </p>
              </div>
            </form>
        </div>
      </template>
      
      <!-- Javascript -->
      <script>
      </script>
      
      <!-- SASS styling -->
      <style lang="scss">
      </style>
      

      Notes:-

      1. Our App.vue component consists of 3 part: template (HTML), script (Javascript) and styles (SASS).
      2. Our template has an upload form.
      3. The form attribute enctype="multipart/form-data" is important. To enable file upload, this attribute must be set. Learn more about enctype here.
      4. We have a file input <input type="file" /> to accept file upload. The property multiple indicate it’s allow multiple file upload. Remove it for single file upload.
      5. We will handle the file input change event. Whenever the file input change (someone drop or select files), we will trigger the filesChange function and pass in the control name and selected files $event.target.files, and then upload to server.
      6. We limit the file input to accept images only with the attribute accept="image/*".
      7. The file input will be disabled during upload, so user can only drop / select files again after upload complete.
      8. We capture the fileCount of the when file changes. We use the fileCount variable in displaying number of files uploading Uploading {{ fileCount }} files....

      Style our File Upload Component

      Now, that’s the interesting part. Currently, our component look like this:

      File upload component without styling

      We need to transform it to look like this:

      File upload component with styling

      Let’s style it!

      <!-- App.vue -->
      ...
      
      <!-- SASS styling -->
      <style lang="scss">
        .dropbox {
          outline: 2px dashed grey; /* the dash box */
          outline-offset: -10px;
          background: lightcyan;
          color: dimgray;
          padding: 10px 10px;
          min-height: 200px; /* minimum height */
          position: relative;
          cursor: pointer;
        }
      
        .input-file {
          opacity: 0; /* invisible but it's there! */
          width: 100%;
          height: 200px;
          position: absolute;
          cursor: pointer;
        }
      
        .dropbox:hover {
          background: lightblue; /* when mouse over to the drop zone, change color */
        }
      
        .dropbox p {
          font-size: 1.2em;
          text-align: center;
          padding: 50px 0;
        }
      </style>
      

      With only few lines of scss, our component looks prettier now.

      Notes:-

      1. We make the file input invisible by applying opacity: 0 style. This doesn’t hide the file input, it just make it invisible.
      2. Then, we style the file input parent element, the dropbox css class. We make it look like a drop file zone surround with dash.
      3. Then, we align the text inside dropbox to center.

      File Upload Component Code

      Let’s proceed to code our component.

      <!-- App.vue -->
      ...
      
      <!-- Javascript -->
      <script>
        import { upload } from './file-upload.service';
      
        const STATUS_INITIAL = 0, STATUS_SAVING = 1, STATUS_SUCCESS = 2, STATUS_FAILED = 3;
      
        export default {
          name: 'app',
          data() {
            return {
              uploadedFiles: [],
              uploadError: null,
              currentStatus: null,
              uploadFieldName: 'photos'
            }
          },
          computed: {
            isInitial() {
              return this.currentStatus === STATUS_INITIAL;
            },
            isSaving() {
              return this.currentStatus === STATUS_SAVING;
            },
            isSuccess() {
              return this.currentStatus === STATUS_SUCCESS;
            },
            isFailed() {
              return this.currentStatus === STATUS_FAILED;
            }
          },
          methods: {
            reset() {
              // reset form to initial state
              this.currentStatus = STATUS_INITIAL;
              this.uploadedFiles = [];
              this.uploadError = null;
            },
            save(formData) {
              // upload data to the server
              this.currentStatus = STATUS_SAVING;
      
              upload(formData)
                .then(x => {
                  this.uploadedFiles = [].concat(x);
                  this.currentStatus = STATUS_SUCCESS;
                })
                .catch(err => {
                  this.uploadError = err.response;
                  this.currentStatus = STATUS_FAILED;
                });
            },
            filesChange(fieldName, fileList) {
              // handle file changes
              const formData = new FormData();
      
              if (!fileList.length) return;
      
              // append the files to FormData
              Array
                .from(Array(fileList.length).keys())
                .map(x => {
                  formData.append(fieldName, fileList[x], fileList[x].name);
                });
      
              // save it
              this.save(formData);
            }
          },
          mounted() {
            this.reset();
          },
        }
      
      </script>
      

      Notes:-

      1. Our component will have a few statuses: STATUSINITIAL, STATUSSAVING, STATUSSUCCESS, STATUSFAILED, the variable name is pretty expressive themselves.
      2. Later on, we will call the Hapi.js file upload API to upload images, the API accept a field call photos. That’s our file input field name.
      3. We handle the file changes with the filesChange function. FileList is an object returned by the files property of the HTML <input> element. It allow us to access the list of files selected with the <input type=“file”> element. Learn more [here]((https://developer.mozilla.org/en/docs/Web/API/FileList).
      4. We then create a new FormData, and append all our photos files to it. FormData interface provides a way to easily construct a set of key/value pairs representing form fields and their values. Learn more here.
      5. The save function will call our file upload service (hang on, we will create the service next!). We also set the status according to the result.
      6. mount() is the vue component life cycle hook. During that point, we will set our component status to initial state.

      File Upload Service

      Let’s proceed to create our service. We will be using axios to make HTTP calls.

      Install axios

      # install axios
      npm install axios --save
      

      Service

      // file-upload.service.js
      
      import * as axios from 'axios';
      
      const BASE_URL = 'http://localhost:3001';
      
      function upload(formData) {
          const url = `${BASE_URL}/photos/upload`;
          return axios.post(url, formData)
              // get data
              .then(x => x.data)
              // add url field
              .then(x => x.map(img => Object.assign({},
                  img, { url: `${BASE_URL}/images/${img.id}` })));
      }
      
      export { upload }
      

      Nothing much, the code is pretty expressive itself. We upload the files, wait for the result, map it accordingly.

      You may run the application now with npm run dev command. Try uploading a couple of images, and it’s working! (Remember to start your backend server)

      Display Success and Failed Result

      We can upload the files successfully now. However, there’s no indication in UI. Let’s update our HTML template.

      <!-- App.vue -->
      
      <!-- HTML Template -->
      <template>
        <div id="app">
          <div class="container">
            ...form...
      
            <!--SUCCESS-->
            <div v-if="isSuccess">
              <h2>Uploaded {{ uploadedFiles.length }} file(s) successfully.</h2>
              <p>
                <a href="https://www.digitalocean.com/community/tutorials/javascript:void(0)" @click="reset()">Upload again</a>
              </p>
              <ul class="list-unstyled">
                <li v-for="item in uploadedFiles">
                  <img :src="item.url" class="img-responsive img-thumbnail" :alt="item.originalName">
                </li>
              </ul>
            </div>
            <!--FAILED-->
            <div v-if="isFailed">
              <h2>Uploaded failed.</h2>
              <p>
                <a href="https://www.digitalocean.com/community/tutorials/javascript:void(0)" @click="reset()">Try again</a>
              </p>
              <pre>{{ uploadError }}</pre>
            </div>
          </div>
        </div>
      </template>
      

      Notes:-

      1. Display the uploaded image when upload successfully.
      2. Display the error message when upload failed.

      Fake the Upload in Front-end

      If you are lazy to start the back-end application (Hapi, Express, etc) to handle file upload. Here is a fake service to replace the file upload service.

      // file-upload.fake.service.js
      
      function upload(formData) {
          const photos = formData.getAll('photos');
          const promises = photos.map((x) => getImage(x)
              .then(img => ({
                  id: img,
                  originalName: x.name,
                  fileName: x.name,
                  url: img
              })));
          return Promise.all(promises);
      }
      
      function getImage(file) {
          return new Promise((resolve, reject) => {
              const fReader = new FileReader();
              const img = document.createElement('img');
      
              fReader.onload = () => {
                  img.src = fReader.result;
                  resolve(getBase64Image(img));
              }
      
              fReader.readAsDataURL(file);
          })
      }
      
      function getBase64Image(img) {
          const canvas = document.createElement('canvas');
          canvas.width = img.width;
          canvas.height = img.height;
      
          const ctx = canvas.getContext('2d');
          ctx.drawImage(img, 0, 0);
      
          const dataURL = canvas.toDataURL('image/png');
      
          return dataURL;
      }
      
      export { upload }
      

      Came across this solution in this Stackoverflow post. Pretty useful. My online demo is using this service.

      Basically, what the code do is read the source, draw it in canvas, and save it as data url with the canvas toDataURL function. Learn more about canvas here.

      Now you can swap the real service with the fake one.

      <!-- App.vue -->
      ...
      
      <!-- Javascript -->
      <script>
        // swap as you need
        import { upload } from './file-upload.fake.service'; // fake service
        // import { upload } from './file-upload.service';   // real service
      </script>
      
      ...
      
      

      Done! Stop your backend API, refresh your browser, you should see our app is still working, calling fake service instead.

      Bonus: Delay Your Promises

      Sometimes, you may want to delay the promises to see the state changes. In our case, the file upload may complete too fast. Let’s write a helper function for that.

      // utils.js
      
      // utils to delay promise
      function wait(ms) {
          return (x) => {
              return new Promise(resolve => setTimeout(() => resolve(x), ms));
          };
      }
      
      export { wait }
      
      

      Then, you can use it in your component

      <!-- App.vue -->
      ...
      
      <!-- Javascript -->
      <script>
        import { wait } from './utils';
        ...
      
        save(formData) {
           ....
      
              upload(formData)
                .then(wait(1500)) // DEV ONLY: wait for 1.5s 
                .then(x => {
                  this.uploadedFiles = [].concat(x);
                  this.currentStatus = STATUS_SUCCESS;
                })
               ...
      
            },
      </script>
      

      Summary

      That’s it. This is how you can handle file upload without using any 3rd party libraries and plugins in Vue. It isn’t that hard right?

      Happy coding!

      The UI (Front-end)

      The API (Back-end) Tutorials and Sourcode



      Source link

      How To Handle DOM and Window Events with React


      The author selected Creative Commons to receive a donation as part of the Write for DOnations program.

      Introduction

      In web development, events represent actions that happen in the web browser. By responding to events with event handlers, you can create dynamic JavaScript applications that respond to any user action, including clicking with a mouse, scrolling along a webpage, touching a touch screen, and more.

      In React apps, you can use event handlers to update state data, trigger prop changes, or prevent default browser actions. To do this, React uses a SyntheticEvent wrapper instead of the native Event interface. SyntheticEvent closely emulates the standard browser event, but provides more consistent behavior for different web browsers. React also gives you tools to safely add and remove a Window event listener when a component mounts and unmounts from the Document Object Model (DOM), giving you control over Window events while preventing memory leaks from improperly removed listeners.

      In this tutorial, you’ll learn how to handle events in React. You’ll build several sample components that handle user events, including a self-validating input component and an informative tooltip for the input form. Throughout the tutorial, you’ll learn how to add event handlers to components, pull information from the SyntheticEvent, and add and remove Window event listeners. By the end of this tutorial, you’ll be able to work with a variety of event handlers and apply the catalog of events supported by React.

      Prerequisites

      In this step, you’ll create a validating component using an <input> HTML element and the onChange event handler. This component will accept input and validate it, or make sure that the content adheres to a specific text pattern. You’ll use the SyntheticEvent wrapper to pass event data into the callback function and update the component using the data from the <input>. You will also call functions from the SyntheticEvent, such as preventDefault to prevent standard browser actions.

      In React, you don’t need to select elements before adding event listeners. Instead, you add event handlers directly to your JSX using props. There are a large number of supported events in React, including common events such as onClick or onChange and less common events such as onWheel.

      Unlike native DOM onevent handlers, React passes a special wrapper called SyntheticEvent to the event handler rather than the native browser Event. The abstraction helps reduce cross-browser inconsistencies and gives your components a standard interface for working with events. The API for SyntheticEvent is similar to the native Event, so most tasks are accomplished in the same manner.

      To demonstrate this, you will start by making your validating input. First, you will create a component called FileNamer. This will be a <form> element with an input for naming a file. As you fill in the input, you’ll see the information update a preview box above the component. The component will also include a submit button to run the validation, but for this example the form will not actually submit anything.

      First, create the directory:

      • mkdir src/components/FileNamer

      Then open FileNamer.js in your text editor:

      • nano src/components/FileNamer/FileNamer.js

      Inside FileNamer.js, create a wrapper <div>, then add another <div> with a class name of preview and a <form> element inside the wrapper by writing the following lines of code:

      events-tutorial/src/components/FileNamer/FileNamer.js

      import React from 'react';
      
      export default function FileNamer() {
        return(
          <div className="wrapper">
            <div className="preview">
            </div>
            <form>
            </form>
          </div>
        )
      }
      

      Next, add an input element for the name to display in the preview box and a Save button. Add the following highlighted lines:

      events-tutorial/src/components/FileNamer/FileNamer.js

      import React from 'react';
      
      export default function FileNamer() {
        return(
          <div className="wrapper">
            <div className="preview">
              <h2>Preview:</h2>
            </div>
            <form>
              <label>
                <p>Name:</p>
                <input name="name" />
              </label>
              <div>
                <button>Save</button>
              </div>
            </form>
          </div>
        )
      }
      

      In the preview <div>, you added an <h2> element with the text Preview. This will be your preview box. Inside your form, you added an <input> surrounded by a <label> element with Name: as its text. Then you added a button called Save directly before the closing <form> tag.

      Save and close the file.

      Next, open App.js:

      • nano src/components/App/App.js

      Import FileNamer, then render inside the App function by adding the following highlighted lines:

      events-tutorial/src/components/App/App.js

      import React from 'react';
      import FileNamer from '../FileNamer/FileNamer';
      
      function App() {
          return <FileNamer />
      }
      
      export default App;
      

      Save and close the file. When you do the browser will refresh and you’ll see your component.

      Name element

      Next, add some light styling to help define the sections and to add some padding and margins to the elements.

      Open FileNamer.css in your text editor:

      • nano src/components/FileNamer/FileNamer.css

      Give the .preview class a gray border and padding, then give the .wrapper class a small amount of padding. Display the items in a column using flex and flex-direction, and make all the text align left. Finally, remove the default button styles by removing the border and adding a black border:

      events-tutorial/src/components/FileNamer/FileNamer.css

      .preview {
          border: 1px darkgray solid;
          padding: 10px;
      }
      
      .wrapper {
          display: flex;
          flex-direction: column;
          padding: 20px;
          text-align: left;
      }
      
      .wrapper button {
          background: none;
          border: 1px black solid;
          margin-top: 10px;
      }
      

      Save and close the file. Then open FileNamer.js:

      • nano src/components/FileNamer/FileNamer.js

      Import the styles to apply them to your component:

      events-tutorial/src/components/FileNamer/FileNamer.js

      import React from 'react';
      import './FileNamer.css';
      
      export default function FileNamer() {
        return(
          <div className="wrapper">
            <div className="preview">
              <h2>Preview:</h2>
            </div>
            <form>
              <label>
                <p>Name:</p>
                <input name="name" />
              </label>
              <div>
                <button>Save</button>
              </div>
            </form>
          </div>
        )
      }
      

      Save the file. When you do, the browser will refresh and you’ll find the component has the new styles.

      Styled component

      Now that you have a basic component, you can add event handlers to the <input> element. But first, you’ll need a place to store the data in the input field. Add the useState Hook to hold the input:

      events-tutorial/src/components/FileNamer/FileNamer.js

      import React, { useState } from 'react';
      import './FileNamer.css';
      
      export default function FileNamer() {
        const [name, setName] = useState('');
        return(
          <div className="wrapper">
            <div className="preview">
              <h2>Preview: {name}.js</h2>
            </div>
            <form>
              <label>
                <p>Name:</p>
                <input name="name" />
              </label>
              <div>
                <button>Save</button>
              </div>
            </form>
          </div>
        )
      }
      

      In this code, you destructured useState into a variable name to hold the input and a function called setName to update the data. Then you displayed the name in the preview section followed by the .js extension, as if the user were naming a file.

      Now that you can store the input data, you can add an event handler to the <input> component. There are often several different event handlers you can use for a given task. In this case, your app needs to capture the data the user types into the element. The most common handler for this situation is onChange, which fires every time the component changes. However, you could also use keyboard events, such as onKeyDown, onKeyPress, and onKeyUp. The difference primarily has to do with when the event fires and the information passed to the SyntheticEvent object. For example, onBlur, an event for when an element becomes unfocused, fires before onClick. If you want to handle user information before another event fires, you can pick an earlier event.

      Your choice of event is also determined by the type of data you want to pass to the SyntheticEvent. The onKeyPress event, for example, will include the charCode of the key that the user pressed, while onChange will not include the specific character code, but will include the full input. This is important if you want to perform different actions depending on which key the user pressed.

      For this tutorial, use onChange to capture the entire input value and not just the most recent key. This will save you the effort of storing and concatenating the value on every change.

      Create a function that takes the event as an argument and pass it to the <input> element using the onChange prop:

      events-tutorial/src/components/FileNamer/FileNamer.js

      import React, { useState } from 'react';
      import './FileNamer.css';
      
      export default function FileNamer() {
        const [name, setName] = useState('');
        return(
          <div className="wrapper">
            <div className="preview">
              <h2>Preview: {name}.js</h2>
            </div>
            <form>
              <label>
                <p>Name:</p>
                <input name="name" onChange={event => {}}/>
              </label>
              <div>
                <button>Save</button>
              </div>
            </form>
          </div>
        )
      }
      

      As mentioned earlier, the event here is not the native browser event. It’s the SyntheticEvent provided by React, which is often treated the same. In the rare case you need the native event, you can use the nativeEvent attribute on the SyntheticEvent.

      Now that you have the event, pull out the current value from the target.value property of the event. Pass the value to setName to update the preview:

      events-tutorial/src/components/FileNamer/FileNamer.js

      import React, { useState } from 'react';
      import './FileNamer.css';
      
      export default function FileNamer() {
        const [name, setName] = useState('');
        return(
          <div className="wrapper">
            <div className="preview">
              <h2>Preview: {name}.js</h2>
            </div>
            <form>
              <label>
                <p>Name:</p>
                <input
                 autoComplete="off"
                 name="name"
                 onChange={event => setName(event.target.value) }
               />
              </label>
              <div>
                <button>Save</button>
              </div>
            </form>
          </div>
        )
      }
      

      In addition, you set the attribute autoComplete to "off" to turn off browser suggestions.

      Save the file. When you do, the page will reload, and when you type in the <input> you’ll see an update in the preview.

      Typing into the input element

      Note: You could also access the name of the input using event.target.name. This would be useful if you were using the same event handler across multiple inputs, since the name would automatically match the name attribute of the component.

      At this point, you have a working event handler. You are taking the user information, saving it to state, and updating another component with the data. But in addition to pulling information from an event, there are situations where you’ll need to halt an event, such as if you wanted to prevent a form submission or prevent a keypress action.

      To stop an event, call the preventDefault action on the event. This will stop the browser from performing the default behavior.

      In the case of the FileNamer component, there are certain characters that could break the process of choosing a file that your app should forbid. For example, you wouldn’t want a user to add a * to a filename since it conflicts with the wildcard character, which could be interpreted to refer to a different set of files. Before a user can submit the form, you’ll want to check to make sure there are no invalid characters. If there is an invalid character, you’ll stop the browser from submitting the form and display a message for the user.

      First, create a Hook that will generate an alert boolean and a setAlert function. Then add a <div> to display the message if alert is true:

      events-tutorial/src/components/FileNamer/FileNamer.js

      import React, { useState } from 'react';
      import './FileNamer.css';
      
      export default function FileNamer() {
        const [name, setName] = useState('');
        const [alert, setAlert] = useState(false);
      
        return(
          <div className="wrapper">
            <div className="preview">
              <h2>Preview: {name}.js</h2>
            </div>
            <form>
              <label>
                <p>Name:</p>
                <input
                  autoComplete="off"
                  name="name"
                  onChange={event => setName(event.target.value) }
                />
              </label>
              {alert && <div> Forbidden Character: *</div>}
              <div>
                <button>Save</button>
              </div>
            </form>
          </div>
        )
      }
      

      In this code, you used the && operator to only show the new <div> if alert is set equal to true first. The message in the <div> will tell the user that the * character is not allowed in the input.

      Next, create a function called validate. Use the regular expression .test method to find out if the string contains a *. If it does, you will prevent the form submission:

      events-tutorial/src/components/FileNamer/FileNamer.js

      import React, { useState } from 'react';
      import './FileNamer.css';
      
      export default function FileNamer() {
        const [name, setName] = useState('');
        const [alert, setAlert] = useState(false);
        const validate = event => {
          if(/*/.test(name)) {
            event.preventDefault();
            setAlert(true);
            return;
          }
            setAlert(false);
       };
      
        return(
          <div className="wrapper">
            <div className="preview">
              <h2>Preview: {name}.js</h2>
            </div>
            <form>
              <label>
                <p>Name:</p>
                <input
                  autoComplete="off"
                  name="name"
                  onChange={event => setName(event.target.value) }
                />
              </label>
              {alert && <div> Forbidden Character: *</div>}
              <div>
                <button onClick={validate}>Save</button>
              </div>
            </form>
          </div>
        )
      }
      

      When the validate function is called and the test returns true, it will use event.preventDefault then call setAlert(true). Otherwise, it will call setAlert(false). In the last part of the code, you added the event handler to the <button> element with onClick.

      Save the file. As before, you could have also used onMouseDown, but onClick is more common and thus allows you to avoid any unexpected side effects. This form doesn’t have any submit actions, but by preventing the default action, you prevent the page from reloading:

      Prevent Default and trigger a warning

      Now you have a form that uses two event handlers: onChange and onClick. You are using the event handlers to connect user actions to the component and the application, making it interactive. In doing so, you learned to add events to DOM elements, and how there are several events that fire on the same action, but that provide different information in the SyntheticEvent. You also learned how to extract information from the SyntheticEvent, update other components by saving that data to state, and halt an event using preventDefault.

      In the next step, you’ll add multiple events to a single DOM element to handle a variety of user actions.

      Step 2 — Adding Multiple Event Handlers to the Same Element

      There are situations when a single component will fire multiple events, and you’ll need to be able to connect to the different events on a single component. For example, in this step you’ll use the onFocus and onBlur event handlers to give the user just-in-time information about the component. By the end of this step, you’ll know more about the different supported events in React and how to add them to your components.

      The validate function is helpful for preventing your form from submitting bad data, but it’s not very helpful for user experience: The user only receives information about the valid characters after they’ve filled out the entire form. If there were multiple fields, it wouldn’t give the user any feedback until the last step. To make this component more user friendly, display the allowed and disallowed characters when the user enters the field by adding an onFocus event handler.

      First, update the alert <div> to include information about what characters are allowed. Tell the user alphanumeric characters are allowed and the * is not allowed:

      events-tutorial/src/components/FileNamer/FileNamer.js

      import React, { useState } from 'react';
      import './FileNamer.css';
      
      export default function FileNamer() {
      ...
        return(
          <div className="wrapper">
            <div className="preview">
              <h2>Preview: {name}.js</h2>
            </div>
            <form>
              <label>
                <p>Name:</p>
                <input
                  autocomplete="off"
                  name="name"
                  onChange={event => setName(event.target.value) }
                />
              </label>
              {alert &&
               <div>
                 <span role="img" aria-label="allowed">✅</span> Alphanumeric Characters
                 <br />
                 <span role="img" aria-label="not allowed">⛔️</span> *
               </div>
             }
              <div>
                <button onClick={validate}>Save</button>
              </div>
            </form>
          </div>
        )
      }
      

      In this code, you used Accessible Rich Internet Applications (ARIA) standards to make the component more accessible to screen readers.

      Next, add another event handler to the <input> element. You will alert the user about the allowed and disallowed characters when they activate the component by either clicking or tabbing into the input. Add in the following highlighted line:

      events-tutorial/src/components/FileNamer/FileNamer.js

      import React, { useState } from 'react';
      import './FileNamer.css';
      
      export default function FileNamer() {
      ...
        return(
          <div className="wrapper">
            <div className="preview">
              <h2>Preview: {name}.js</h2>
            </div>
            <form>
              <label>
                <p>Name:</p>
                <input
                  autocomplete="off"
                  name="name"
                  onChange={event => setName(event.target.value) }
                  onFocus={() => setAlert(true)}
                />
              </label>
              {alert &&
                <div>
                  <span role="img" aria-label="allowed">✅</span> Alphanumeric Characters
                  <br />
                  <span role="img" aria-label="not allowed">⛔️</span> *
                </div>
              }
              <div>
                <button onClick={validate}>Save</button>
              </div>
            </form>
          </div>
        )
      }
      

      You added the onFocus event handler to the <input> element. This event triggers when the user selects the field. After adding the event handler, you passed an anonymous function to onFocus that will call setAlert(true) and display the data. In this case, you don’t need any information from the SyntheticEvent; you only need to trigger an event when the user acts. React is still sending the SyntheticEvent to the function, but in the current situation you don’t need to use the information in it.

      Note: You could trigger the data display with onClick or even onMouseDown, but that wouldn’t be accessible for users that use the keyboard to tab into the form fields. In this case, the onFocus event will handle both cases.

      Save the file. When you do, the browser will refresh and the information will remain hidden until the user clicks on the input.

      Trigger the event when clicking on the input

      The user information now appears when the field is focused, but now the data is present for the duration of the component. There’s no way to make it go away. Fortunately, there’s another event called onBlur that fires when the user leaves an input. Add the onBlur event handler with an anonymous function that will set the alert to false. Like onFocus, this will work both when a user clicks away or when a user tabs away:

      events-tutorial/src/components/FileNamer/FileNamer.js

      import React, { useState } from 'react';
      import './FileNamer.css';
      
      export default function FileNamer() {
      ...
        return(
          <div className="wrapper">
            <div className="preview">
              <h2>Preview: {name}.js</h2>
            </div>
            <form>
              <label>
                <p>Name:</p>
                <input
                  autocomplete="off"
                  name="name"
                  onBlur={() => setAlert(false)}
                  onChange={event => setName(event.target.value) }
                  onFocus={() => setAlert(true)}
                />
              </label>
              {alert &&
                <div>
                  <span role="img" aria-label="allowed">✅</span> Alphanumeric Characters
                  <br />
                  <span role="img" aria-label="not allowed">⛔️</span> *
                </div>
              }
              <div>
                <button onClick={validate}>Save</button>
              </div>
            </form>
          </div>
        )
      }
      

      Save the file. When you do, the browser will refresh and the information will display when the user clicks on the element and disappear when the user clicks away:

      Show information on focus and hide on blur

      You can add as many event handlers as you need to an element. If you have an idea of an event you need, but aren’t sure of the name, scroll through the supported events and you may find what you need.

      In this step you added multiple event handlers to a single DOM element. You learned how different event handlers can handle a broad range of events—such as both click and tab—or a narrow range of events.

      In the next step, you’ll add global event listeners to the Window object to capture events that occur outside the immediate component.

      Step 3 — Adding Window Events

      In this step, you’ll put the user information in a pop-up component that will activate when the user focuses an input and will close when the user clicks anywhere else on the page. To achieve this effect, you’ll add a global event listener to the Window object using the useEffect Hook. You’ll also remove the event listener when the component unmounts to prevent memory leaks, when your app take up more memory than it needs to.

      By the end of this step, you’ll be able to safely add and remove event listeners on individual components. You’ll also learn how to use the useEffect Hook to perform actions when a component mounts and unmounts.

      In most cases, you’ll add event handlers directly to DOM elements in your JSX. This keeps your code focused and prevents confusing situations where a component is controlling another component’s behavior through the Window object. But there are times in which you’ll need to add global event listeners. For example, you may want a scroll listener to load new content, or you may want to capture click events outside of a component.

      In this tutorial, you only want to show the user the information about the input if they specifically ask for it. After you display the information, you’ll want to hide it whenever the user clicks the page outside the component.

      To start, move the alert display into a new <div> with a className of information-wrapper. Then add a new button with a className of information and an onClick event that will call setAlert(true):

      events-tutorial/src/components/FileNamer/FileNamer.js

      import React, { useState } from 'react';
      import './FileNamer.css';
      
      export default function FileNamer() {
      ...
        return(
          <div className="wrapper">
            <div className="preview">
              <h2>Preview: {name}.js</h2>
            </div>
            <form>
              <label>
                <p>Name:</p>
                <input
                  autocomplete="off"
                  name="name"
                  onChange={event => setName(event.target.value) }
                />
              </label>
              <div className="information-wrapper">
                <button
                  className="information"
                  onClick={() => setAlert(true)}
                  type="button"
                >
                  more information
                </button>
               {alert &&
                 <div className="popup">
                   <span role="img" aria-label="allowed">✅</span> Alphanumeric Characters
                   <br />
                   <span role="img" aria-label="not allowed">⛔️</span> *
                 </div>
               }
              </div>
              <div>
                <button onClick={validate}>Save</button>
              </div>
            </form>
          </div>
        )
      }
      

      You also removed the onFocus and onBlur handlers from the <input> element to remove the behavior from the last step.

      Save and close the file. Then open FileNamer.css:

      • nano src/components/FileNamer/FileNamer.css

      Add some styling to absolutely position the popup information above the button. Then change the <button> with a class of information to be blue with no border.

      events-tutorial/src/components/FileNamer/FileNamer.css

      
      .information {
         font-size: .75em;
         color: blue;
         cursor: pointer;
      }
      
      .wrapper button.information {
          border: none;
      }
      
      .information-wrapper {
         position: relative;
      }
      
      .popup {
          position: absolute;
          background: white;
          border: 1px darkgray solid;
          padding: 10px;
          top: -70px;
          left: 0;
      }
      
      .preview {
          border: 1px darkgray solid;
          padding: 10px;
      }
      
      .wrapper {
          display: flex;
          flex-direction: column;
          padding: 20px;
          text-align: left;
      }
      
      .wrapper button {
          background: none;
          border: 1px black solid;
          margin-top: 10px;
      }
      

      Save and close the file. When you do, the browser will reload, and when you click on more information, the information about the component will appear:

      Trigger information pop-up

      Now you can trigger the pop-up, but there’s no way to clear it. To fix that problem, add a global event listener that calls setAlert(false) on any click outside of the pop-up.

      The event listener would look something like this:

      window.addEventListener('click', () => setAlert(false))
      

      However, you have to be mindful about when you set the event listener in your code. You can’t, for example, add an event listener at the top of your component code, because then every time something changed, the component would re-render and add a new event listener. Since your component will likely re-render many times, that would create a lot of unused event listeners that take up memory.

      To solve this, React has a special Hook called useEffect that will run only when specific properties change. The basic structure is this:

      useEffect(() => {
       // run code when anything in the array changes
      }, [someProp, someOtherProp])
      

      In the simplified example, React will run the code in the anonymous function whenever someProp or someOtherProp changes. The items in the array are called dependencies. This Hook listens for changes to the dependencies and then runs the function after the change.

      Now you have the tools to add and remove a global event listener safely by using useEffect to add the event listener whenever alert is true and remove it whenever alert is false.

      There is one more step. When the component unmounts, it will run any function that you return from inside of your useEffect Hook. Because of this, you’ll also need to return a function that removes the event listener when the component unmounts.

      The basic structure would be like this:

      useEffect(() => {
       // run code when anything in the array changes
        return () => {} // run code when the component unmounts
      }, [someProp, someOtherProp])
      

      Now that you know the shape of your useEffect Hook, use it in your application. Open up FileNamer.js:

      • nano src/components/FileNamer/FileNamer.js

      Inside, import useEffect, then add an empty anonymous function with a dependency of alert and setAlert in the array after the function:

      events-tutorial/src/components/FileNamer/FileNamer.js

      import React, { useEffect, useState } from 'react';
      import './FileNamer.css';
      
      export default function FileNamer() {
        const [name, setName] = useState('');
        const [alert, setAlert] = useState(false);
      
        useEffect(() => {
        }, [alert, setAlert]);
      ...
      

      In this code, you added both alert and setAlert. To be complete, React recommends you add all external dependencies to the useEffect function. Since you will be calling the setAlert function, it can be considered a dependency. setAlert will not change after the first render, but it’s a good practice to include anything that could be considered a dependency.

      Next, inside the anonymous function, create a new function called handleWindowClick that calls setAlert(false):

      events-tutorial/src/components/FileNamer/FileNamer.js

      import React, { useEffect, useState } from 'react';
      import './FileNamer.css';
      
      export default function FileNamer() {
        const [name, setName] = useState('');
        const [alert, setAlert] = useState(false);
      
        useEffect(() => {
          const handleWindowClick = () => setAlert(false)
        }, [alert, setAlert]);
        ...
      }
      

      Then add a conditional that will call window.addEventListener('click', handleWindowClick) when alert is true and will call window.removeEventListener('click', handleWindowClick) when alert is false. This will add the event listener every time you trigger the pop-up and remove it everytime the pop-up is closed:

      events-tutorial/src/components/FileNamer/FileNamer.js

      import React, { useEffect, useState } from 'react';
      import './FileNamer.css';
      
      export default function FileNamer() {
        const [name, setName] = useState('');
        const [alert, setAlert] = useState(false);
      
        useEffect(() => {
          const handleWindowClick = () => setAlert(false)
          if(alert) {
            window.addEventListener('click', handleWindowClick);
          } else {
            window.removeEventListener('click', handleWindowClick);
          }
        }, [alert, setAlert]);
        ...
      }
      

      Finally, return a function that will remove the event listener. Once again, this will run when the component unmounts. There may not be a live event listener, but it’s still worth cleaning up in situations where the listener still exists:

      events-tutorial/src/components/FileNamer/FileNamer.js

      
      import React, { useEffect, useState } from 'react';
      import './FileNamer.css';
      
      export default function FileNamer() {
        const [name, setName] = useState('');
        const [alert, setAlert] = useState(false);
      
        useEffect(() => {
          const handleWindowClick = () => setAlert(false)
          if(alert) {
            window.addEventListener('click', handleWindowClick);
          } else {
            window.removeEventListener('click', handleWindowClick)
          }
          return () => window.removeEventListener('click', handleWindowClick);
        }, [alert, setAlert]);
        ...
      }
      

      Save the file. When you do, the browser will refresh. If you click on the more information button, the message will appear. If you look at the global event listeners in the developer tools, you’ll see there is a click listener:

      Click event listener

      Click anywhere outside the component. The message will disappear and you’ll no longer see the global click event listener.

      No click event listener

      Your useEffect Hook successfully added and removed a global event listener based on a user interaction. It wasn’t tied to a specific DOM element, but was instead triggered by a change in the component state.

      Note: From an accessibility standpoint, this component is not complete. If a user is not able to use the mouse, they will be stuck with an open pop-up because they would never be able to click outside the component. The solution would be to add another event listener for keydown that would also remove the message. The code would be nearly identical except the method would be keydown instead of click.

      In this step you added global event listeners inside a component. You also learned how to use the useEffect Hook to properly add and remove the event listener as the state changes and how to clean up event listeners when the component unmounts.

      Conclusion

      Event handlers give you the opportunity to align your components with user actions. These will give your applications a rich experience and will increase the interactive possibilities of your app. They will also give you the ability to capture and respond to user actions.

      React’s event handlers let you keep your event callbacks integrated with the HTML so that you can share functionality and design across an application. In most cases, you should focus on adding event handlers directly to DOM elements, but in situations where you need to capture events outside of the component, you can add event listeners and clean them up when they are no longer in use to prevent memory leaks and create performative applications.

      If you would like to look at more React tutorials, check out our React Topic page, or return to the How To Code in React.js series page. To learn more about events in JavaScript, read our Understanding Events in JavaScript and Using Event Emitters in Node.js tutorials.



      Source link