i dont know if this is possible - but i am working on a Vue app with multiple input fields that posts to the same list - and i need this to be stored somehow, so when you refresh the site, the outputs from the input fields are saved.
This means both the taskList and subTaskList array should be saved ( i know i've only worked on taskList ).
The example i posted here saves the data fine, however if you refresh, it will post all the data in all the components, can this be fixed so it will only be in the right components?
const STORAGE_KEY = 'madplan-storage'
Vue.component('list-component', {
data: function() {
return {
newTask: "",
taskList: [],
newSubTask: "",
subTaskList: [],
};
},
created() {
this.taskList = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
},
template:
'<div>' +
'<section class="prefetch">' +
'<input v-if="showInput" class="input typeahead" type="text" placeholder="Tilføj ret til madplanen" v-model="newTask" v-on:keyup.enter="addTask">' +
'</section>' +
'<details open v-for="task in taskList" v-bind:key="task.text" class="sub-list-item">' +
'<summary>{{ task.text }}<i class="fa fa-times" aria-hidden="true" v-on:click="removeTask(task)"></i>' + '</summary>' +
'<input class="subInput" type="text" placeholder="Tilføj til indøbsseddel" v-model="newSubTask" v-on:keyup.enter="addSubTask">' +
'</details>' +
'</div>',
computed: {
showInput: function() {
return !this.taskList.length
},
},
methods: {
//addTasks
//
addTask: function() {
var task = this.newTask.trim();
if (task) {
this.taskList.push({
text: task
});
this.newTask = "";
localStorage.setItem(STORAGE_KEY, JSON.stringify(this.taskList));
}
},
addSubTask: function() {
var task = this.newSubTask.trim();
if (task) {
this.subTaskList.push({
text: task
});
this.newSubTask = "";
this.$emit('addedtask', task);
localStorage.setItem(STORAGE_KEY, JSON.stringify(this.subTaskList));
}
},
//removeTasks
//
removeTask: function(task) {
var index = this.taskList.indexOf(task);
this.taskList.splice(index, 1);
},
},
});
new Vue({
el: "#madplan",
data: {
newTask: "",
taskList: [],
newSubTask: "",
subTaskList: [],
},
methods: {
acknowledgeAddedTask: function(cls, task) {
this.$data.subTaskList.push({
text: task,
class: "list-item " + cls
})
},
acknowledgeRemovedTask: function(task) {
this.$data.subTaskList = this.$data.subTaskList.filter(it => it.text != task.text)
},
removeSubTask: function(task) {
var index = this.subTaskList.indexOf(task);
this.subTaskList.splice(index, 1);
},
}
});
<section id="madplan" class="section-wrapper">
<section class="check-list">
<div id="mandag" class="dayWrapper">
<h1>Day One</h1>
<list-component
class="mandag"
v-on:addedtask='task => acknowledgeAddedTask("mandag", task)'
></list-component>
</div>
<div id="tirsdag" class="dayWrapper">
<h1>Day Two</h1>
<list-component
class="tirsdag"
v-on:addedtask='task => acknowledgeAddedTask("tirsdag", task)'
></list-component>
</div>
<ul id="indkobsseddel">
<h2>Shopping List</h2>
<li v-for="task in subTaskList" v-bind:key="task.text" :class="task.class">{{ task.text }}<i class="fa fa-times" aria-hidden="true" v-on:click="removeSubTask(task)"></i></li>
</ul>
</section>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script src="https://unpkg.com/vue/dist/vue.js" charset="utf-8"></script>
To be clear, i will come with an example:
As it is now, if i post "Foo" and "Bar" to the "Day One" component and refresh the page, it will then post "Foo" and "Bar" to both "Day One" and "Day Two".
Essentially, i would like to be able to post for an example "Foo" to "Day One", "Bar" to "Day Two" and from there post "Hello World" to the Shopping List, and it would all be saved in the right places, instead of posting everything everywhere.
BTW: I'm a scrub at backend work.
To persist global state, you may use the plugin vue-persistent-state like this:
import persistentStorage from 'vue-persistent-storage';
const initialState = {
newTask: "",
taskList: [],
newSubTask: "",
subTaskList: [],
};
Vue.use(persistentStorage, initialState);
Now newTask, taskList, newSubTask and subTaskList is available as data in all components and Vue instances. Any changes will be stored in localStorage, and you can use this.taskList etc. as you would in a vanilla Vue app.
Your list component now becomes:
Vue.component('list-component', {
// data removed
// created removed
template:
'<div>' +
'<section class="prefetch">' +
'<input v-if="showInput" class="input typeahead" type="text" placeholder="Tilføj ret til madplanen" v-model="newTask" v-on:keyup.enter="addTask">' +
'</section>' +
'<details open v-for="task in taskList" v-bind:key="task.text" class="sub-list-item">' +
'<summary>{{ task.text }}<i class="fa fa-times" aria-hidden="true" v-on:click="removeTask(task)"></i>' + '</summary>' +
'<input class="subInput" type="text" placeholder="Tilføj til indøbsseddel" v-model="newSubTask" v-on:keyup.enter="addSubTask">' +
'</details>' +
'</div>',
computed: {
showInput: function() {
return !this.taskList.length
},
},
methods: {
//addTasks
//
addTask: function() {
var task = this.newTask.trim();
if (task) {
this.taskList.push({
text: task
});
this.newTask = "";
// localStorage.setItem not needed
}
},
addSubTask: function() {
var task = this.newSubTask.trim();
if (task) {
this.subTaskList.push({
text: task
});
this.newSubTask = "";
// $emit not needed, state is global and shared
// localStorage.setItem not needed
}
},
//removeTasks
//
removeTask: function(task) {
var index = this.taskList.indexOf(task);
this.taskList.splice(index, 1);
},
},
});
If you want to understand how this works, the code is pretty simple. It basically
adds a mixin to make initialState available in all Vue instances, and
watches for changes and stores them.
Disclaimer: I'm the author of vue-persistent-state.
Related
I'm starting to use Vuejs and I'm having difficulty to do something that I think simple.
To summarize, I have a list of filters that contains all the information about the filters (name, items available for selection, witch item selected and if the filter is selected).
In my page, I have a table that lists all the filters selected and drop-down list with the rest of filters not selected.
To be clean, I have two methods in the computed section:
filtersSelected (return all the filter with "isSelected" is true)
filtersNoSelected (return all the filter with "isSelected" is false)
Vue.component('filter-item', {
props: ['filter'],
template:
'<tr>' +
'<th scope="row">{{ filter.id }}</th>' +
'<td>' +
'{{ filter.name }}' +
'</td>' +
'<td>' +
'<button type="button" class="btn btn-sm btn-info">></button>' +
'</td>' +
'<td>' +
'<button type="button" class="btn btn-sm btn-danger" v-on:click="$emit(\'remove\', filter)">X</button>' +
'</td>' +
'</tr>'
})
Vue.component('filter-panel', {
props: ['allfilters'],
data: function () {
return {
filters: this.allfilters
}
},
methods: {
removeFilter: function (filter) {
filter.isSelected = false;
}
},
template:
'<table class="table table-sm">' +
'<thead>' +
'<tr>' +
'<th scope="col">#</th>' +
'<th scope="col">Name</th>' +
'<th scope="col">Update</th>' +
'<th scope="col">Delete</th>' +
'</tr>' +
'</thead>' +
'<tbody>' +
'<filter-item v-for="filter in filters" v-bind:key="filter.id" v-bind:filter="filter" #remove="removeFilter">' +
'</filter-item>' +
'</tbody>' +
'</table>'
})
Vue.component('filter-select', {
props: ['allfilters'],
data: function () {
return {
selectedItem: 0,
filters: this.allfilters
}
},
methods: {
},
template:
'<div><select class="selectpicker" data-live-search="true" v-model="selectedItem" v-on:change="$emit(\'selected\', selectedItem)"> ' +
'<option disabled value="0" selected>Select</option>' +
'<option v-for="filter in filters" v-bind:key="filter.id" v-bind:value="filter.id">{{ filter.name }}' +
'</option>' +
'</select>' +
'<span>{{selectedItem}}</span></div>'
})
var app = new Vue({
el: '#app',
data: {
filters:
[
{
id: 1,
name: "Country",
type: "List",
isSelected: false,
listItem: [
{
value: "1",
text: "France"
},
{
value: "2",
text: "United States"
},
{
value: "3",
text: "China"
}
],
selectedItem: ["1", "3"]
},
{
id: 2,
name: "Product category",
type: "List",
isSelected: false,
listItem: [
{
value: "1",
text: "Food"
},
{
value: "2",
text: "Drink"
},
{
value: "3",
text: "Home"
}
],
selectedItem: ["1"]
}
],
filterSelected: 2
},
methods:
{
AddFilter: function (filterId) {
var filter = this.filters.find(function (f) { return f.id === filterId });
filter.isSelected = true;
$('#FilterModal').modal('hide');
}
},
computed:
{
filtersSelected: function () {
var list = this.filters.filter(function (f) { return f.isSelected });
return list;
},
filtersNoSelected: function () {
var list = this.filters.filter(function (f) { return !f.isSelected });
return list;
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<filter-panel v-bind:allfilters="filtersSelected"></filter-panel>
<filter-select v-bind:allfilters="filtersNoSelected" v-on:selected="filterSelected = $event"></filter-select>
<button type="button" class="btn btn-primary" v-on:click="AddFilter(filterSelected)">Add the filter</button>
So the logic is simple, when we select a filter and click on the add button, the method will put "isSelected" to true. If we click on delete, the method will put "isSelected" to false. And after, the computed methods will share to my components the filtered list. But it's doesn't work.
I checked the logic step by step in the browser, we pass by the computed methods, the computed methods send a different list each time, but i have no change in my page.
I tried another solution, share all the list to my components and add "v-if" or "v-show" on and but it worked only for the table, not for the drop-down list. beside, I don't like this solution.
With all the posts I read on this forum, I have not found a solution to my problem.
(use key in v-for or use reactivity)
Because I'm in the learning phase, I want to know what is the good way to do it. I want to use the best practice !
Thank you in advance for your helps !
I have ran into similar type of issue. This may be helpful in your case also. When you use arrays or objects in data and if you change the data inside that array/object then Vue cannot recognise it.
Vue can identify if you push some data in the array and re-render the component, but it cannot do that if some nested data changes.
We use lodash.cloneDeep() in our project to overcome this issue.
The mutations should be performed on new copy, can you try something like this:
AddFilter: function (filterId) {
var index = this.filters.findIndex(function (f) { return f.id === filterId });
let newFilters = _.cloneDeep(this.filters);
newFilters[index].isSelected = true;
this.filters = newFilters;
$('#FilterModal').modal('hide');
}
This concept is similar to state in react, that you should not mutate
the state directly(its similar not exactly the same)
You can read about it in Official docs, which suggests that
The object must be plain
My page has a Select All checkbox at the top where upon clicking it, it should have checked all the checkboxes. Here's my code:
<div class="columns bottom-border">
<div class="column">Student</div>
<div><a v-on:click="revokePoints()">Revoke</a><br/><input type="checkbox" v-model="selectAll">Select All</div>
</div>
<div class="columns" v-for="(behavior) in sortBehaviors(behaviorList)" :key="behavior._id">
<div class="column">{{ behavior.studentID.firstName }} </div>
<div class="column is-1"><center><input type="checkbox" :value="setCheckedValue(behavior.dataType,behavior._id,behavior.studentID._id,behavior.actionDate)" :id="setCheckedValue(behavior.dataType,behavior._id,behavior.studentID._id,behavior.actionDate)" v-model="checkedIDs"></center></div>
</div>
data() {
return {
positiveName: '',
behaviorList: [],
checkedIDs: [],
selected: []
};
},
computed:{
selectAll: {
get: function () {
return this.behaviorList ? this.selected.length == this.behaviorList.length : false;
},
set: function (value) {
var mySelected = [];
let self = this;
if (value) {
this.behaviorList.forEach(function (behavior) {
var getDataType = behavior.dataType
var getID = behavior._id
var getStudentID = behavior.studentID._id
var getActionDate = behavior.actionDate
var getGeneratedID = self.setCheckedValue(getDataType,getID,getStudentID,getActionDate);
mySelected.push(getGeneratedID);
});
}
self.selected = mySelected;
console.log("self selected")
console.log(self.selected)
}
}
},
methods: {
setCheckedValue(dataType,id,studentID,actionDate){
return "1:" + dataType + "|2:" + id + "|3:" + studentID + "|4:" + actionDate
},
revokePoints(){
var pointsToRevoke = this.checkedIDs;
console.log("pointsToRevoke")
console.log(pointsToRevoke)
}
When I click on the Select All checkbox, console will display that self.selected will have the id of all the checkboxes. But the issue is the checkbox for all the values displayed are not checked...
It is difficult to help because your code is not completed. But I would approach that a bit differently. I hope this codepen can help you.
const list = [
{ id: 1, name: 'New York', checked: true },
{ id: 2, name: 'Sydney', checked: false },
{ id: 3, name: 'London', checked: false },
{ id: 4, name: 'Chicago', checked: true }
]
new Vue({
el: '#app',
data() {
return {
list,
isAllChecked: false
};
},
methods: {
checkAll: function() {
this.list = this.list.map(city => ({ ...city,
checked: !this.isAllChecked
}))
this.isAllChecked = !this.isAllChecked
}
},
computed: {
getAllCheckedIDs: function() {
return this.list.filter(city => city.checked).map(city => city.id)
},
getNotAllCheckedIDs: function() {
return this.list.filter(city => !city.checked).map(city => city.id)
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<ul>
<li v-for="city in list" :key="city.id">
<label>
{{city.name}}
<input type="checkbox" v-model="city.checked" />
</label>
</li>
</ul>
<button #click="checkAll">Check all</button>
<br/>
<div>Checked IDs: {{getAllCheckedIDs}}</div>
<div>Not Checked IDs: {{getNotAllCheckedIDs}}</div>
</div>
Struggling to sort out how to get a selected value from a Typeahead component to pass back to the parent component. I'm allowing the user to search from a variety of data to link a record to a post. Once the user clicks one of the typeahead drop-down records, I pass the item to the sendlink method - I've checked that the data passes ok. When I do the emit using the selected-link event, I'm not getting the data in the parent component.
PostList.vue
<template>
<div>
<div v-if='posts.length === 0' class="header">There are no posts yet!</div>
<form action="#" #submit.prevent="createPost()" class="publisher bt-1 border-fade bg-white" autocomplete="off">
<div class="input-group">
<input v-model="post.content" type="text" name="content" class="form-control publisher-input" placeholder="What's the lastest?" autofocus>
<span class="input-group-btn">
<button type="submit" class="btn btn-primary">Post</button>
</span>
</div>
<span class="publisher-btn file-group">
<i class="fa fa-camera file-browser"></i>
<input type="file">
</span>
</form>
<div #click="doit" v-on:selected-link="onSelectedLink">{{ modellink.name }}</div>
<typeahead
source="/api/typeahead"
placeholder="Link Post to Trip, Business, etc"
filter-key="title"
:start-at="3">
</typeahead>
<post v-for="post in posts"
:key="post.id"
:post="post"
#post-deleted="deletePost($event)">
</post>
</div>
</template>
<script>
var axios = require("axios");
import post from './PostItem.vue';
import typeahead from './Typeahead.vue';
export default {
components: {
post,
typeahead
},
props: ['postableId', 'postableType', 'model'],
data: function() {
return {
modellink: {
"name": "n/a",
"description": "",
"id": null,
"model": "n/a"
},
post: {
id: 1,
content: "",
edited: false,
created_at: new Date().toLocaleString(),
user: {
id: 1,
name: '',
}
},
posts: [
{
id: 1,
content: "",
edited: false,
created_at: new Date().toLocaleString(),
user: {
id: 1,
name: '',
}
}
]
};
},
created() {
this.fetchPostsList();
},
methods: {
onSelectedLink: function (talink) {
alert(JSON.stringify(talink, null, 4));
this.link = talink
},
doit() {
alert(JSON.stringify(this.modellink, null, 4));
},
fetchPostsList() {
if( this.postableId ) {
axios.get('/api/' + this.postableType + '/' + this.postableId + '/posts').then((res) => {
this.posts = res.data;
});
} else {
axios.get('/api/post').then((res) => {
//alert(JSON.stringify(res.data[0], null, 4));
this.posts = res.data;
});
}
},
createPost() {
axios.post('api/post', {content: this.post.content, user_id: Laravel.userId, vessel_id: Laravel.vesselId })
.then((res) => {
this.post.content = '';
// this.post.user_id = Laravel.userId;
// this.task.statuscolor = '#ff0000';
this.edit = false;
this.fetchPostsList();
})
.catch((err) => console.error(err));
},
deletePost(post) {
axios.delete('api/post/' + post.id)
.then((res) => {
this.fetchPostsList()
})
.catch((err) => console.error(err));
},
}
}
</script>
Typeahead.vue
<template>
<div>
<input
v-model="query"
#blur="reset"
type="text"
class="SearchInput"
:placeholder="placeholder">
<transition-group name="fade" tag="ul" class="Results">
<li v-for="item in items" :key="item.id">
<span #click="sendlink(item)">
<strong>{{ item.name }}</strong> - <small>{{ item.model }}</small><br>
<small>{{ item.description }}</small>
</span>
</li>
</transition-group>
<p v-show="isEmpty">Sorry, but we can't find any match for given term :( </p>
</div>
</template>
<script>
var axios = require("axios");
export default {
name: 'Typeahead',
props: {
modellink: {
type: Object,
required: false
},
source: {
type: [String, Array],
required: true
},
filterKey: {
type: String,
required: true
},
startAt: {
type: Number,
default: 3
},
placeholder: {
type: String,
default: ''
}
},
data() {
return {
items: [],
query: '',
taitem: ''
}
},
computed: {
lookup() {
if(this.query.length >= this.startAt) {
axios.get(this.source + '/' + this.query).then((res) => {
this.items = res.data;
return res.data;
});
}
},
isEmpty() {
if( typeof this.lookup === 'undefined' ) {
return false
} else {
return this.lookup.length < 1
}
}
},
methods: {
sendlink: function (taitem) {
this.$emit('selected-link', taitem);
},
reset() {
this.query = ''
}
}
}
</script>
In your PostList.vue, move the v-on:selected-link="onSelectedLink" from the div to typeahead like below. When emitting an event from child to parent, the listener on the parent needs to be on the child component tag for it to work.
<div #click="doit">{{ modellink.name }}</div>
<typeahead
source="/api/typeahead"
placeholder="Link Post to Trip, Business, etc"
filter-key="title"
:start-at="3"
v-on:selected-link="onSelectedLink">
</typeahead>
I have a parent component making an Ajax request using Axios, The response is then assigned to a variabled called 'carousel' and is then passed down to the child component.
In the child component on 'created()' I am assigning the passed prop 'carousel' to a new variable called 'slides'
Problem is when I do this is returns undefined and my thinking is the Axios query hasn't returned before this happens.
Is there a way to delay the axios request before the prop is passed and the child component always gets the expected response.
My code is below.
Parent
<template>
<div class='product-container'>
<home-carousel :carousel="carousel"></home-carousel>
<profiler></profiler>
<cta-sections :panels="panels"></cta-sections>
</div>
</template>
<script>
import api from '../api/Home'
import CtaSections from '../components/CtaSections'
import HomeCarousel from '../components/HomeCarousel'
import Profiler from '../components/Profiler'
export default {
components: {
CtaSections,
HomeCarousel,
Profiler,
},
data() {
return {
panels: [],
slides: 'test',
carouselPass: [],
carousel: [],
}
},
created() {
axios.get(window.SETTINGS.API_BASE_PATH + 'pages/5')
.then(response => {
this.panels = response.data.acf.split_panels;
this.carousel = response.data.acf.carousel;
this.carousel.forEach(function (item, index) {
if (index === 0) {
item.active = true;
item.opacity = 1;
} else {
item.active = false;
item.opacity = 0;
}
item.id = index
})
})
},
}
</script>
Child
<template>
<div class='slider'>
<transition-group class='carouse carousel--fullHeight carousel--gradient' tag="div" name="fade">
<div v-for="slide in slides"
class="carousel__slide"
v-bind:class="{ active: slide.active }"
:key="slide.id"
:style="{ 'background-image': 'url(' + slide.image.url + ')' }"
v-show="slide.active"
>
<div class="carousel__caption carousel__caption--centered">
<h2 class="heading heading--white heading--uppercase heading--fixed">{{ slide.tagline }}</h2>
</div>
</div>
</transition-group>
<div class='carousel__controls carousel__controls--numbered carousel__controls--white carousel__controls--bottomRight carousel__controls--flex'>
<div #click="next" class="in">
<img src="/static/img/svg/next-arrow.svg" />
<span v-if="carousel.length < 10">0</span>
<span>{{ slideCount }}</span>
<span>/</span>
<span v-if="carousel.length < 10">0</span>
<span>{{ carousel.length }}</span>
</div>
</div>
</div>
</template>
<script>
import bus from '../bus'
import Booking from './Booking'
export default {
name: 'HomeCarousel',
props: ['carousel'],
data() {
return {
slideCount: 1,
slides: [],
/*
slides: [{
image: this.themepath + 'home-banner.jpg',
active: true,
captionText: 'A PLACE AS UNIQUE AS YOU ARE',
buttonText: 'book now',
buttonUrl: '#',
opacity: 1,
id: 1
},
{
image: this.themepath + 'home-banner2.jpg',
active: false,
captionText: 'A PLACE AS UNIQUE AS YOU ARE',
buttonText: 'book now',
buttonUrl: '#',
opacity: 0,
id: 2
}
]
*/
}
},
methods: {
showBooking: function() {
this.$store.state.showBooking = true;
},
next() {
const first = this.slides.shift();
this.slides = this.slides.concat(first)
first.active = false;
this.slides[0].active = true;
if (this.slideCount === this.slides.length) {
this.slideCount = 1;
} else {
this.slideCount++;
}
},
previous() {
const last = this.slides.pop()
this.slides = [last].concat(this.slides)
// Loop through Array and set all active values to false;
var slideLength = this.slides.length;
for (var slide = 0; slide < slideLength; slide++) {
this.slides[slide].active = false;
}
// Apply active class to first slide
this.slides[0].active = true;
this.slideCount--;
},
loopInterval() {
let self = this;
setInterval(function () {
self.next()
}, 8000);
}
},
created() {
this.slides = this.carousel;
}
}
</script>
You can just watch the prop and set this.slides when it changes, i.e. when the async call has finished:
watch:{
carousel(value) {
this.slides = value
}
}
Here's a JSFiddle: https://jsfiddle.net/nwLh0d4w/
I'm struggling to understand how to make my component reactive. At the moment the button is rendered correctly but once the create/delete event happens, the template does not change. Any tips on how to update the component after the event has taken place?
new Vue({
el: '#app'
});
Vue.component('favourite-button', {
props: ['id', 'favourites'],
template: '<input class="hidden" type="input" name="_method" value="{{ id }}" v-model="form.listings_id"></input><button v-if="isFavourite == true" class="favourited" #click="delete(favourite)" :disabled="form.busy"><i class="fa fa-heart" aria-hidden="true"></i><button class="not-favourited" v-else #click="create(favourite)" :disabled="form.busy"><i class="fa fa-heart" aria-hidden="true"></i></button><pre>{{ isFavourite == true }}</pre>',
data: function() {
return {
form: new SparkForm({
listings_id: ''
}),
};
},
created() {
this.getFavourites();
},
computed: {
isFavourite: function() {
for (var i = 0; this.favourites.length; i++)
{
if (this.favourites[i].listings_id == this.id) {
return true;
}
}
},
},
methods: {
getFavourites() {
this.$http.get('/api/favourites')
.then(response => {
this.favourites = response.data;
});
},
create() {
Spark.post('/api/favourite', this.form)
.then(favourite => {
this.favourite.push(favourite);
this.form.id = '';
});
},
delete(favourite) {
this.$http.delete('/api/favourite/' + this.id);
this.form.id = '';
}
}
});
Vue.component('listings', {
template: '#listing-template',
data: function() {
return {
listings: [], favourites: [],
};
},
created() {
this.getListings();
},
methods: {
getListings() {
this.$http.get('/api/listings')
.then(response => {
this.listings = response.data;
});
}
}
});
Vue expects HTML template markups to be perfect. Otherwise you will run into multiple issues.
I just inspected your template and found an issue - the first <button> element does not close.
Here is the updated version of your code:
Vue.component('favourite-button', {
props: ['id', 'favourites'],
template: `
<input class="hidden" type="input" name="_method" value="{{ id }}" v-model="form.listings_id"></input>
<button v-if="isFavourite == true" class="favourited" #click="delete(favourite)" :disabled="form.busy">
<i class="fa fa-heart" aria-hidden="true"></i>
</button> <!-- This is missing in your version -->
<button class="not-favourited" v-else #click="create(favourite)" :disabled="form.busy">
<i class="fa fa-heart" aria-hidden="true"></i>
</button>
<pre>{{ isFavourite == true }}</pre>
`,
...
Note the comment on 7th line above, the closing </button> tag is not present in your template.
As a side note, if you do not want to type back-slash at the end of every line to make multi-line template strings, you can use back-ticks as shown in my code example above. This will help you avoid markup errors leading to Vue component issues and many hours of debugging.
Another reference: Check out "Multi-line Strings" in this page: https://developers.google.com/web/updates/2015/01/ES6-Template-Strings
Relevant lines (copied from above page):
Any whitespace inside of the backtick syntax will also be considered part of the string.
console.log(`string text line 1
string text line 2`);
EDIT: Found a possible bug in code
Here is another issue in your create method of favourite-button component:
methods: {
// ...
create() {
Spark.post('/api/favourite', this.form)
.then(favourite => {
this.favourite.push(favourite); // Note: This is the problem area
this.form.id = '';
});
},
//...
}
Your success handler refers to this.favourite.push(...). You do not have this.favourite in data or props of your component. Shouldn't it be this.favourites?