Image Upload with Javascript Fetch using a WordPress REST API Endpoint

·

6 min read

First of all I want to credit this blog post for documenting the main points of this task, namely how to do an Ajax style upload (ie: without page reload)

attacomsian.com/blog/uploading-files-using-..

The Challenge

A custom made user profile page needed a profile image uploaded and stored with the other profile data.

The profile editing page is Svelte App and it interfaces with my custom made WordPress API endpoints.

WordPress has a pretty good file upload and management interface on its back-end but I could not find any kind of documentation or way to use it via API calls. I suspect that this does not exist.

So, I had to roll my own. The above mentioned post was an excellent starting point.

The HTML/JS part

First let me give the code and then I explain it. It's a Svelte component but you don't need to know much about Svelte to understand it.

<script lang="ts">

export let callBack= (url:string)=>{}
export let buttonText="Upload File"
export let userID=0

async function uploadIt(ev:Event){
  try {
    let file=(ev.target as any).files[0]

    //check file type and size requirements
    if(!['image/jpeg', 'image/gif', 'image/png'].includes(file.type)) {
      alert(`Only JPG, GIF or PNG images are accepted.`)
      return;
    }
    if(file.size>2097152){
      alert(`The file needs to be smaller. Less than 2MB.`)
      return;
    }

    const fd = new FormData()
    fd.append('img',file)
    fd.append('userID',String(userID))

    let res= await fetch("/wp-json/esis/v1/upload-image", { 
      method: "post", 
      body: fd,
    }).then(r => { if (!r.ok) { throw Error(r.statusText) } return r })  //checking for HTTP errors (not 2xx return code)
    .then(r => r.json())  

    if(res.ok){
      callBack(res.url)
    } else {
      alert(`Error uploading image: ${res.msg}`)
    }
  } catch(ev){
    console.log("uploadIt couldn't get the file info.")
  }

}

</script>
<div class="upload">
  <button>{buttonText}</button>
  <input type="file" id="profile-img" on:change={uploadIt}>
</div>

<style>
  .upload {
    position: relative;
    overflow: hidden;
  }
  input[type="file"] {
    position:absolute;
    opacity:0;
    z-index:99;
    top:0;
    left:0;
    width:100%;
    height:100%;
  }
</style>

The export let lines are just the declaration for the attributes for the component that it needs to receive.

The uploadIt function is bound to the change event on input element with the type="file". It receives the event object, from which we only need the .target to get the DOM reference to this element. From that the file upload data is extracted ( .files[0] ) which is an object with various information about the file such as file name, file size, file type.

This object is then added to a FormData object, along with the userID which I needed in my use case. The whole thing is then sent to the API endpoint which handles the upload and returns a URL for the uploaded file if everything is good. The callBack function is then called with that URL.

Customizing the File Upload Button

The <input type="file"> generates a very unattractive element with a the filename next to a button, and according to my search this behaviour can't be modified.

So, based on a tip on StackOverflow question, I created a button that is shown to the user. Arranging the input element using position:absolute, z-index and opacity:0, I made it into an invisible element which still takes the mouse clicks, thus activates the file selection box. This probably would need more work if accessibility was important.

That's is for the Javascript part.

The back-end API endpoint handler

I'm only showing the function part of the handler since my implementation for the WordPress API endpoints is using an usual pattern.

function (\WP_REST_Request $req) {
  $id=$req->get_params()['userID'];
  $img=$req->get_file_params()['img'];

  $url="wp-content/uploads/".$id."__".$img['name'];
  $fileLoc= ABSPATH.$url;

  if(copy($img['tmp_name'],$fileLoc)){
    return array("ok"=>true,"url"=>"/".$url);
  } else {
    return array("ok"=>false,"msg"=>"File Copy Error:".error_get_last());
  }
}

Here the two parameters are extracted. They need to use two different methods (get_params and get_file_params because they are two different types of parameters)

The actual file uploaded by the server will be in the path specified in thetmp_name attribute of the file parameter. This just need to be copied to the location where we actually want it (I guess move/rename should be okay also).

The rest is just error handling and returning the result for the JS code.

Conclusion

The procedure is not complicated but it took me quite a while to round up the pieces and some things I had to figure out by trial and error. So I thought I'd document it here, for my own reference and to help others.