0

I've looked around for several days and cannot seem to solve this. I have a form that handles a file upload via an XMLHTTPRequestUpload, I have an onProgress callback that I would like to call and update visually what's happening in the UI, but I can't seem to make any successful function calls from inside "onProgress".

I tried to use console.log to find the scope in which my function updateProgress is defined. 'this' inside of the uploadFile, onProgress call is the XMLHTTP request itself. So I tried it for 'AddShow', the class itself. It doesn't report an error, and lists that updateProgress is a member function of the class, but when trying to call it, I still get

AddShow.updateProgress is not a function
    at XMLHttpRequestUpload.onProgress

So where's waldo? How do I call my function?

Here's my entire React Component Class:

export class AddShow extends React.Component {
  constructor(props) {
    super(props);

    this.handleTextChange = this.handleTextChange.bind(this);
    this.uploadFile = this.uploadFile.bind(this);
    this.updateProgress = this.updateProgress.bind(this);
  }

  // function that updates the state of the form input
  handleTextChange(evt) {
    this.props.dispatch(actions.changeShowTitleText(evt.target.value));
  }

  //function that show the progress of file upload
  updateProgress(progress) {
    progress = Math.floor(progress * 100);
    //call the dispatch actions to update redux state
    this.props.dispatch(actions.changeCompleteValue(progress));
    this.props.dispatch(actions.changeCompleteText(progress + '%'));
    this.props.dispatch(actions.changeCompleteARIA(progress + ' percent'));

    // this.props.complete = progress;
    // this.props.completeText = progress + '%';
    // this.props.ariaCompleteText = progress + ' percent';
  }

  // when 'add show' is pressed, validate form and upload
  onSubmit(e) {
    e.preventDefault();
    let titleText = this.props.showTitle;
    if (titleText < 1) {
      alert("Please provide a Show Title.");
    } else {
      // a title has been typed in, call upload with that info.
      this.uploadFile(titleText);
    }
  }

  //function that finally uploads the file given all the information
  uploadFile(title) {
    var uploader = new VimeoUpload({
      name: title,
      file: selectedFile,
      token: process.env.ACCESS_TOKEN,
      onProgress: function(data) {
        var percent = data.loaded / data.total;
        AddShow.updateProgress(percent);
      },
      onComplete: function(videoId, index) {
        var url = 'https://vimeo.com/' + videoId
      }
    });

    uploader.upload();
  }

  // set the global file variable if the input changes
  handleFileSelect(file) {
    console.log("These are the files retrieved", file.target.files[0]);
    selectedFile = file.target.files[0];
  }

  render() {
    var {dispatch, showTitle, complete, completeText, ariaCompleteText} = this.props;
    completeText = '0%';
    ariaCompleteText = "0 percent";

    return(
      <div className="row">
        <div className="column small-centered small-11 medium-8 large-6">
          <div className="container">
            <p>Drag the Video You Want to Upload to Screen</p>
            <form ref="form" onSubmit={this.onSubmit.bind(this)} className="add-show-form">
              <input type="file" placeholder="Select Video File" onChange={(evt) => this.handleFileSelect(evt)}/>
              <p ref="alertText"></p>
              <input type="text" value={this.props.showTitle} onChange={this.handleTextChange} placeholder="Show Title"/>
              <button className="button expanded">Add Show</button>
            </form>
            <div className="progress" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuetext={this.props.ariaCompleteText} aria-valuemax="100">
              <span className="progress-meter" style={{width: this.props.complete + '%'}}>
                <p className="progress-meter-text">{this.props.completeText}</p>
              </span>
            </div>
          </div>
        </div>
      </div>
    );
  }
};
Aaron Chamberlain
  • 653
  • 2
  • 10
  • 26
  • hmm Try this, change your anonymous method to an arrow function to bind the instance by using `onProgress: (data) => {` and change `AddShow.updateProgress(percent);` to `this.updateProgress(percent);` Then please comment on what error you get, if any. – Juan Jun 07 '17 at 21:58
  • `AddShow.updateProgress` cannot work because `updateProgress` is method of an **instance** of `AddShow`. – Felix Kling Jun 07 '17 at 22:37

3 Answers3

0

The easiest solution for you is to just use an arrow function which has lexical scoping and will be scoped to the context of uploadFile:

uploadFile(title) {
  var uploader = new VimeoUpload({
    // ...
    onProgress: (data) => { // <-- use arrow function
      var percent = data.loaded / data.total;
      this.updateProgress(percent);
    }
  // ...
}

Alternatively, you can save the context in a variable and use that:

uploadFile(title) {
  var context = this; // save context
  var uploader = new VimeoUpload({
    // ...
    onProgress: function(data) {
      var percent = data.loaded / data.total;
      context.updateProgress(percent); // use context
    }
  // ...
}

Do note that this line

AddShow.updateProgress(percent);

attempts to access updateProgress on the function itself, not the prototype, which doesn't have this method.

If you wanted to call the function in a static manner like this, you'd need to call it on the prototype.

nem035
  • 34,790
  • 6
  • 87
  • 99
0

Solution 1: Use static method

static updateProgress(progress) {
    progress = Math.floor(progress * 100);
    //call the dispatch actions to update redux state
    this.props.dispatch(actions.changeCompleteValue(progress));
    this.props.dispatch(actions.changeCompleteText(progress + '%'));
    this.props.dispatch(actions.changeCompleteARIA(progress + ' percent'));
}

Solution 2: Use arrow function in uploadFile method

uploadFile(title) {
  var uploader = new VimeoUpload({
    name: title,
    file: selectedFile,
    token: process.env.ACCESS_TOKEN,
    onProgress: function(data) => {
      var percent = data.loaded / data.total;
      // this now refers to your AddShow component
      this.updateProgress(percent);
    },
    onComplete: function(videoId, index) {
      var url = 'https://vimeo.com/' + videoId
    }
  });

  uploader.upload();
}

You cannot call addShow method directly from the Class as it is not a static method. To solve this issue I would suggest you either add a static keyword in your method so that you can invoke it from the the class directly without having to instantiate it or by using an arrow function in your uploadFile method.

Mμ.
  • 8,382
  • 3
  • 26
  • 36
0

You need to maintain a reference to the outer scope (i.e. the outer this) within the new scope of the onProgress callback. The traditional (ES5) way to approach this was to create a reference to it within a variable (typically named self), then use that to call the method within the callback:

  uploadFile(title) {
    // self now references the outer this
    var self = this;
    var uploader = new VimeoUpload({
      name: title,
      file: selectedFile,
      token: process.env.ACCESS_TOKEN,
      onProgress: function(data) {
        var percent = data.loaded / data.total;

        // we can access the outer scope on self here
        self.updateProgress(percent);
      },
      onComplete: function(videoId, index) {
        var url = 'https://vimeo.com/' + videoId
      }
    });

    uploader.upload();
  }

Given that you are using classes in your code, I would assume you are happy to use ES6 code, so the more elegant way to achieve the same effect is to use a lambda (or fat-arrow) function. This means declaring your functions with this syntax:

const someFunction = (anArgument) => {
    return aResult;
}

Which is (roughly) equivalent to:

function someFunction(anArgument) {
    return aResult;
}

The difference is that when you declare a function with lambda syntax, a new scope is not created. Effectively, the this inside the function is the same as the this outside it. We can then write the uploadFile handler as:

  uploadFile(title) {
    var uploader = new VimeoUpload({
      name: title,
      file: selectedFile,
      token: process.env.ACCESS_TOKEN,
      onProgress: (data) => {
        var percent = data.loaded / data.total;
        this.updateProgress(percent);
      },
      onComplete: (videoId, index) => {
        var url = 'https://vimeo.com/' + videoId
      }
    });

    uploader.upload();
  }
Alex Young
  • 4,009
  • 1
  • 16
  • 34
  • Thanks, this worked perfectly. I was translating from the older syntax to the new ES6 one to closer match the documentation and I guess I missed that. – Aaron Chamberlain Jun 07 '17 at 22:25
  • *"The difference is that when you declare a function with lambda syntax, a new scope is not created."* That's not correct. `this` is not the same as scope. Every function call creates a new scope. However in the case of an arrow function, the scope doesn't have its own `this` value. – Felix Kling Jun 07 '17 at 22:39