How can I bind directives to custom components in VueJS? - vue.js

So I am using vuetify with vue-cli and this is my current component code:
<template>
<div>
<v-row>
<v-col xl3 md3 xs12>
<strong>{{field}}</strong>
</v-col>
<v-col xl9 md9 xs12>
{{value}}
</v-col>
</v-row>
</div>
</template>
<script>
export default {
data() {
return {
}
},
props: ['field', 'value']
}
</script>
And I am using it in my templates like this
<template>
<two-column field="Some Field" value="Some Value"></two-column>
</template>
<script>
import TwoColumnRow from './vuetify_modifications/TwoColumnRow'
...
</script>
Now everything works perfectly but what if I want to make the grid sizes dynamic? Like for example I do with something like
<two-column field="Some Field" value="Some Value" sizes="xl3 md3 xs12"></two-column>
Is that possible? Thank you in advance.

How about this:
<foo :sizes="{ xl3: '', md3: '', xs12: '' }"></foo>
And:
<template>
<div>
<v-row>
<v-col v-bind="sizes">
<strong>{{field}}</strong>
</v-col>
</v-row>
</div>
</template>
<script>
export default {
props: {
sizes: { type: Object, default: () => {} }
// ...
}
}
</script>

One way I've been able to accomplish this is through the use of computed properties.
For simplicity of creating the example I've used colors to represent what is happening. Since it seems as through all you're really asking is how could you dynamically apply classes or value based conditions inside a component, this should work with some tweaks.
const TwoColumnRow = Vue.component('two-column', {
template: '#two-column-row-template',
data: function() {
return {}
},
props: ['field', 'value', 'colors'],
computed: {
colorList: function() {
// Split the string of colors by space and return an array of values
return this.colors.split(' ');
}
}
});
const vm = new Vue({
el: '#app-container',
data: {}
});
.red {
color: red;
}
.blue {
color: blue;
}
<script src="https://unpkg.com/vue#2.1.10/dist/vue.js"></script>
<div id="app-container">
<table>
<two-column field="toast" value="cheese" colors="blue red"></two-column>
</table>
</div>
<script type="x-template" id="two-column-row-template">
<tr>
<td v-bind:class="colorList[0]">{{field}}</td>
<td v-bind:class="colorList[1]">{{value}}</td>
</tr>
</script>
This runs, so you could insert some statements {{colorList}} inside the component to see what is being rendered.

Related

this.$refs.key is undefined inside method

I have the following component based on Vuetify card component:
CardTemplate
<template>
<HeightCalculator>
<template #default="{ height }">
<v-card>
<v-card-title ref="title">
{{ title }}
</v-card-title>
<v-card-text
class="overflow-y-auto"
:style="{ height: calcHeight(height) }"
>
<slot></slot>
</v-card-text>
</v-card>
</template>
</HeightCalculator>
</template>
<script>
import HeightCalculator from '../HeightCalculator.vue'
export default {
props: {
title: {
type: String,
required: true
}
},
components: {
HeightCalculator
},
methods: {
calcHeight (height) {
return `calc(${height} - ${this.$refs.title.clientHeight})`
}
}
}
</script>
NOTE: HeightCalculator is just a renderless component which returns a calculated height (in this case is going to be something like 50vh).
As you can see this.$refs.title is undefined therefore I cannot access to its .clientHeight. What am I doing wrong?

import dynamically fontawesome icons in Vue

I have a problem rendering icons dynamically. I use v-for to get all the data from the object array. Also, I have a second array where I save the name of the icons I worked with. However, when the first array is looping, the second array (icons) doesn't move.
I tried to create a method that maps the data from the first and second array to create a new array. But nothing happens.
My code:
Component.vue
<template>
<div class="items">
<div class="item" v-for="(param, index) in params" :key="index">
<font-awesome-icon :icon="['fab', 'temp']" :temp="getIcon" :key="index" class="fab fa" />
<h3 class="skills-title">{{ param.name }}.</h3>
<p style="display: none">{{ param.description }}.</p>
</div>
</div>
</template>
<script>
export default {
name: "PxSkillCard",
data() {
return {
params: [],
icons: ["laravel", "wordpress-simple"],
};
},
methods: {
getIcon() {
let temp = this.params.map((aux, index) => {
return [aux, this.icons[index]];
});
},
},
};
</script>
And I separated the fontawesome file in a apart module
fontawesome.js
import Vue from "vue";
import { library } from "#fortawesome/fontawesome-svg-core";
import {
faLaravel,
faWordpressSimple
} from "#fortawesome/free-brands-svg-icons";
import { faPlus } from "#fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "#fortawesome/vue-fontawesome";
library.add(
faLaravel,
faWordpressSimple
);
Vue.component("font-awesome-icon", FontAwesomeIcon);
The final result is:
What about with my code (or my logic)?
You are already looping through everything in your template, there's no need to loop again in your function.
Something like this should work.
<template>
<div class="items">
<div class="item" v-for="(param, index) in params" :key="index">
<font-awesome-icon :icon="['fab', icons[index]]" :key="index" class="fab fa" />
<h3 class="skills-title">{{ param.name }}.</h3>
<p style="display: none">{{ param.description }}.</p>
</div>
</div>
</template>
<script>
export default {
name: "PxSkillCard",
data() {
return {
params: [],
icons: ["laravel", "wordpress-simple"],
};
},
};
</script>
This assume, both arrays are the same size and the data in params and icons are in the correct order.

Copy text upon clicking on icon in v-text-field

I'm trying to figure out how to allow users to copy their login details when they click the copy icon. How to get the value of the relevant v-text-field?
I thought I should use #click:append and link it to a method. However, I struggle how to get a value.
<template>
<v-card class="col-12 col-md-8 col-lg-6 p-6 px-16" elevation="4">
<div class="title h2 mb-10 text-uppercase text-center">
Success
<v-icon color="green" x-large>
mdi-check-circle
</v-icon>
</div>
<v-text-field
:value="newAccount.login"
label="Login"
outlined
readonly
append-icon="mdi-content-copy"
#click:append="copy('login')"
></v-text-field>
<v-text-field
:value="newAccount.password"
label="Password"
outlined
readonly
append-icon="mdi-content-copy"
></v-text-field>
</v-card>
</template>
<script>
export default {
props: ["newAccount"],
data() {
return {
copied: false,
};
},
methods: {
copy(target) {
if (target === "login") {
console.log("login is clicked");
}
},
},
computed: {},
};
</script>
The value of the v-text-field is available from its value property. Apply a template ref on the v-text-field to get a reference to the component programmatically from vm.$refs, then use .value off of that:
<template>
<v-text-field
ref="login"
#click:append="copy('login')"
></v-text-field>
</template>
<script>
export default {
methods: {
copy(field) {
console.log('value', this.$refs[field].value)
}
}
}
</script>
Alternatively, you could access the nested template ref of v-text-field's <input>, which has a ref named "input", so copy() would access it from this.$refs[field].$refs.input. Then, you could select() the text value, and execute a copy command:
export default {
methods: {
copy(field) {
const input = this.$refs[field].$refs.input
input.select()
document.execCommand('copy')
input.setSelectionRange(0,0) // unselect
}
}
}
demo

Render Component in loop, use Index in method of child component (VueJS)

I have two components where some exchange of props takes place. Props is the whole todo array, which is updated by a click on the button with the "addTodo" method. Passing the array down to the child works fine. I can display the props dynamically in my p-tags, but it seems to be not possible to use it my the methods of my child component.
<template>
<v-app>
<v-content>
<h2>Add a Todo</h2>
<v-col cols="12" sm="6" md="3">
<v-text-field label="Regular" v-model="text"></v-text-field>
</v-col>
<div class="my-3">
<v-btn medium #click="addTodo">Add Todo</v-btn>
</div>
<div v-for="(todo, index) in todos" v-bind:key="index">
<HelloWorld
v-bind:todos="todos"
v-bind:index="index"
v-bind:class="(todos[index].done)?'green':'red'"
/>
</div>
</v-content>
</v-app>
</template>
<script>
import HelloWorld from "./components/ToDo.vue";
export default {
components: {
HelloWorld
},
data: function() {
return {
text: "",
todos: []
};
},
methods: {
addTodo() {
this.todos.push({
text: this.text,
done: false
});
}
}
};
</script>
This is my child component
<template>
<v-card max-width="250">
<v-card-text>
<h2 class="text-center">{{todos[index].text}}</h2>
<p class="display-1 text--primary"></p>
<p>{{index}}</p>
</v-card-text>
<v-card-actions>
<v-btn text color="deep-purple accent-4" #click="done"></v-btn>
<v-btn text color="orange accent-4">Delete Task</v-btn>
</v-card-actions>
</v-card>
</template>
<script>
export default {
props: ["todos", "index"],
methods: {
done() {
this.todos[1].text = "bla";
}
}
};
</script>
<style scoped>
.seperator {
display: flex;
justify-content: space-between;
}
</style>
I pass a whole array with objects as props, and using the index inside the p-tag works fine, but I also want to use it like this:
methods: {
done() {
this.todos[index].text = "bla";
}
}
'index' is not defined
Everything works fine, but I am not able use the index value inside the method. What am I doing wrong here?
The way you write it out, there is nothing in scope defining index. Where is that value coming from?
Index is a prop and so it must be referenced with this.
done () {
this.todos[this.index].text = 'bla'
}

Rendering a new Vue component after click

I am using Vue-CLI. I have a Vue component which is called viewGenres.vue. This component contains Vuetify table which presents all of the current genres from Vue store. I am trying to add a new option for each genre which is "Edit".
My objective is that for each line in the table there will be an edit button. Once the button is clicked, a new component called editGenre.vue should be rendered.
This component should contain a filled-out form with all the existing details of the specific genre.
I have several questions:
1) Once I click on the edit button, the following exception appears on browser:
ReferenceError: Vue is not defined at VueComponent.editGenre
2) In order for me to load the right properties from the DB, I need to define the "ID" prop of the editGenre component. Does anyone have any recommendation on the best method to do so?
This is viewGenres.vue: (the method editGenre is the one responsible for rendering the new component).
<template>
<div class="root" ref="container">
<h2>Genres Info</h2>
<br>
<v-data-table
:headers="headers"
:items="genres"
hide-actions
class="elevation-1">
<template slot="items" slot-scope="props">
<td class="text-xs-left">{{ props.item.id }}</td>
<td class="text-xs-left">{{ props.item.name }}</td>
<td class="text-xs-left">{{ props.item.desc }}</td>
<td class="text-xs-left">{{ props.item.artists }}</td>
<td class="text-xs-left"><v-btn flat #click="editGenre(props.item.id)">EDIT</v-btn></td>
<td class="text-xs-left"><v-btn flat #click="deleteGenre(props.item.id)">Delete</v-btn></td>
</template>
</v-data-table>
</div>
</template>
<script>
import editGenre from '#/components/administratorView/Genres/editGenre.vue'
const firebase = require('../../../firebaseConfig.js')
export default {
data: function(){
return{
headers: [
{ text: 'ID', value: 'id'},
{ text: 'Name', value: 'name'},
{ text: 'Description', value: 'desc'},
{ text: 'Artists', value: 'artists'},
{ text: 'Edit Genre'},
{ text: 'Delete From DB'}
]
}
},
computed: {
genres: function(){
return this.$store.state.genre.genres
}
},
components: {
editGenre
},
methods: {
editGenre: function(id){
var ComponentClass = Vue.extend(editGenre)
var instance = new ComponentClass()
instance.$mount()
this.$refs.container.appendChild(instance.$el)
},
deleteGenre: function(id){
console.log("Trying to delete " +id)
firebase.firestore.collection("genres").doc(id).delete().then(()=>{
this.$store.dispatch('genre/getGenresFromDB')
alert("Deleted Document Successfully")
}).catch(function(error){
alert(error)
})
}
},
mounted(){
this.$store.dispatch('genre/getGenresFromDB')
}
}
</script>
<style scoped>
</style>
This is editGenre.vue:
<template>
<v-dialog v-model="editGenre" persistent max-width="500px">
<v-card>
<v-card-title>
<h2>Edit Genre {{genre.name}}</h2>
</v-card-title>
<v-card-text>
<v-text-field
v-model="name"
label="Name"
:error-messages="nameErrors"
#touch="$v.name.$touch()"
#blur="$v.name.$touch()"
/>
<v-textarea
v-model="desc"
label="Description"
box
/>
<v-combobox
v-model="artists"
label="Artists"
:items="artistNames"
:error-messages="artistsErrors"
#touch="$v.artists.$touch()"
#blur="$v.artists.$touch()"
multiple>
</v-combobox>
<v-btn
color="primary"
#click="submit">
Submit
</v-btn>
<v-btn
color="primary"
#click="close">
Close
</v-btn>
</v-card-text>
</v-card>
</v-dialog>
</template>
<script>
import { required } from 'vuelidate/lib/validators'
const firebase = require('../../../firebaseConfig')
export default{
data: function(){
return{
name: '',
desc: '',
artists: []
}
},
props: {
id: String
},
mounted: function(){
let docRef = firebase.firestore.collection("genres").doc(this.id)
docRef.get().then(function(doc){
if(doc.exists){
this.name = doc.data().name
this.desc = doc.data().desc
this.artists = doc.data().artists
}
else{
console.error("Doc Doesn't Exist!")
}
}).catch(function(error){
console.error(error)
})
}
}
</script>
<style scoped>
</style>
Thank You!
Tom
You missed to import Vue in your viewGenres.vue component, so add it as follow :
....
<script>
import Vue from 'vue'
import editGenre from '#/components/administratorView/Genres/editGenre.vue'
const firebase = require('../../../firebaseConfig.js')
....
You could pass props by this way :
var ComponentClass = Vue.extend(
props:{
id:{type:String, default () { return id}}
},editGenre)
and remove this :
props: {
id: String
}
according to Evan You :
It's not recommended to use new to manually construct child components. It is imperative and hard to maintain. You probably want to make your child components data-driven, using and v-for to dynamically render child components instead of constructing them yourself.