0

I am trying to use the 'Stepper' react material-ui component, but I am having difficulty using it in a class fashion, rather than function as they have in their previews.

Here is what I have so far, and it does load but with some problems:

  1. The text that appears is 'unknown step' meaning that the function 'getStepContent' does not gets called properly
  2. Every time I am hitting the 'next' button, it gives me an error saying: 'Cannot read property 'has' of undefined' seems like almost all of my function calls are messed up..

Here is my code:

import React, { Component } from "react";
import "./CharacterCreate.css";
import PropTypes from 'prop-types';
import Tabs from '@material-ui/core/Tabs';
import Tab from '@material-ui/core/Tab';
import Typography from '@material-ui/core/Typography';
import Box from '@material-ui/core/Box';

import { makeStyles } from '@material-ui/core/styles';
import Stepper from '@material-ui/core/Stepper';
import Step from '@material-ui/core/Step';
import StepLabel from '@material-ui/core/StepLabel';
import Button from '@material-ui/core/Button';


export default class CharacterCreate extends Component {

  constructor(props) {
    super(props);
    this.state = {
      activeStep: 0,
      skipped :new Set()
    };
    this.handleNext = this.handleNext.bind(this);
    this.isStepSkipped = this.isStepSkipped.bind(this);
  }

  getSteps() {
    return ['Select campaign settings', 'Create an ad group', 'Create an ad'];
  }

  getStepContent(step) {
    switch (step) {
      case 0:
        return 'Select campaign settings...';
      case 1:
        return 'What is an ad group anyways?';
      case 2:
        return 'This is the bit I really care about!';
      default:
        return 'Unknown step';
    }
  }

  isStepOptional(step) {
    return step === 1;
  }

  isStepSkipped(step) {
    return this.state.skipped.has(step);
  }

  handleNext() {
    let newSkipped = this.skipped;
    if (this.isStepSkipped(this.activeStep)) {
      newSkipped = new Set(newSkipped.values());
      newSkipped.delete(this.activeStep);
    }

    this.setState({activeStep: prevActiveStep => prevActiveStep + 1})
    this.setState({skipped: this.skipped});
  }

  handleBack() {
    this.setState({activeStep: prevActiveStep => prevActiveStep - 1})
  }

  handleSkip() {
    if (!this.isStepOptional(this.activeStep)) {
      // You probably want to guard against something like this,
      // it should never occur unless someone's actively trying to break something.
      throw new Error("You can't skip a step that isn't optional.");
    }

    this.setState({activeStep: prevActiveStep => prevActiveStep + 1})
    this.setSkipped(prevSkipped => {
      const newSkipped = new Set(prevSkipped.values());
      newSkipped.add(this.activeStep);
      return newSkipped;
    });
  }

  handleReset() {
    this.setState({activeStep: 0})
  }


render() {

  const steps = this.getSteps();

  return (
    <div className="root">
      <Stepper activeStep={this.activeStep}>
        {steps.map((label, index) => {
          const stepProps = {};
          const labelProps = {};
          if (this.isStepOptional(index)) {
            labelProps.optional = <Typography variant="caption">Optional</Typography>;
          }
          if (this.isStepSkipped(index)) {
            stepProps.completed = false;
          }
          return (
            <Step key={label} {...stepProps}>
              <StepLabel {...labelProps}>{label}</StepLabel>
            </Step>
          );
        })}
      </Stepper>
      <div>
        {this.activeStep === steps.length ? (
          <div>
            <Typography className="instructions">
              All steps completed - you&apos;re finished
            </Typography>
            <Button onClick={this.handleReset} className="button">
              Reset
            </Button>
          </div>
        ) : (
          <div>
            <Typography className="instructions">{this.getStepContent(this.activeStep)}</Typography>
            <div>
              <Button disabled={this.activeStep === 0} onClick={this.handleBack} className="button">
                Back
              </Button>
              {this.isStepOptional(this.activeStep) && (
                <Button
                  variant="contained"
                  color="primary"
                  onClick={this.handleSkip}
                  className="button"
                >
                  Skip
                </Button>
              )}

              <Button
                variant="contained"
                color="primary"
                onClick={this.handleNext}
                className="button"
              >
                {this.activeStep === steps.length - 1 ? 'Finish' : 'Next'}
              </Button>
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

}

I know it's a lot, but I'm simply trying to use the same example code from material-ui website as a class instead of a function..

Thank you for your help!

1 Answers1

0

I think you're wiping out this.state.skipped here, since this.skipped doesn't appear to be declared anywhere.

this.setState({skipped: this.skipped});

After this call, this.state.skipped is undefined, so calling this.state.skipped.has(...) blows up.

I suspect you meant to use this.state.skipped.


Another source of trouble might be a scope issue that arises from the way your click handlers are declared and attached, e.g. onClick={this.handleNext}.

TLDR: Try onClick={() => this.handleNext()} instead.

In javascript the scope (what this refers to) inside a method call is generally set to the object on which it was called.

So if you call this.handleNext(), references to this inside handleNext will be your component, as you expect.

However, if instead you do:

const {handleNext} = this;
handleNext();

The this reference may not be what you expect, because the method wasn't invoked as a method on your component. It was invoked as a standalone function, detached from your component. And this is effectively what happens when you pass an event handler down to another component. Inside the child component (the button, for example), your handler is just a function passed in as a prop, detached from your component:

// inside the button component
const {onClick} = this.props;
onClick(); // no scope; detached from your component

There are several ways to fix this, but the two most straightforward are:

  1. Declare a new function that invokes the handler on the component:
onClick={ () => this.handleNext() }
  1. Make your handler an arrow function, because arrow functions automatically adopt the parent scope where they're declared. So instead of this:
handleNext() {
  let newSkipped = this.skipped;
  ...

Do this:

handleNext = () => {
  let newSkipped = this.skipped;

Hope this helps. Sorry it's so long. Give it a shot and let me know.


Side note: you can do both of these in a single call:

this.setState({activeStep: prevActiveStep => prevActiveStep + 1})
this.setState({skipped: this.skipped});
this.setState({
  activeStep: prevActiveStep => prevActiveStep + 1,
  skipped: this.state.skipped
})
ray
  • 26,557
  • 5
  • 28
  • 27
  • Thank you so much! It did fix the issue that I had with the error showing, but now it simply won't do anything... But thank you again for helping me in the right direction! –  Sep 20 '19 at 05:27
  • Updated my answer with a long ramble about scope that might be a source of some of your issues. – ray Sep 20 '19 at 05:49
  • Thank you for explaining it all! It makes a lot more sense now after your detailed explanation. I tried what you said but unfortunately it didn't work. However! when I tried to debug it with console logs, it seems as if the 'this' does not get passed correctly into the 'handleNext' function, because it seems that its properties are undefined. –  Sep 20 '19 at 05:59
  • Fixed it! my issue was this line: ```prevActiveStep => prevActiveStep + 1 ``` inside the setState function. I simply had to change it to : ```this.state.activeStep+1``` and it is now working perfectly! Thanks! –  Sep 20 '19 at 06:24