Artur Woźniak

May 13, 2020 in Quick Tips

Quick Tips: add and remove active style

In this quick tip, I would like to show an easy way to add an active style to any HTML element.

To do that we’re going to need:

  1. Simple array with objects
  2. Map method to iterate over our array
  3. Function  with parameter for our click event
  4. Find  method to return array element that pass a test
  5. UseState hook
  6. Styles for active element

1. Simple array with objects

const textData = [
  {
    id1,
    text'Yes! I\'m active 💪'
  },
  {
    id2,
    text'My turn now 🤠'
  },
  {
    id3,
    text'Thanks for activating 😘'
  }
];

2. Map method to iterate over our array

textData.map(({ id, text }) => {
        return (
          <p
            key={id}
            style={id === activeId ? activeStyle : {}}
            onClick={handleClick(id)}
          >
            {id === activeId ? text : "Activate me, please"}
          </p>
        );
      })

3. Function  with parameter for our click event and find method to return array element that pass a test

const handleClick = (id)=> ()=> {
    const activeElement = textData.find((item)=> item.id === id )

    activeElement && setActiveId(id)
  }

4. useState hook  - to  keep tracking current clicked element

const [activeId, setActiveId] = useState(null);

5. Styles for active element

const activeStyle = {
  background'hotpink',
  color'white'
};

Once we put all the small chunks together, we get this as a result:

import React, { useState } from 'react';
import { render } from 'react-dom';

const textData = [
  {
    id1,
    text'Yes! I\'m active 💪'
  },
  {
    id2,
    text'My turn now 🤠'
  },
  {
    id3,
    text'Thanks for activating 😘'
  }
];

const activeStyle = {
  background'hotpink',
  color'white'
};

const App = ()=> {
  const [activeId, setActiveId] = useState(null);

  const handleClick = (id)=> ()=> {
    const activeElement = textData.find((item)=> item.id === id )

    activeElement && setActiveId(id)
  }

    return (
      <div>
        {textData.map(({ id, text }) => {
        return (
          <p
            key={id}
            style={id === activeId ? activeStyle : {}}
            onClick={handleClick(id)}
          >
            {id === activeId ? text : "Activate me, please"}
          </p>
        );
      })}

      </div>
    );
}

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

Code result will look like that:

If you’re wondering if it’s the only way, the answer is simple — of course not! The point was to show you an example solution!

For instance, let’s do the same for page navigation, but using the data attribute.
We need to build our Navigation component with the list of navigation options. To do that, we need to return nav tag (<nav>) with the three list items <li> - Home, About, Contact -  from the Navigation component.

The first version could look like this:

import React, { forwardRef } from "react";

const style = {
  nav: {
    position"fixed",
    top0,
    right0,
    left0,
    backgroundColor"AliceBlue",
    padding"35px",
    color"DarkOrange"
  },
  list: {
    margin0,
    padding0,
    display"flex",
    justifyContent"flex-end"
  },
  listItem: {
    listStyleType"none",
    marginRight"20px",
    textTransform"uppercase",
    cursor"pointer"
  }
};

const activeStyle = {
  ...style.listItem,
  color"black",
  borderBottom"1px solid"
};

const Navigation = forwardRef(
  ({ currentOption, handleNavOptionClick }, ref) => {
    const { nav, list, listItem } = style;

    return (
      <nav ref={ref} style={nav}>
        <ul style={list}>
          <li
            style={currentOption === "home" ? activeStyle : listItem}
            data-name="home"
            onClick={handleNavOptionClick}
          >
            Home
          </li>
          <li
            style={currentOption === "about" ? activeStyle : listItem}
            data-name="about"
            onClick={handleNavOptionClick}
          >
            About
          </li>
          <li
            style={currentOption === "contact" ? activeStyle : listItem}
            data-name="contact"
            onClick={handleNavOptionClick}
          >
            Contact
          </li>
        </ul>
      </nav>
    );
  }
);

export default Navigation;

As you can see, we have to pass two properties — ‘currentOption’ and ‘handleNavOptionClick’. Every list item element has a data-name attribute. We will use it to define which element was clicked, then we will store the data-name value in our App component state, and after that, we will pass it through the ‘currentOption’ property to list item style attribute and identify the selected navigation option — home, about or contact.

What I don’t like in the above code example is that I have to manually set <li> tag with all the necessary attributes. To avoid copy/paste of the whole list item when adding a new option in the navigation, we could define a simple array of objects representing our navigation options, and use the map() method to iterate over it calling function on each element of the array.
In that case, our code will change into the following form:

import React, { forwardRef } from "react";

const styles = {
  nav: {
    position"fixed",
    top0,
    right0,
    left0,
    backgroundColor"AliceBlue",
    padding"35px",
    color"DarkOrange"
  },
  list: {
    margin0,
    padding0,
    display"flex",
    justifyContent"flex-end"
  },
  listItem: {
    listStyleType"none",
    marginRight"20px",
    textTransform"uppercase",
    cursor"pointer"
  }
};

const activeStyle = {
  ...styles.listItem,
  color"black",
  borderBottom"1px solid"
};

const navigationOptions = [
  {
    id1,
    option"home"
  },
  {
    id2,
    option"about"
  },
  {
    id3,
    option"contact"
  }
];

const Navigation = forwardRef(
  ({ currentOption, handleNavOptionClick }, ref) => {
    const { nav, list, listItem } = styles;

    return (
      <nav ref={ref} style={nav}>
        <ul style={list}>
          {navigationOptions &&
            navigationOptions.map(({ id, option }) => {
              return (
                <li
                  key={option}
                  style={currentOption === option ? activeStyle : listItem}
                  data-name={option}
                  onClick={handleNavOptionClick}
                >
                  {option}
                </li>
              );
            })}
        </ul>
      </nav>
    );
  }
);

export default Navigation;

Now, to add a new option, we have to extend our ’navigationOptions’ array by a new object, nothing else.

What we are missing?
We still have to import our Navigation component to our App component, initialize the state and handleNavOptionClick event.

import React, { useState, useRef } from "react";
import { render } from "react-dom";
import Navigation from "./components/Navigation";

const App = () => {
  const [activeNavOption, setActiveNavOption] = useState("home");
  const navigation = React.createRef();

  const handleNavOptionClick = event => {
    const navOptionName = event.target.getAttribute("data-name");
    setActiveNavOption(navOptionName);
  };

  return (
    <div>
      <Navigation
        ref={navigation}
        currentOption={activeNavOption}
        handleNavOptionClick={handleNavOptionClick}
      />
    </div>
  );
};

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

The code result will look like that:

In the above examples, we have successfully added an active style using:

  • state
  • onClick event
  • find
  • getAttribute

In our examples, I used the inline styles to keep things simple, but for adding an active class, you can follow the same pattern only exchange style attribute to className.

I’m sure that you have noticed that I’m using React.forwardRef in my Navigation component.
Next time, I will explain why :)

Stay tuned!