I have a form that is built with Vuejs in my Rails 5.1 app. All my fields work well and persist data to the database, except for file uploads. I get the error
[Vue warn]: Error compiling template: printed at the top of the console, then essentially my entire template code, then
- <input v-model="variation.photo_one" type="file">:
File inputs are read only. Use a v-on:change listener instead.
I am new to Vuejs and cannot figure out how to get this to work even after reading many other online posts regarding this.
_form.html.erb
<%= content_tag :div,
id: "product-form",
data: {
id: product.id,
product: product.to_json(except: [:id, :created_at, :updated_at]),
variations_attributes: product.variations.to_json(except: [:product_id, :created_at, :updated_at]),
} do %>
...
<div class="col-md-4 upload-block">
<label>Photo One</label>
<input type="file" v-model="variation.photo_one" style="margin-bottom: .5em">
</div>
...
<% end %>
app_vue.js
import Vue from 'vue/dist/vue.esm'
import TurbolinksAdapter from 'vue-turbolinks'
import VueResource from 'vue-resource'
Vue.use(VueResource)
Vue.use(TurbolinksAdapter)
document.addEventListener('turbolinks:load', () => {
Vue.http.headers.common['X-CSRF-Token'] = document.querySelector('meta[name="csrf-token"]').getAttribute('content')
var element = document.getElementById("product-form")
if (element != null) {
var id = element.dataset.id
var product = JSON.parse(element.dataset.product)
var variations_attributes = JSON.parse(element.dataset.variationsAttributes)
variations_attributes.forEach(function(variation) { variation._destroy = null })
product.variations_attributes = variations_attributes
var app = new Vue({
el: element,
data: function() {
return { id: id, product: product }
},
methods: {
addVariation: function() {
this.product.variations_attributes.push({
id: null,
name: "",
photo_one: "",
//position: "",
_destroy: null
})
},
removeVariation: function(index) {
var variation = this.product.variations_attributes[index]
if (variation.id == null) {
this.product.variations_attributes.splice(index, 1)
} else {
this.product.variations_attributes[index]._destroy = "1"
}
},
undoRemove: function(index) {
this.product.variations_attributes[index]._destroy = null
},
saveProduct: function() {
// Create a new product
if (this.id == null) {
this.$http.post('/products', { product: this.product }).then(response => {
Turbolinks.visit(`/products/${response.body.id}`)
}, response => {
console.log(response)
})
// Edit an existing product
} else {
this.$http.put(`/products/${this.id}`, { product: this.product }).then(response => {
Turbolinks.visit(`/products/${response.body.id}`)
}, response => {
console.log(response)
})
}
},
existingProduct: function() {
return this.product.id != null
}
}
})
}
})
Files are a bit awkward in Vue. As the message says, you cannot use v-model for an input with type="file". Instead you must use the change event and call a method in your component to manually handle the file.
<input type="file" #change="handleFileChange" />
methods: {
handleFileChange(event) {
//you can access the file in using event.target.files[0]
this.fileField = event.target.files[0];
}
}
When you submit the AJAX request, you will likely need to submit a FormData object instead of submitting a javascript object. The MDN docs have an explanation on how to use that. I find the FormData is the more awkward part of dealing with file uploads. https://developer.mozilla.org/en-US/docs/Web/API/FormData
Related
My component:
<template>
<div>
<select v-model="minecraftVersion">
<option
v-for="version in minecraftVersions"
:key="version.version"
:value="version.version"
>{{version.version}}</option>
</select>
<code>
dependencies {
minecraft "com.mojang:minecraft:{{minecraftVersion}}"
mappings "net.fabricmc:yarn:{{yarnVersion}}:v2"
modImplementation "net.fabricmc:fabric-loader:{{loaderVersion}}"
modImplementation "{{fabricMaven}}{{fabricVersion}}"
}
</code>
</div>
</template>
<script>
import stripIndent from "strip-indent";
export default {
data() {
return {
minecraftVersions: [],
minecraftVersion: "<minecraftVersion>",
yarnVersion: "<yarnVersion>",
loaderVersion: "<loaderVersion>",
fabricMaven: "<fabricMaven>",
fabricVersion: "<fabricVersion>"
};
},
mounted() {
this.$el.querySelectorAll("code").forEach(code => {
code.innerHTML = stripIndent(code.innerHTML);
});
fetch("https://meta.fabricmc.net/v2/versions/game")
.then(response => response.json())
.then(data => this.minecraftVersions = data);
let hash = this.$route.hash.substr(1).split("-");
if (hash[0] == "version") {
this.minecraftVersion = hash.slice(1).join("-");
} else {
for (let version of this.minecraftVersions) {
if (version.stable) {
this.minecraftVersion = version.version;
break;
}
}
}
},
watch: {
minecraftVersion(newVersion, oldVersion) {
if (oldVersion != "<minecraftVersion>") {
this.$router.replace({ hash: "#version-" + newVersion });
}
}
}
};
</script>
Data fetches fine, but all {{mustache}} props inside <code> don't react to changes. They are displayed as if they weren't changed. When putting outside <code>, all props work as intended.
P.S. Also when choosing a version with a hyphen it throws SyntaxError: Document.querySelector: '#version-1.16-pre2' is not a valid selector, yet continues to work as intended
querySelectorAll() interfered somehow. Deleting it helps
enter image description hereI am making an app in Nuxt and vue using storyblok as my CMS. However, I have been receiving errors when trying to link the storyblok array to my arrays called in my template using v-for.
Here is the template:
<template>
<div>
<!-- instance header -->
<InstanceHeader title="Books" />
<div class="pageContainer">
<div class="booksInfoPost">
<div class="booksInfoPost__subHeader"><h3>Top Books</h3></div>
<div class="booksInfoPost__topBooks">
<BooksInfoPostTop
v-for="book in books"
:key ="book.id"
:bookCover="book.bookCover"
:title="book.title"
:author="book.author"
:content="book.content"
:id="book.id"
/>
</div>
<div class="booksInfoPost__subHeader"><h3>Book Titles</h3></div>
<BooksInfoPost
v-for="book in posts"
:key ="book.id"
:bookCover="book.bookCover"
:title="book.title"
:author="book.author"
:content="book.content"
:id="book.id"
/>
</div>
</div>
Here is my script:
export default {
components: {
InstanceHeader,
BooksInfoPostTop,
BookTitles,
BooksInfoPost
},
data() {
/* return {
books: [],
posts: []
} */
},
async asyncData(context) {
return {
bookTitles: context.app.$storyapi
.get("cdn/stories", { version: "draft", starts_with: 'books/book-titles'})
.then(response => {
console.log(response);
return {
posts: response.data.stories.map(bp => {
return {
id: bp.slug,
bookCover: bp.content.bookCover,
title: bp.content.title,
author: bp.content.author
};
}),
}
}),
topBooks: context.app.$storyapi
.get("cdn/stories", { version: "draft", starts_with: 'books/top-books'})
.then(response => {
console.log(response);
return {
books: response.data.stories.map(b => {
return {
id: b.slug,
bookCover: b.content.bookCover,
title: b.content.title,
author: b.content.author
};
}),
}
})
}
}
}
I noticed this error more when I tried calling two APIs from storyblok. When I called one API call I did not see this error. I have also tried using Axios but I am getting errors using that method as well. I am not the most experienced developer and If anyone can help I'll appreciate it. Thanks
export default {
components: {
InstanceHeader,
BooksInfoPostTop,
BookTitles,
BooksInfoPost
},
async asyncData(context) {
const result = {};
const mapBooks = b => {
return {
id: b.slug,
bookCover: b.content.bookCover,
title: b.content.title,
author: b.content.author
};
};
const { data } = await context.app.$storyapi
.get("cdn/stories", {
version: "draft",
starts_with: 'books/book-titles'
});
result.posts = data.stories.map(mapBooks);
const result = await context.app.$storyapi
.get("cdn/stories", {
version: "draft",
starts_with: 'books/top-books'
});
result.books = result.data.stories.map(mapBooks);
return result; // it has right property names {books:[], posts:[]}
}
}
Well as you mentioned in the comment it was a little mess before. So i tidied it up. The idea is that you need direct property names instead of nested objects. This way it should work, if it is not working check the network tab for the errors.
I'm using a Vue Multiselect instance with 2 functions (one basically hits the database for an autocomplete function, which works. The other is adding a new one that isn't in the database)
So say 'Tag One' is in the database, if I type that and it shows then hitting enter or selecting will save it to the tags (multiselect with tagging enabled). However, if I type 'Tag Three' which isn't in the database and I hit enter or select, it just disappears and doesn't add to the tags or call the axios function in my addTag method.
What exactly am I doing wrong?
<script src="https://unpkg.com/vue-multiselect#2.1.0"></script>
<script src="https://unpkg.com/#johmun/vue-tags-input/dist/vue-tags-input.js"></script>
<div id="tagApp">
<multiselect
label="tag_data"
track-by="campaign_tag_id"
v-model="value"
:options="options"
:multiple="true"
:taggable="true"
#tag="addTag"
#search-change="val => read(val)"
:preselect-first="false"
:close-on-select="false"
:clear-on-select="true"
:preserve-search="true"
tag-placeholder="Add this as new tag"
placeholder="Search or add a tag"
></multiselect>
</div>
new Vue({
components: {
Multiselect: window.VueMultiselect.default
},
el: "#tagApp",
data() {
return{
value: [],
loading: false,
options: []
}
},
methods: {
read: function(val){
if (val) {
this.loading = true;
this.options = [];
const self = this;
console.log(val);
axios.get('search',{params: {query: val}})
.then(function (response) {
self.options = response.data;
console.log(response.data);
});
} else {
this.options = [];
}
},
addTag(newTag) {
const tag = {
tag_data: newTag,
};
const campaign_id = document.querySelector("input[name=campaign_id]").value;
this.options.push(tag);
this.value.push(tag);
axios.post('tags/save',{
tag_data: newTag,
})
.then(function (response){
console.log(response);
})
.catch(function (error) {
console.log(error);
});
}
}
})
I don’t think you are doing anything wrong, I just don’t think the component supports what you want to do.
But, what you could do is always add the search term to the options array if it isn’t there already. Putting the below in the axios callback could be all you need.
self.options = response.data;
self.options.push(val)
Now you could slice it to the front, or keep it pushed at the end, and code to prevent duplicates etc.
I have a functioning Vue Multiselect where I'm using an axios call to fill the options from my database values. This works perfectly and allows me to choose from existing options or enter new options in order to create tags.
As it is, this works perfectly. But I need a way, if possible, to make another Axios call every time the user selects and option or hits the enter key to save a tag option. Is there a way to do this?
This is my first experience with Vue and I'm really not sure how feasible this is, but basically I'm just wondering how to make an axios call every time a tag is selected or entered with the enter key
<div id="tagApp">
<multiselect
label="tag_data"
track-by="campaign_tag_id"
v-model="value"
:options="options"
:multiple="true"
:taggable="true"
#tag="addTag"
#search-change="val => read(val)"
:preselect-first="false"
:close-on-select="false"
:clear-on-select="true"
:preserve-search="true"
tag-placeholder="Add this as new tag"
placeholder="Search or add a tag"
></multiselect>
</div>
new Vue({
components: {
Multiselect: window.VueMultiselect.default
},
el: "#tagApp",
data() {
return{
value: [],
loading: false,
options: []
}
},
methods: {
read: function(val){
//console.log('searched for', val);
if (val) {
this.loading = true;
this.options = [];
const self = this;
console.log(val);
axios.get('campaigns/search',{params: {query: val}})
.then(function (response) {
self.options = response.data;
console.log(response.data);
});
} else {
this.options = [];
}
},
addTag(newTag) {
const tag = {
tag_data: newTag,
};
this.options.push(tag);
this.value.push(tag);
}
}
})
Monitor #select event and trigger a function where your Axios call will happen.
<div id="tagApp">
<multiselect
...
#select= "callAxios"
...
></multiselect>
</div>
...
methods: {
callAxios: function(val){
//your Axios call here
},
...
}
...
I'm building a key-command resource and giving VueJS a whirl while doing so. I'm a newbie but am gaining the grasp of things (slowly...).
I want to be able to search in a global search form for key commands I'm defining as actions within sections of commands (see data example below). I would like to search through all the actions to show only those that match the search criteria.
My HTML is below:
<div id="commands">
<input v-model="searchQuery" />
<div class="commands-section" v-for="item in sectionsSearched"
:key="item.id">
<h3>{{ item.section }}</h3>
<div class="commands-row" v-for="command in item.command" :key="command.action">
{{ command.action }}
</div>
</div>
</div>
My main Vue instance looks like this:
import Vue from 'vue/dist/vue.esm'
import { commands } from './data.js'
document.addEventListener('DOMContentLoaded', () => {
const element = document.getElementById("commands")
if (element != null) {
const app = new Vue({
el: element,
data: {
searchQuery: '',
commands: commands
},
computed: {
sectionsSearched() {
var self = this;
return this.commands.filter((c) => {
return c.command.filter((item) => {
console.log(item.action)
return item.action.indexOf(self.searchQuery) > -1;
});
});
},
}
});
}
});
And finally the data structure in data.js
const commands = [
{
section: "first section",
command: [
{ action: '1' },
{ action: '2' },
{ action: '3' },
],
},
{
section: "second section",
command: [
{ action: 'A' },
{ action: 'B' },
{ action: 'C' },
]
},
]
export { commands };
I'm able to output the commands using the console.log(item.action) snippet you see in the computed method called sectionsSearched.
I see no errors in the browser and the data renders correctly.
I cannot however filter by searching in real-time. I'm nearly positive it's a combination of my data structure + the computed method. Can anyone shed some insight as to what I'm doing wrong here?
I'd ideally like to keep the data as is because it's important to be sectioned off.
I'm a Rails guy who is new to this stuff so any and all feedback is welcome.
Thanks!
EDIT
I've tried the proposed solutions below but keep getting undefined in any query I pass. The functionality seems to work in most cases for something like this:
sectionsSearched() {
return this.commands.filter((c) => {
return c.command.filter((item) => {
return item.action.indexOf(this.searchQuery) > -1;
}).length > 0;
});
},
But alas nothing actually comes back. I'm scratching my head hard.
There is a issue in your sectionsSearched as it is returning the array of just commands.
See this one
sectionsSearched() {
return this.commands.reduce((r, e) => {
const command = e.command.filter(item => item.action.indexOf(this.searchQuery) > -1);
const section = e.section;
r.push({
section,
command
});
}, []);
}
const commands = [
{
section: "first section",
command: [
{ action: '1' },
{ action: '2' },
{ action: '3' },
],
},
{
section: "second section",
command: [
{ action: 'A' },
{ action: 'B' },
{ action: 'C' },
]
},
]
const element = document.getElementById("commands")
if (element != null) {
const app = new Vue({
el: element,
data: {
searchQuery: '',
commands: commands
},
computed: {
sectionsSearched() {
var self = this;
return this.commands.filter((c) => {
// the code below return an array, not a boolean
// make this.commands.filter() not work
// return c.command.filter((item) => {
// return item.action.indexOf(self.searchQuery) > -1;
// });
// to find whether there has command action equal to searchQuery
return c.command.find(item => item.action === self.searchQuery);
});
},
}
});
}
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<div id="commands">
<input v-model="searchQuery" />
<div class="commands-section" v-for="item in sectionsSearched"
:key="item.id">
<h3>{{ item.section }}</h3>
<div class="commands-row" v-for="command in item.command" :key="command.action">
{{ command.action }}
</div>
</div>
</div>
Is that work as you wish ?
sectionsSearched() {
return this.commands.filter((c) => {
return c.command.filter((item) => {
return item.action.indexOf(this.searchQuery) > -1;
}).length > 0;
});
},
}
since filter will always return an array(empty or not) which value always is true.