loading...

Creating a Dynamic Menu with React

daviddoes profile image David Clark Updated on ・5 min read

originally posted to medium


I must say — this was a somewhat difficult feature for me to figure out how to implement. I looked at libraries such as react-select and other’s implementations of similar features, but just couldn’t find what I was looking for. Stepping out of everyone’s comfort zone of using someone else’s hard work, I greased up my elbows and set to work.

React is still new to me, along with everything that is software development. Therefore, it takes a bit more mental computation to understand exactly what I’m trying to accomplish.

In this case, I needed to understand how to get previously-stored values into a element in a form. The idea is that the user is utilizing this form to record meditation sessions — or what I refer to as mindful moments — and in this form the user records the location where they had that mindful moment. Chances are they’re going to meditate in the same location more than once, so we need to make it so that the user can either use an already-existing location, or create a new one.

Code Walk-through

Let’s see how to do this…

// LocationSelect component

import React from 'react';
import { connect } from 'react-redux';
import { getMoments } from '../actions/moments';

Of course, we need to import React and connect, and also the action to get our list of moments — a call to our API — which contains the location that we need for our later on.

Let’s initialize our component’s state:

class LocationSelect extends React.Component {
  constructor(props){
    super(props);
    this.state = {
      location: ''
    };
  }

So we’re telling our component to store a location key in its state, but not yet giving it a value. We just giving that value a place to go later on.

Let’s get our moments object from our API:

  componentDidMount(){
    if (this.props.authToken){
     getMoments(this.props.authToken);
    }
  }

If our props contains the authToken , then run the getMoments action with that authToken. We want this to happen once this component mounts.

Inside our render(), we want to sort our select menu’s options to make it more user-friendly. To do this, we need to first get all of our previously-entered data, store it in a new array, then sort that array.

render() {
  let momentsList = [];
  this.props.moments.forEach(({ id, location }) => momentsList.push({ id, location }));

  let uniqueSet = [...new Set(momentsList.map(moment => moment.location))];

So we create our new array, momentsList. Since they’re passed as props, we need to grab them from there, and run a forEach, grabbing the id and the location from each iteration (in this case, each moment). Then, we’re pushing id and location into our new array from each moment we iterate over. We only want the id and location, not any other information that might be stored in that object.

We then need to create a new Set, so that we can store data of any type. We’re saying, Create a new array called uniqueSet, which will be a new Set created from a map() over our previous array, grabbing the location.

*I know this is rather messy — I’d love to know of a more succinct way to do this if possible!

Next, let’s sort that new array alphabetically:

let sortedList = uniqueSet.sort()
  .map((location, index) => <option key={index}>{location}</option>);

The default behavior of sort() is to sort alphabetically.

Our map function is taking the location and index of each of those now-sorted items and putting them into an array for our to use later. Notice we’re using the index as our key for React, and location as our text to display.

Inside our return statement is where we’re going to see all of this come to fruition on the user-side.

return (
      <div className="dropdown">
        <label htmlFor="location">Location</label>
        <input
          required
          className="form-input"
          type="text"
          name="location"
          placeholder="create or choose"
          value={this.props.location}
          onChange={event => this.handleTextFieldChange(event, 'location')}
          maxLength="20"
          autoComplete="off"
        />
        <select onChange={event => this.handleTextFieldChange(event, 'location')}>
          {sortedList}
        </select>
      </div>
    );

Here you can see that we are rendering to the page an and our . Our input is the text field used to create a new location, while our select is where we’re rendering all previously-enter location items.

Our select is receiving our sortedList array to be used as s — remember when we wrote that above?

If we scroll up in our imaginative document here, we need to write our onChange handler, handleTextFieldChange.

handleTextFieldChange(event) {
    let location = event.target.value;
    let text = location // capitalize first letter
      .toLowerCase()
      .split(' ')
      .map(s => s.charAt(0).toUpperCase() + s.substr(1))
      .join(' ');
    this.props.setLocation(text, 'location');
  }

event.target.value is either our input or our select. If we type into our input field, or if we select an option from the menu. We’re also manipulating all text that gets put into that input field; we’re capitalizing the first character. This helps to keep things looking tidy. Users might feel like capitalizing one day, or using all lowercase the next. This way, our stored data is uniform. You can read more about this in my previous post.

Then we finish off our component:

const mapStateToProps = state => ({
  moments: state.moments.moments,
  authToken: state.auth.authToken
});
export default connect(mapStateToProps)(LocationSelect);

and render it in our parent component after importing it.

I understand this is a rough how-to. As one with not a ton of experience with React and JavaScript, and having no one in-person to bounce ideas off of, I was left with reading docs and seeing what others have done. I never did find something doing this same thing, so I had to utilize what I could piece together. For instance, Set is very new to me, and I honestly don’t think I used it in the correct manner. That said, it’s what worked for what I needed.

I do hope this has helped someone, and I very much welcome any and all input. Below, you can find the component in its entirety:

import React from 'react';
import { connect } from 'react-redux';
import { getMoments } from '../actions/moments';

class LocationSelect extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      location: ''
    };
  }

componentDidMount() {
    if (this.props.authToken) {
      getMoments(this.props.authToken);
    }
  }

handleTextFieldChange(event) {
    let location = event.target.value;
    let text = location // capitalize first letter
      .toLowerCase()
      .split(' ')
      .map(s => s.charAt(0).toUpperCase() + s.substr(1))
      .join(' ');
    this.props.setLocation(text, 'location');
  }

render() {
    let momentsList = [];
    this.props.moments.forEach(({ id, location }) => momentsList.push({ id, location }));
    let uniqueSet = [...new Set(momentsList.map(moment => moment.location))];

// sort list alpha, map to render
    let sortedList = uniqueSet
      .sort((a, b) => {
        if (a < b) return -1;
        else if (a > b) return 1;
        return 0;
      })
      .map((location, index) => <option key={index}>{location}</option>);

// store locations to state
    return (
      <div className="dropdown">
        <label htmlFor="location">Location</label>
        <input
          required
          className="form-input"
          type="text"
          name="location"
          placeholder="create or choose"
          value={this.props.location}
          onChange={event => this.handleTextFieldChange(event, 'location')}
          maxLength="20"
          autoComplete="off"
        />
        <select onChange={event => this.handleTextFieldChange(event, 'location')}>
          {sortedList}
        </select>
      </div>
    );
  }
}


const mapStateToProps = state => ({
  moments: state.moments.moments,
  authToken: state.auth.authToken
});

export default connect(mapStateToProps)(LocationSelect);

Changelog
July 25, 2019

  • fix formatting errors carried over from Medium
  • update .sort() code block

Posted on by:

daviddoes profile

David Clark

@daviddoes

I am a Full Stack Web Developer in Seattle, WA. I live my life with an unquenchable thirst for knowledge and improvement, which is what drew me to working with software in the first place.

Discussion

markdown guide