Jeb50
Jeb50

Reputation: 7067

How to async all uploaded files before last step?

I want users upload multiple image files, app reads all of them and push into an array, when all finish then lastStep()

<input type="file" (change) = "fileChangedEvent($event);" multiple/>

fileChangedEvent(event: any): void
{
    for (let f of event.target.files)
    {
        var reader = new FileReader();
        reader.readAsDataURL(f);
        reader.onload= (event) => {this.arrayThumbnail.push(event.target.result);}; 
    } 
    // when all done
    lastStep();  // work on arrayThumbnail[]
}

But lastStep() always run before for ... of loop, resulting it works on an empty arrayThumbnail[]. Following async/await produces the same result:

fileChangedEvent(event: any): void
{
    async() => {
    for (let f of event.target.files)
        {
            var reader = new FileReader();
            await reader.readAsDataURL(f);
            await reader.onload= (event) => {this.arrayThumbnail.push(event.target.result);}; 
        } 
        // when all done
        lastStep();  // work on arrayThumbnail[]
    }
}
    

Explicit Promise doesn't work either:

myPromise(fs: any): Promise<number>
{
    var bogus = new Promise<number>((resolve, reject) =>
    {
        for (let f of fs)
        {
            var reader = new FileReader();
            reader.readAsDataURL(f);
            reader.onload= (event) => {this.arrayThumbnail.push(event.target.result);};    
        };
        resolve(1);
    })
    return bogus;
}

fileChangedEvent(event: any): void
{
    this.myPromise(event.target.files).then(
    x=>
    {
        lastStep();
    });
}

Upvotes: 0

Views: 827

Answers (1)

Mulan
Mulan

Reputation: 135387

You have the right idea to use promises, you just need to hook things up a little differently. Run the snippet below and select two small files to test it out!

// onChange listener
function onChange(event) {
  const promises = []
  for (const file of event.target.files)
    promises.push(readFile(file))
  Promise.all(promises).then(lastStep)
}

// read one file
function readFile(f) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader()
    reader.readAsDataURL(f)
    reader.onload = event => resolve(event.target.result)
    reader.onerror = reject
  })
}

// example last step
function lastStep(data) {
  console.log("last step")
  console.log(`uploaded ${data.length} files`)
  for (const blob of data) {
    const pre = document.createElement("pre")
    pre.textContent = blob
    document.body.appendChild(pre)
  }
}

document.forms.myapp.files.addEventListener("change", onChange)
<form id="myapp">
  <input type="file" name="files" multiple>
</form>

Once you've wrapped your head around that, know that we can rewrite onChange even easier -

// onChange listener simplified
function onChange(event) {
  Promise.all(Array.from(event.target.files, readFile)).then(lastStep)
}

And now there's really no reason to separate onChange and lastStep. Using async and await we can very easily combine the two into one -

// onChange listener
async function onChange(event) {
  const data = await Promise.all(Array.from(event.target.files, readFile))
  console.log(`uploaded ${data.length} files`)
  for (const blob of data) {
    const pre = document.createElement("pre")
    pre.textContent = blob
    document.body.appendChild(pre)
  }
}

// read one file
function readFile(f) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader()
    reader.readAsDataURL(f)
    reader.onload = event => resolve(event.target.result)
    reader.onerror = reject
  })
}

document.forms.myapp.files.addEventListener("change", onChange)
<form id="myapp">
  <input type="file" name="files" multiple>
</form>

without promises

"2nd thought, what we're doing here is each file has one Promise, then wait till all settled. Is it possible to do one promise for all files?"

The benefit of Promises is they are light-weight and composable, meaning you can build larger async computations out of smaller ones. Composing callbacks is much more challenging and therefore easier to make common mistakes. We could wrap all of this in a single promise if we wanted to, or skip promises altogether -

// onChange listener
function onChange(event) {
  const data = []
  const noEror = true

  // for each file, f
  for (const f of event.target.files) {

    // readFile with callback
    readFile(f, (err, result) => {

      // handle errors
      // only call error handler a maximum of one time
      if (err && noError) {
        noError = false
        return handleError(err)
      }
      // add individual result to data
      data.push(result) 
      
      // once data.length is equal to number of input files
      // then proceed to the last step
      if (data.length == event.target.files.length)
        return lastStep(data)
    })
  }
}

// read one file
function readFile(f, callback) {
  const reader = new FileReader()
  reader.readAsDataURL(f)
  reader.onload = event => callback(null, event.target.result)
  reader.onerror = err => callback(err)
}

function handleError (err) { ... }
function lastStep (data) { ... }

As you can see, that's a lot to write for what should be a simple procedure. And all of this work would be duplicated each time we needed similar functionality. To get around this, we could write a generic asyncEach utility that can be used whenever we need to iterate over an array using an asynchronous operation f, and specify a final callback for when all operations are complete -

// asyncEach generic utility
function asyncEach(arr, f, callback, data = []) {
  if (arr.length == 0)
    callback(null, data)
  else
    f(arr[0], (err, result) =>
      err
        ? callback(err)
        : asyncEach(arr.slice(1), f, callback, [...data, result]) 
    )
}

// read one file
function readFile(f, callback) {
  const reader = new FileReader()
  reader.readAsDataURL(f)
  reader.onload = event => callback(null, event.target.result)
  reader.onerror = err => callback(err)
}

// onChange listener
function onChange(event) {
  asyncEach(Array.from(event.target.files), readFile, (err, data) => {
    if (err) return console.error(err)
    console.log(`uploaded ${data.length} files`)
    for (const blob of data) {
      const pre = document.createElement("pre")
      pre.textContent = blob
      document.body.appendChild(pre)
    }
  })
}

document.forms.myapp.files.addEventListener("change", onChange)
<form id="myapp">
  <input type="file" name="files" multiple>
</form>

I think this is a wonderful exercise but most people wouldn't attempt writing their own asyncEach and the common need for this type of thing was the basis of popular libraries like async. However because of the widespread success of Promise and new async-await syntax, there is little need for these old and cumbersome callback patterns.

Upvotes: 1

Related Questions