This very much falls into the category of “things that must surely be easier”. So let me know in the comments if it is actually easier.
Here’s the thing, I’m tinkering with Vue and Laravel. I have a Vue component that represents a Laravel model object. I note that the component has a hierarchy of parent components.
In it’s simplest form it looks like this:
<template>
<div class="thing">
{{ thingData.title }}
</div>
</template>
<script>
export default {
props: [
'thingData'
]
}
</script>
Now, I want to add a delete button that will fire a request off to Laravel to delete the model and the redirect back to the model index.
Maybe it looks something like this:
<template>
<div class="thing">
{{ thingData.title }} ( <a href="...">Delete</a> )
</div>
</template>
<script>
export default {
props: [
'thingData'
]
}
</script>
And we need to work out what the HREF should be.
I COULD create a “GET” route for this. But let’s try and keep it RESTful/resourceful.
A delete request should really use the DELETE HTTP method.
So we need to add a little hidden form to make this work and submit that on click:
<template>
<div class="thing">
{{ thingData.title }} ( <a href="...">Delete</a> )
<form style="display: none;" :action="deleteUrl" method="delete">
</form>
</div>
</template>
<script>
export default {
props: [
'thingData'
]
}
</script>
OK. Getting there. Now I need to get the deleteUrl
into the action property of the form. But this is a Vue component, not something in Blade that I can use Laravel methods in; I can’t do something like {{ action('SomeController@destroy') }}
I confess, at this point, I took a bit of a shortcut and just coded this as a computed property with a hard-coded base URL and making use of the thing’s ID:
<template>
<div class="thing">
{{ thingData.title }} ( <a href="...">Delete</a> )
<form style="display: none;" :action="deleteUrl" method="delete">
</form>
</div>
</template>
<script>
export default {
props: [
'thingData'
],
computed: {
deleteUrl: function () {
return '/admin/things/' + this.thingData.id;
}
}
}
</script>
Hmm. This is not simple is it?
Now we need to trigger the form submission when the delete link is clicked. To do that we need to identify the components form element and submit it.
Let’s start with adding a method and calling that method on click:
<template>
<div class="thing">
{{ thingData.title }} ( <a href="..." @click="deleteThing">Delete</a> )
<form style="display: none;" :action="deleteUrl" method="delete">
</form>
</div>
</template>
<script>
export default {
props: [
'thingData'
],
computed: {
deleteUrl: function () {
return '/admin/things/' + this.thingData.id;
}
},
methods: {
deleteThing: function( ev ) {
ev.preventDefault();
// What now?
}
}
}
</script>
So…how do we track down the form element we need to submit? A bit of Googling shows that I can use this.$el
in JavaScript to get my component, and I can getElementsByTagName('form')
from there. So here’s the method:
methods: {
deleteThing: function ( event ) {
event.preventDefault();
this.$el.getElementsByTagName('form')[0].submit();
}
}
Now this will fail because we don’t have a CSRF token. Yes, Laravel kindly adds this as a header to all AJAX requests made using Axios. But with a straight form submission I’ll need to add the token. This is easy in Blade, you just add @csrf
and go and make yourself a celebratory cup of coffee.
But in a component?
Well, I can’t find any Laravel cleverness that somehow automatically gives you some kind of global token in Vue that you can use in any component.
So I COULD pass it down through my component hierarchy as props, but that seems messy.
A better way would be to have it as some kind of global state/prop that I can use.
One method for doing this is to add an Instance Property.
Laravel DOES give you a head start in having an HTML meta element in the <head>
that has the CSRF token in it. So that’s helpful.
We can the set a Vue instance property in our top-level app.js like so:
// Add the token
Vue.prototype.$token = document.head.querySelector('meta[name="csrf-token"]').content;
I could not then find a way to directly reference this in my components template. So I had to pull it into the components data
and then bind that to a new hidden input in the form:
<template>
<div class="thing">
{{ thingData.title }} ( <a href="..." @click="deleteThing">Delete</a> )
<form style="display: none;" :action="deleteUrl" method="delete">
<input type="hidden" name="_token" :value="token">
</form>
</div>
</template>
<script>
export default {
props: [
'thingData'
],
data: function () {
return {
token: this.$token
}
},
computed: {
deleteUrl: function () {
return '/admin/things/' + this.thingData.id;
}
},
methods: {
deleteThing: function( ev ) {
ev.preventDefault();
this.$el.getElementsByTagName('form')[0].submit();
}
}
}
</script>
Oh, and, hang on, one final thing…
method="delete"
on a form
rarely works, so you need to fake it with a post
request and another input.
Here’s the final code:
<template>
<div class="thing">
{{ thingData.title }} ( <a href="..." @click="deleteThing">Delete</a> )
<form style="display: none;" :action="deleteUrl" method="post">
<input type="hidden" name="_token" :value="token">
<input type="hidden" name="_method" value="DELETE">
</form>
</div>
</template>
<script>
export default {
props: [
'thingData'
],
data: function () {
return {
token: this.$token
}
},
computed: {
deleteUrl: function () {
return '/admin/things/' + this.thingData.id;
}
},
methods: {
deleteThing: function( ev ) {
ev.preventDefault();
this.$el.getElementsByTagName('form')[0].submit();
}
}
}
</script>
Phew!
Really…what did I miss here? This is SO complicated. There HAS to be a better way.