0

Following on from this question I am now looking at adding, deleting and updating a user's image on the front end using Vue.js and vue-resource. I have the delete functionality working no problem but I am having an issue with how I add and upddate a users' image.

Here is my test form code:

<form method="post" action="" enctype="multipart/form-data" accept-charset="UTF-8" @submit.prevent="submitForm">
  {{ getCsrfInput() }}
  <input type="hidden" name="action" value="users/saveUser">
  <input type="hidden" name="redirect" value="/">
  <input type="hidden" name="userId" value="{{ currentUser.id }}">

    <div class="control">
      <label for="avatar" class="label">Member Image</label>
      <input id="avatar" type="file" name="userPhoto">
    </div>

  <button type="submit" class="button is-primary">Save Profile</button>
</form>

Looking at the Craft log when you submit the form without Ajax it works successfully and this is the log:

$_POST=array (
  'CRAFT_CSRF_TOKEN' => 'Vq_qS8xUX_Z0d09MHA_n3lFLCse3hLuc5Vx0sREE|b95a7c106acaa1e421d409d2eb708ac2beb18b96',
  'action' => 'users/saveUser',
  'redirect' => 'members/profile/gareth
',
  'userId' => '39',
)

$_FILES=array (
  'userPhoto' => 
  array (
    'name' => 'new-me.jpg',
    'type' => 'image/jpeg',
    'tmp_name' => '/Applications/MAMP/tmp/php/phpfDqUNQ',
    'error' => 0,
    'size' => 395885,
  ),
)

My main question is how do I submit to the $_FILES array using ajax? When I post my form using ajax I get the following Craft log:

$_POST=array (
  'userPhoto' => 
  array (
    'name' => 'me.jpg',
    'type' => 'image/jpeg',
    'tmp_name' => '/Applications/MAMP/tmp/php/phpLha7Uh',
    'error' => '0',
    'size' => '117463',
  ),
  'deleteUserPhoto' => '',
  'userId' => '39',
  'action' => 'users/saveUser',
  'CRAFT_CSRF_TOKEN' => 'Vq_qS8xUX_Z0d09MHA_n3lFLCse3hLuc5Vx0sREE|b95a7c106acaa1e421d409d2eb708ac2beb18b96',
)

Is the only way to handle file uploads on the front end to create your own controller and work things out through that or can I set something within my ajax post that will add to the $_FILES array?

Here is my Vue.js code for review:

Vue.http.options.emulateJSON = true;
Vue.http.options.emulateHTTP = true;

var app = new Vue({
  delimiters: ['@{', '}'],
  el: '#app',
  data: {
    user: {
      userPhoto: {
        'name': 'me.jpg',
        'type': 'image/jpeg',
        'tmp_name': '/Applications/MAMP/tmp/php/phpLha7Uh',
        'error': 0,
        'size': 117463
      },
      userId: '{{ currentUser.id }}',
      action: 'users/saveUser'
    },
  },
  methods: {
    submitForm: function() {

      var data = this.user;

      data[window.csrfTokenName] = window.csrfTokenValue; // Append CSRF Token

      this.$http.post('/', data)
        .then(function(response) {
          // success stuff here
        });
    }

  }
});
0

In the end I found a solution which doesn't require creating my own controller. I am not sure it is possible to post directly to the $_FILES array in Craft but by using the native FormData object in Javascript you can basically pass all the form fields in the correct format to the vue-resource post method. I found this article useful on my search for solving what I needed. In doing this it kind of bypasses the Vue data model which is not exactly what I was after but it works well and keeps things simple. One caveat is that support for the FileData object is only on modern browsers but you can polyfill for older browsers.

Here is my final test form code:

<form method="post" action="" enctype="multipart/form-data" accept-charset="UTF-8" @submit.prevent="submitForm" id="imageForm">
  {{ getCsrfInput() }}
  <input type="hidden" name="action" value="users/saveUser">
  <input type="hidden" name="redirect" value="members/profile/{% include '_includes/helpers/user_profile_link' %}">
  <input type="hidden" name="userId" value="{{ currentUser.id }}">

  {% if currentUser.photoUrl %}
    <transition name="fade">
      <img src="{{ currentUser.photoUrl }}" alt="{{ currentUser.fullName }}" v-if="showImage">
    </transition>

    <a href="#" id="userPhoto" v-on:click.prevent="deletePhoto" v-if="showImage">Delete</a>

    <input type="hidden" name="deleteUserPhoto" id="deleteUserPhotoInput" value="" v-model="user.deleteUserPhoto">

    <div class="control">
      <label for="avatar" class="label">Member Image</label>
      <input id="avatar" type="file" name="userPhoto">
    </div>
  {% else %}
    <div class="control">
      <label for="avatar" class="label">Member Image</label>
      <input id="avatar" type="file" name="userPhoto">
    </div>
  {% endif %}

  <button type="submit" class="button is-primary">Save Profile</button>
</form>

Here is my final Vue.js code:

Vue.http.options.emulateJSON = true;
Vue.http.options.emulateHTTP = true;

var app = new Vue({
  delimiters: ['@{', '}'],
  el: '#app',
  data: {
    user: {
      deleteUserPhoto: '',
      userId: '{{ currentUser.id }}',
      action: 'users/saveUser'
    },
    showImage: true
  },
  methods: {
    submitForm: function() {
      var form = document.getElementById('imageForm');
      var formData = new FormData(form);

      this.$http.post('/', formData)
        .then(function(response) {
          //this.updated = true;
        });
    },
    deletePhoto: function () {
      this.user.deleteUserPhoto = 'noMorePhoto';

      var data = this.user;

      data[window.csrfTokenName] = window.csrfTokenValue; // Append CSRF Token

      this.$http.post('/', data)
        .then(function(response) {
          this.showImage = false;
        });
    }
  }
});

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.