I've got the following problem, while reading in a File and reaching it to the Parent Component.
First I've got an FileInput on the FileReaderComponent, when File is Changed it sends an emit('change', file.content).
The top Component gets the file.content and sets it as the Input Prop of the FileViewerComponent.
As so far, it works as expected. But when I add a second FileReaderComponent, which Content should be displayed in the second FileViewerComponent.
But it always uses the #change from the first FileReaderComponent.
I'm very new to Vue. I build an minimal example to show:
Same behavior. When I use the Second FileReaderComponent it should put data into readerOutput2 but it puts data into readerOutput1 for some reason.
I can't figure out what I did wrong.
App.vue
<template>
<!-- <img alt="Vue logo" src="./assets/lpdLogo.svg" style="width: 100px; fill: green;"> -->
<FileReaderComponent #change="data.readerOutput1 = $event"/>
<FileViewerComponent v-model:input="data.readerOutput1"/>
<FileReaderComponent #change="data.readerOutput2 = $event"/>
<FileViewerComponent v-model:input="data.readerOutput2"/>
</template>
<script>
import FileReaderComponent from './components/FileReaderComponent.vue';
import FileViewerComponent from './components/FileViewerComponent.vue';
import { reactive } from 'vue';
export default {
name: 'App',
components: {
FileReaderComponent,
FileViewerComponent
},
setup () {
const data = reactive({
readerOutput1: '',
readerOutput2: ''
});
function log(toLog) {
console.log(toLog);
}
return { log, data }
}
}
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
FileReaderComponent.vue
<template>
<div id="fileUploadComponent" class="inline-flex items-center p-2 rounded border-2 border-yellow-500 m-1">
<input type='file' id="fileInput" name="fileInput" #change="fileUploadChange()" ref="fileInput" class="hidden"/>
<label for="fileInput" class="h-10 w-40 rounded text-gray-300 dark:text-gray-400 bg-gray-700 hover:bg-gray-500 text-xs" style="line-height: 2.5rem">
<span class="block text-center w-full">Datei öffnen</span></label>
<span id="filenamefield" class="inline-block m-2 text-gray-700 dark:text-gray-300">{{file.name}}</span>
</div>
</template>
<script>
import {reactive} from 'vue';
export default {
name: 'FileReaderComponent',
emits: ['inFocus', 'submit', 'change'],
props: {
input: {
type: String,
required: false,
},
},
methods : {
setContent(content) {
console.log('FileUpload, set Content', content)
this.file.content = content;
this.$emit('change', content);
},
fileUploadChange() {
console.log('fileUploadChange triggered');
this.file.input = this.$refs.fileInput.files[0];
this.file.name = this.file.input.name;
let file = this.file.input;
let parent = this;
parent.setContent('Content Loading')
function onloadevent(evt) {
parent.setContent(evt.target.result);
}
if (file) {
var reader = new FileReader();
reader.readAsText(file, "UTF-8");
reader.onload = function (evt) {
onloadevent(evt);
}
reader.onerror = function () {
onloadevent('An Error occurred while reading File');
}
}
}
},
setup(props, {emit}) {
const file = reactive({
name: 'Keine Ausgewählt',
input: Element,
content: String,
})
return {file}
}
}
</script>
<style scoped>
.inputfile {
width: 0.1px;
height: 0.1px;
opacity: 0;
overflow: hidden;
position: absolute;
z-index: -1;
}
.inputfile+label {
font-weight: 700;
display: inline-block;
}
input:checked+svg {
display: block;
}
</style>
FileViewerComponent
<template>
<div id="fileViewerComponent" class="inline-flex items-center p-2 rounded border-2 border-yellow-500 m-1">
<h3>Input:</h3>
<div id="view">
{{ input }}
</div>
</div>
</template>
<script>
import {reactive} from 'vue';
export default {
name: 'FileViewerComponent',
emits: ['inFocus', 'submit', 'change'],
props: {
input: {
type: String,
required: false,
},
},
methods : {
},
setup(props, {emit}) {
const file = reactive({
name: 'Keine Ausgewählt',
input: Element,
content: String,
})
return {file}
}
}
</script>
<style scoped>
</style>
Finally i got an Solution.
The Problem was caused by the FileInput label pointing to id="fileInput" wich is the input. But when the input is 2 times rendered, the label accesses the first found id="fileInput".
My Solution:
FileUploadComponent.vue
I had to reach an ID to the Component an use it for the Input ID and the label for
<template>
<div id="fileUploadComponent" class="inline-flex items-center p-2 rounded border-2 border-yellow-500 m-1">
<input type='file' v-bind:id="id" name="fileInput" #change="fileUploadChange()" ref="fileInput" class="hidden"/>
<label v-bind:for="id" class="h-10 w-40 rounded text-gray-300 dark:text-gray-400 bg-gray-700 hover:bg-gray-500 text-xs" style="line-height: 2.5rem">
<span class="block text-center w-full">Datei öffnen</span></label>
<span id="filenamefield" class="inline-block m-2 text-gray-700 dark:text-gray-300">{{file.name}}</span>
</div>
</template>
<script>
import {reactive} from 'vue';
export default {
name: 'FileReaderComponent',
emits: ['inFocus', 'submit', 'change'],
props: {
input: {
type: String,
required: false,
},
id : {
type: Number,
required: false,
}
},
methods : {
setContent(content) {
console.log('FileUpload, set Content', content)
this.file.content = content;
this.$emit('change', content);
},
fileUploadChange() {
console.log('fileUploadChange triggered');
this.file.input = this.$refs.fileInput.files[0];
this.file.name = this.file.input.name;
let file = this.file.input;
let parent = this;
parent.setContent('Content Loading')
function onloadevent(evt) {
parent.setContent(evt.target.result);
}
if (file) {
var reader = new FileReader();
reader.readAsText(file, "UTF-8");
reader.onload = function (evt) {
onloadevent(evt);
}
reader.onerror = function () {
onloadevent('An Error occurred while reading File');
}
}
}
},
setup(props, {emit}) {
const file = reactive({
name: 'Keine Ausgewählt',
input: Element,
content: String,
})
return {file}
}
}
</script>
<style scoped>
.inputfile {
width: 0.1px;
height: 0.1px;
opacity: 0;
overflow: hidden;
position: absolute;
z-index: -1;
}
.inputfile+label {
font-weight: 700;
display: inline-block;
}
input:checked+svg {
display: block;
}
</style>
Related
Created a small project with vuejs and want to display all my icons but for some reason it does not displays my icons, Can anyone tell me where I am making the mistakes.
I want to display all my icons from that icons list in the div element.
Any help will be much appreciated, thanks!
ValidatingProps.vue
<template>
<div>
<h1>How to use Validation in props</h1>
<h3>{{ status }}</h3>
<div class="icons" v-html="icons"></div>
</div>
</template>
<script>
import icons from "./icons";
console.log(icons);
export default {
name: "validatingProps",
props: {
status: {
type: String,
default: "Validating",
validator: (value) => {
console.log("Val", value);
return ["in-progress", "complete"].includes(value);
},
},
icons: {
type: String,
required: true,
validator: (value) => {
console.log("Value >>>> ", value);
Object.keys(icons).includes(value);
console.log("Icons props: ", Object.keys(icons).includes(value));
},
},
},
mounted() {
console.log("mounted ", Object.keys(icons));
console.log("this.Icons: ", this.icons);
},
};
</script>
<style scoped>
.icons {
height: 200px;
width: 200px;
border: 2px solid red;
margin: 0 auto;
background: lightcyan;
}
App.vue
<div>
<template>
<ValidatingProps
status="progress"
:icons="Object.keys(this.icons.default)"
/>
</div>
</template>
<script>
import ValidatingProps from "./components/validatingProps.vue";
import * as icon from "./components/icons/";
export default {
name: "App",
components: {
ValidatingProps,
},
data() {
return {
icons: icon,
}
}
</script>
my icons folder structure:
My Div box shows:
I have the following problem:
I have too much logic in my inline style and would to place it inside a computed property. I know, that this is the way, that I should go, but do not know, how to achieve it.
Below I a simple example that I made for better understanding. In it, on button press, the child's component background-color is changing.
My code can be found here: Codesandbox
My parent component:
<template> <div id="app">
<MyChild :colorChange="active ? 'blue' : 'grey'" />
<p>Parent:</p>
<button #click="active = !active">Click Me!</button> </div> </template>
<script> import MyChild from "./components/MyChild";
export default { name: "App", components: {
MyChild, }, data() {
return {
active: false,
}; }, }; </script>
and my child component:
<template> <div class="hello">
<p>Hello from the child component</p>
<div class="myDiv" :style="{ background: colorChange }">
here is my color, that I change
</div> </div> </template>
<script> export default { name: "HelloWorld", props: {
colorChange: {
type: String,
default: "green",
}, }, }; </script>
<!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> .myDiv { color: white; padding: 1rem 0; } </style>
I also have a second question. Let's say, that I have more than one child component and also would like to change to colors on button click, but those colors differ. How can I achieve it without repeating myself (within the computed properties?)
Code example for my parent component:
<MyChild :colorChange="active ? 'blue' : 'grey'" />
<MyChild :colorChange="active ? 'grey' : 'blue'" />
<MyChild :colorChange="active ? 'blue' : 'red'" />
<MyChild :colorChange="active ? 'yellow' : 'blue'" />
Thanks in advance!
Maybe You can bind class and use different css classes:
Vue.component('MyChild',{
template: `
<div class="hello">
<p>Hello from the child component</p>
<div class="myDiv" :class="collorCurrent">
here is my color, that I change
</div>
</div>
`,
props: {
colorChange: {
type: String,
default: "green",
},
colorDef: {
type: String,
default: "green",
},
isActive: {
type: Boolean,
default: false,
},
},
computed: {
collorCurrent() {
return this.isActive ? this.colorChange : this.colorDef
}
}
})
new Vue({
el: "#demo",
data() {
return {
active: false,
}
},
})
.myDiv { color: white; padding: 1rem; }
.blue {
background: blue;
font-size: 22px;
}
.red {
background: red;
font-variant: small-caps;
}
.yellow {
background: yellow;
color: black;
}
.grey {
background: grey;
text-decoration: underline;
}
.green {
background: green;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="demo">
<p>Parent:</p>
<button #click="active = !active">Click Me!</button>
<my-child :is-active="active" :color-def="'grey'" :color-change="'blue'"></my-child>
<my-child :is-active="active" :color-def="'blue'" :color-change="'grey'"></my-child>
<my-child :is-active="active" :color-def="'red'" :color-change="'blue'"></my-child>
<my-child :is-active="active" :color-def="'blue'" :color-change="'yellow'"></my-child>
</div>
I have displayed a list of images with some information. I want those images to be clickable. And when clicked it should show a div with saying "HI!!". I have been trying to add a variable as show:true in Vue data and tried to build some logic that show becomes false when clicked. But I have not been able to achieve it.
Below is the sample code:
template>
<div>
<h1>SpaceX</h1>
<div v-for="launch in launches" :key="launch.id" class="list" #click="iclickthis(launch)">
<div ><img :src="launch.links.patch.small" alt="No Image" title={{launch.name}} /></div>
<div>ROCKET NAME: {{launch.name}} </div>
<div>DATE: {{ launch.date_utc}} </div>
<div>SUCCESS: {{ launch.success}} </div>
<div>COMPLETENESS: {{ launch.landing_success}} </div>
</div>
<!-- <v-model :start="openModel" #close="closeModel" /> -->
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'SpaceXTimeline',
components: {
},
data: () => ({
launches : [],
openModel : false,
show : true,
}),
methods: {
iclickthis(launch) {
// this should load a model search v-model / bootstrap on vue npm install v-model
console.log(launch.name + " is launched");
console.log("DETAILS: "+ launch.details);
console.log("ROCKET INFO: "+ launch.links.wikipedia);
console.log("CREW DETAILS: "+ launch.crew);
console.log("Launchpad Name: "+ launch.launchpad);
this.openModel = true;
},
closeModel () {
this.openModel = false;
}
},
async created() {
const {data} = await axios.get('https://api.spacexdata.com/v4/launches');
data.forEach(launch => {
this.launches.push(launch);
});
}
};
</script>
<style scoped>
.list {
border: 1px solid black;
}
</style>
Thanks, and appreciate a lot.
v-model is a binding and not an element, unless you've named a component that? Is it a misspelling of "modal"?
Either way, sounds like you want a v-if:
<v-model v-if="openModel" #close="closeModel" />
Example:
new Vue({
el: '#app',
components: {},
data: () => ({
launches: [],
openModel: false,
show: true,
}),
methods: {
iclickthis(launch) {
// this should load a model search v-model / bootstrap on vue npm install v-model
console.log(launch.name + ' is launched');
console.log('DETAILS: ' + launch.details);
console.log('ROCKET INFO: ' + launch.links.wikipedia);
console.log('CREW DETAILS: ' + launch.crew);
console.log('Launchpad Name: ' + launch.launchpad);
this.openModel = true;
},
closeModel() {
this.openModel = false;
},
},
async created() {
const {
data
} = await axios.get('https://api.spacexdata.com/v4/launches');
data.forEach(launch => {
this.launches.push(launch);
});
},
})
Vue.config.productionTip = false;
Vue.config.devtools = false;
.modal {
cursor: pointer;
display: flex;
justify-content: center;
position: fixed;
top: 0;
width: 100%;
height: 100vh;
padding: 20px 0;
background: rgba(255, 255, 255, 0.5);
}
img {
cursor: pointer;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<div id="app">
<h1>SpaceX</h1>
<div v-for="launch in launches" :key="launch.id" class="list" #click="iclickthis(launch)">
<div>
<img :src="launch.links.patch.small" alt="No Image" :title="launch.name" />
</div>
<div>ROCKET NAME: {{ launch.name }}</div>
<div>DATE: {{ launch.date_utc }}</div>
<div>SUCCESS: {{ launch.success }}</div>
<div>COMPLETENESS: {{ launch.landing_success }}</div>
</div>
<div v-if="openModel" #click="closeModel" class="modal">
MODAL
</div>
</div>
I have made a form component (CreateDocument) in Nuxt. Inside this component i made also an autocomplete (AutoCompleteFilters).
When I hit enter inside the autocomplete component, also the CreateDocument is listening to the enter key. But I only want that a specific input field is listing to the enter key event.
This is the CreateDocument component:
<template>
<div>
<Notification :message="notification" v-if="notification"/>
<form method="post" #submit.prevent="createDocument">
<div class="create__document-new-document">
<div class="create__document-new-document-title">
<label>Titel</label>
<input
type="text"
class="input"
name="title"
v-model="title"
required
>
</div>
<div class="create__document-new-document-textarea">
<editor
apiKey="nothing"
v-model="text"
initialValue=""
:init="{
height: 750,
width: 1400
}"
>
</editor>
</div>
<div class="create__document-new-document-extra-info">
<div class="create__document-new-document-tags">
<label>Tags</label>
<AutoCompleteFilters/>
</div>
<div class="create__document-new-document-clients">
<label>Klant</label>
<input
type="text"
class="input"
name="client"
v-model="client"
required
>
</div>
</div>
<Button buttonText="save" />
</div>
</form>
</div>
</template>
<script>
import Notification from '~/components/Notification'
import Editor from '#tinymce/tinymce-vue'
import Button from "../Button";
import { mapGetters, mapActions } from 'vuex'
import AutoCompleteFilters from "./filters/AutoCompleteFilters";
export default {
computed: {
...mapGetters({
loggedInUser: 'loggedInUser',
})
},
middleware: 'auth',
components: {
Notification,
Button,
editor: Editor,
AutoCompleteFilters
},
data() {
return {
title: '',
text: '',
tags: '',
client: '',
notification: null,
}
},
methods: {
...mapActions({
create: 'document/create'
}),
createDocument () {
const documentData = {
title: this.title,
text: this.text,
tags: this.tags,
client: this.client,
userId: this.loggedInUser.userId
};
this.create(documentData).then((response) => {
this.notification = response;
this.title = '';
this.text = '';
this.tags = '';
this.client= '';
})
}
}
}
</script>
And this is the AutoCompleteFilters component:
<template>
<div class="autocomplete">
<input
type="text"
id="my-input"
#input="onChange"
v-model="search"
#keydown.down="onArrowDown"
#keydown.up="onArrowUp"
#keydown.enter="onEnter"
/>
<ul
v-show="isOpen"
class="autocomplete-results"
>
<li
v-for="result in results"
:key="results.id"
class="autocomplete-result"
#click="setResult(result.name)"
:class="{ 'is-active': results.indexOf(result) === arrowCounter }"
>
{{ result.name }}
</li>
</ul>
</div>
</template>
<script>
import {mapActions} from 'vuex'
export default {
data() {
return {
isOpen: false,
results: false,
search: '',
arrowCounter: 0,
filter: null,
position: 0
};
},
methods: {
...mapActions({
getFilterByCharacter: 'tags/getTagsFromDb'
}),
onChange(e) {
this.isOpen = true;
this.position = e.target.selectionStart;
},
setResult(result) {
this.search = result;
this.isOpen = false;
},
getResults(){
this.getTagsByValue(this.search).then((response) => {
this.results = response;
});
},
async getTagsByValue(value){
const filters = {autocompleteCharacter : value};
return await this.getFilterByCharacter(filters);
},
onArrowDown() {
if (this.arrowCounter < this.results.length) {
this.arrowCounter = this.arrowCounter + 1;
}
},
onArrowUp() {
if (this.arrowCounter > 0) {
this.arrowCounter = this.arrowCounter - 1;
}
},
onEnter(evt) {
this.search = this.results[this.arrowCounter].name;
this.isOpen = false;
this.arrowCounter = -1;
}
},
watch: {
search: function() {
this.getResults();
}
},
};
</script>
<style>
.autocomplete {
position: relative;
}
.autocomplete-results {
padding: 0;
margin: 0;
border: 1px solid #eeeeee;
height: 120px;
overflow: auto;
width: 100%;
}
.autocomplete-result {
list-style: none;
text-align: left;
padding: 4px 2px;
cursor: pointer;
}
.autocomplete-result.is-active,
.autocomplete-result:hover {
background-color: #4AAE9B;
color: white;
}
</style>
Just as you did in your form to avoid "natural" form submit and replace it with a custom action:
#submit.prevent="createDocument"
... you have to preventDefault the "natural" event that submits the form when you press Enter while focusing the form.
To do so, just add .prevent to your events in the template:
#keydown.down.prevent="onArrowDown"
#keydown.up.prevent="onArrowUp"
#keydown.enter.prevent="onEnter"
I'm working on a basic blog app in Laravel using Vue.js. I've created custom components, registered them in my app.js file, and referred to them in the views by their component names. But I'm getting this error on load:
[Vue warn]: Unknown custom element: - did you register the component correctly? For recursive components, make sure to provide the "name" option.
(found in )
Can anyone spot what I'm doing wrong? Here is my code:
CreatePost.vue:
<template>
<div class="card mt-4" :key="componentKey">
<div class="card-header">New Post</div>
<div class="card-body">
<div v-if="status_msg" :class="{ 'alert-success': status, 'alert-danger': !status }" class="alert" role="alert">
{{ status_msg }}
</div>
<form>
<div class="form-group">
<label for="exampleFormControlInput1">Title</label>
<input v-model="title" type="text" class="form-control" id="title" placeholder="Post Title" required />
</div>
<div class="form-group">
<label for="exampleFormControlTextarea1">Post Content</label>
<textarea v-model="post_body" class="form-control" id="post-content" rows="3" required></textarea>
</div>
<div class>
<el-upload action="https://jsonplaceholder.typicode.com/posts/" list-type="picture-card" :on-preview="handlePictureCardPreview" :on-change="updateImageList" :auto-upload="false">
<i class="el-icon-plus"></i>
</el-upload>
<el-dialog :visible.sync="dialogVisible">
<img width="100%" :src="dialogImageUrl" alt />
</el-dialog>
</div>
</form>
</div>
<div class="card-footer">
<button type="button" #click="createPost" class="btn btn-success">{{ isCreatingPost ? "Posting..." : "Create Post" }}</button>
</div>
</div>
</template>
<style>
.avatar-uploader .el-upload {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.avatar-uploader .el-upload:hover {
border-color: #409eff;
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
line-height: 178px;
text-align: center;
}
.avatar {
width: 178px;
height: 178px;
display: block;
}
</style>
<script>
import { setTimeout } from "timers";
import { mapState, mapActions } from "vuex";
export default {
name: "create-post",
props: ["posts"],
data() {
return {
dialogImageUrl: "",
dialogVisible: false,
imageList: [],
status_msg: "",
status: "",
isCreatingPost: false,
title: "",
post_body: "",
componentKey: 0
};
},
computed: {},
mounted() {},
methods: {
...mapActions(["getAllPosts"]),
updateImageList(file) {
this.imageList.push(file.raw);
},
handlePictureCardPreview(file) {
this.dialogImageUrl = file.url;
this.imageList.push(file);
this.dialogVisible = true;
},
createPost(e) {
e.preventDefault();
if (!this.validateForm()) {
return false;
}
const that = this;
this.isCreatingPost = true;
let formData = new FormData();
formData.append("title", this.title);
formData.append("post_body", this.post_body);
$.each(this.imageList, function(key, image) {
formData.append(`images[${key}]`, image);
});
api.post("/post/create_post", formData, { headers: { "Content-Type": "multipart/form-data" }})
.then(res => {
this.title = this.post_body = "";
this.status = true;
this.showNotification("Post Successfully Created");
this.isCreatingPost = false;
this.imageList = [];
/* "that", defined above, used here instead of "this" to avoid conflict issues */
that.getAllPosts();
that.componentKey += 1;
});
},
validateForm() {
if (!this.title) {
this.status = false;
this.showNotification("Post title cannot be empty");
return false;
}
if (!this.post_body) {
this.status = false;
this.showNotification("Post body cannot be empty");
return false;
}
return true;
},
showNotification(message) {
this.status_msg = message;
setTimeout(() => {
this.status_msg = "";
}, 3000);
}
}
};
</script>
app.js:
/**
* First we will load all of this project's JavaScript dependencies which
* includes Vue and other libraries. It is a great starting point when
* building robust, powerful web applications using Vue and Laravel.
*/
require('./bootstrap');
window.Vue = require('vue');
import store from './store/index';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI);
/**
* The following block of code may be used to automatically register your
* Vue components. It will recursively scan this directory for the Vue
* components and automatically register them with their "basename".
*
* Eg. ./components/ExampleComponent.vue -> <example-component></example-component>
*/
// const files = require.context('./', true, /\.vue$/i)
// files.keys().map(key => Vue.component(key.split('/').pop().split('.')[0], files(key).default))
Vue.component('example-component', require('./components/ExampleComponent.vue').default);
Vue.component('all-posts', require('./components/AllPosts.vue').default);
Vue.component('create-post', require('./components/CreatePost.vue').default);
/**
* Next, we will create a fresh Vue application instance and attach it to
* the page. Then, you may begin adding components to this application
* or customize the JavaScript scaffolding to fit your unique needs.
*/
const app = new Vue({
store,
el: '#app',
});
posts.blade.php:
#extends('layouts.app')
#section('content')
<div class="container">
<div class="row">
<div class="col-md-6">
<create-post />
</div>
<div class="col-md-6">
<all-post />
</div>
</div>
</div>
#endsection
Your component must be wrapped in an element with the id "app".
<div id="app">
<all-posts />
</div>
make sure that somewhere in your posts.blade or layouts.app you have this element surrounding the code that includes your component
Try to import the components and let your Vue instance know that there are custom components. In CreatePost.vue:
import { elUpload, elDialog } from '...'; // path to your components or lib
...
export default {
...
components: {
elUpload,
elDialog,
}
...
}