So while I'm learning vue, I wanted to double check if someone can show me what I'm doing wrong or lead me in the right answer. Below, I will show the code and then explain what I'm attempting to do.
Here is my Vue.js app:
Vue.component('o365_apps_notifications', {
template:
`
<div class="notification is-success is-light">
// Call the name here and if added/removed.
</div>
`,
});
new Vue({
name: 'o365-edit-modal',
el: '#o365-modal-edit',
components: 'o365_apps_notifications',
data() {
return {
list: {},
movable: true,
editable: true,
isDragging: false,
delayedDragging: false,
options: {
group: 'o365apps',
disabled: true,
handle: '.o365_app_handle',
}
}
},
methods: {
add(index, obj) {
console.log(obj.name);
this.$data.list.selected.push(...this.$data.list.available.splice(index, 1));
this.changed();
},
remove(index, obj) {
console.log(obj.name);
this.$data.list.available.push(...this.$data.list.selected.splice(index, 1));
this.changed();
},
checkMove(evt) {
console.log(evt.draggedContext.element.name);
},
},
});
Here is my modal:
<div id="o365-modal-edit" class="modal">
<div class="modal-background"></div>
<div class="modal-card px-4">
<header class="modal-card-head">
<p class="modal-card-title">Applications</p>
<button class="delete" aria-label="close"></button>
</header>
<section class="modal-card-body">
<div class="container">
<div id="o365-modal-edit-wrapper">
<div class="columns">
<div class="column is-half-desktop is-full-mobile buttons">
// Empty
</div>
<div class="column is-half-desktop is-full-mobile buttons">
// Empty
</div>
</div>
</div>
</div>
</section>
<footer class="modal-card-foot">
<o365-apps-notifications></o365-apps-notifications>
</footer>
</div>
</div>
Here is what I'm attempting to do:
Inside my modal, I have my o365_apps_notifications html tag called, my add() and remove() methods output a name on each add/remove using console.log(obj.name); and my checkMove method also drags the same name on drag as shown below:
How could I get my component to render and output the name inside the modal footer? I've tried all methods, but I can't seem to figure out how to trigger the component.
Also, would I have to do something special to make the component fade out after a set timeframe?
All help is appreciated!
A couple issues:
You've declared the notification component with underscores (o365_apps_notifications), but used hyphens in the modal's template. They should be consistent (the convention is hyphens).
The notification component is declared globally (with Vue.component), but it looks like you're trying to add it to the modal's components, which is intended for local components. Only one registration is needed (the global component registration should do).
<o365-apps-notifications>
The notification component should have public props that take the item name and state:
Vue.component('o365-apps-notifications', {
props: {
item: String,
isAdded: Boolean
},
})
Then, its template could use data binding to display these props.
Vue.component('o365-apps-notifications', {
template:
`<div>
{{ item }} {{ isAdded ? 'added' : 'removed '}}
</div>`
})
For the fade transition, we want to conditionally render this data based on a local Boolean data property (e.g., named show):
Vue.component('o365-apps-notifications', {
template:
`<div v-if="show">
...
</div>`,
data() {
return {
show: false
}
}
})
...and add the <transition> element along with CSS to style the fade:
Vue.component('o365-apps-notifications', {
template:
`<transition name="fade">
<div v-if="show">
...
</div>
</transition>`,
})
.fade-enter-active, .fade-leave-active {
transition: opacity .5s;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
To automatically fade out the data, add a watch on item, which sets show=true and then show=false after a delay:
Vue.component('o365-apps-notifications', {
watch: {
item(item) {
if (!item) {
return;
}
this.show = true;
clearTimeout(this._timer);
this._timer = setTimeout(() => this.show = false, 1000);
}
}
})
Usage
In the modal component, declare local data properties that hold the currently added/removed item:
new Vue({
el: '#o365-modal-edit',
data() {
return {
changedItem: null,
changedItemIsAdded: false,
}
},
})
Also update add() and remove() to set these properties:
new Vue({
methods: {
add(index, obj) {
this.changedItem = obj.name;
this.changedItemIsAdded = true;
},
remove(index, obj) {
this.changedItem = obj.name;
this.changedItemIsAdded = false;
},
},
})
Then in the modal component's template, bind these properties to the notification component's props:
<o365-apps-notifications :item="changedItem" :is-added="changedItemIsAdded"></o365-apps-notifications>
demo
Related
When on button click I want to refresh list of items.
Button is trigger on a sibling component.
Watch method only gets called once. But I need a constant refresh
Parent element.
<template>
<div class="container">
<Filter #changedKeywords="reloadItems"></Filter>
<List :platforms="platforms" :filters="keywords"></List>
</div>
</template>
<script>
imports...
export default {
name: "Holder",
components: {Filter, List},
methods: {
reloadItems: function (data){
if(data.keywords) {this.keywords = data.keywords};
}
},
data(){
return {
keywords : null,
}
}
}
</script>
I want to redraw child this element multiple times, on each (filter)button click
<template>
<section class="list">
<div class="container">
<div class="holder">
<Game v-for="data in list" :key="data.id" :data="data" />
</div>
</div>
</section>
</template>
<script>
import Game from "./Game";
export default {
name: "List",
props: ['filters', 'platforms'],
components: {Game},
data() {
return{
list: [],
}
},
watch: {
filters: async function() {
console.log('gets called only once!!!'); // this is where I want to fetch new items
const res = await fetch('/api/game/list/9', {
method: 'POST',
body: JSON.stringify({'filters' : this.filters})
});
this.list = await res.json();
}
},
}
</script>
When you're watching objects and arrays you need to use a deep watcher.
The Solution
watch: {
filter: {
deep: true,
async handler(next, previous) {
//your code here
}
}
}
The Reason
Javascript primitives are stored by value, but Objects (including Arrays which are a special kind of Object) are stored by reference. Changing the contents of an Object doesn't change the reference, and the reference is what is being watched. Going from null to some object reference is an observable change, but subsequent changes aren't. When you use a deep watcher it will detect nested changes.
I have a legacy codebase mostly built in PHP. I'm researching how to turn commonly used parts of the code into re-usable Vue components that can be plugged in as needed.
In one case, I have an onclick event in the html which will need to be individually passed to a child component. onclick="func()"
I want to be able to pass that func to the component from the markup, without defining this one-time use function as a property method either on the component or its parents.
I can't find anything in the Vue docs or elsewhere on how to do that. Every attempt I make gives an error:
Property or method "hi" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property. See: https://v2.vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.
Is there a way to pass externally-defined functions in the global scope to a Vue instance?
Vue tabs:
Vue.config.devtools = true;
Vue.component('tabs', {
template: `
<div class="qv-tabs">
<div class="tab">
<ul>
<li v-for="tab in tabs"
:class="{'is-active' : tab.isActive}"
#click="tab.callHandler"
>
<a :href="tab.href" #click="selectTab(tab)">{{tab.name}}</a>
</li>
</ul>
</div>
<div class="tab-content">
<slot></slot>
</div>
</div>
`,
data(){
return{
tabs: []
};
},
created(){
this.tabs = this.$children;
},
methods:{
selectTab(selectedTab){
this.tabs.forEach(tab => {
tab.isActive = (tab.name == selectedTab.name);
});
},
otherHi() {
alert('other hi');
}
}
});
Vue.component('tab', {
template: `
<div v-show="isActive">
<slot></slot>
</div>
`,
props: {
name: {required: true},
selected: {default: false},
callHandler: Function,
clickHandler: {
type: Function,
default: function() { console.log('default click handler') }
}
},
data(){
return{
isActive: false
}
},
methods: {
callHandler() {
console.log('call handler called');
this.clickHandler();
}
},
computed:{
href(){
return '#' + this.name.toLowerCase().replace(/ /g, '-');
}
},
mounted(){
this.isActive = this.selected;
}
});
new Vue({
el: '.app',
methods: {
vueHi() { alert('hi from vue'); }
}
});
function hi() {
alert('hi!');
}
Markup:
<div class="app">
<tabs>
<tab name="Tab 1" :selected="true" v-bind:call-handler="hi">
<p>Tab content</p>
</tab>
<tab name="Tab 2">
<p>Different content for Tab 2</p>
</tab>
</tabs>
</div>
You could define your methods like this in your component :
...
methods: {
hi
}
...
But you will have to define it in every component where you need this function. Maybe you can use a mixin that define the methods you want to access from yours components, and use this mixins in these components ? https://v2.vuejs.org/v2/guide/mixins.html
Anoter solution ( depending on what you try to achieve ) is to add your method to the Vue prototype like explained here :
https://v2.vuejs.org/v2/cookbook/adding-instance-properties.html
Vue.prototype.$reverseText = function(string) {
return string.split('')
.reverse()
.join('')
}
With this method defined in the Vue prototype, you can use the reverseText method like this in all of your components template :
...
<div> {{ $reverseText('hello') }} </div>
...
Or from yours script with this :
methods: {
sayReverseHello() {
this.$reverseText('hello')
}
}
On my app, I have multiple "upload" buttons and I want to display a spinner/loader for that specific button when a user clicks on it. After the upload is complete, I want to remove that spinner/loader.
I have the buttons nested within a component so on the file for the button, I'm receiving a prop from the parent and then storing that locally so the loader doesn't show up for all upload buttons. But when the value changes in the parent, the child is not getting the correct value of the prop.
App.vue:
<template>
<upload-button
:uploadComplete="uploadCompleteBoolean"
#startUpload="upload">
</upload-button>
</template>
<script>
data(){
return {
uploadCompleteBoolean: true
}
},
methods: {
upload(){
this.uploadCompleteBoolean = false
// do stuff to upload, then when finished,
this.uploadCompleteBoolean = true
}
</script>
Button.vue:
<template>
<button
#click="onClick">
<button>
</template>
<script>
props: {
uploadComplete: {
type: Boolean
}
data(){
return {
uploadingComplete: this.uploadComplete
}
},
methods: {
onClick(){
this.uploadingComplete = false
this.$emit('startUpload')
}
</script>
Fixed event name and prop name then it should work.
As Vue Guide: Custom EventName says, Vue recommend always use kebab-case for event names.
so you should use this.$emit('start-upload'), then in the template, uses <upload-button #start-upload="upload"> </upload-button>
As Vue Guide: Props says,
HTML attribute names are case-insensitive, so browsers will interpret
any uppercase characters as lowercase. That means when you’re using
in-DOM templates, camelCased prop names need to use their kebab-cased
(hyphen-delimited) equivalents
so change :uploadComplete="uploadCompleteBoolean" to :upload-complete="uploadCompleteBoolean"
Edit: Just noticed you mentioned data property=uploadingComplete.
It is easy fix, add one watch for props=uploadComplete.
Below is one simple demo:
Vue.config.productionTip = false
Vue.component('upload-button', {
template: `<div> <button #click="onClick">Upload for Data: {{uploadingComplete}} Props: {{uploadComplete}}</button>
</div>`,
props: {
uploadComplete: {
type: Boolean
}
},
data() {
return {
uploadingComplete: this.uploadComplete
}
},
watch: { // watch prop=uploadComplete, if change, sync to data property=uploadingComplete
uploadComplete: function (newVal) {
this.uploadingComplete = newVal
}
},
methods: {
onClick() {
this.uploadingComplete = false
this.$emit('start-upload')
}
}
})
new Vue({
el: '#app',
data() {
return {
uploadCompleteBoolean: true
}
},
methods: {
upload() {
this.uploadCompleteBoolean = false
// do stuff to upload, then when finished,
this.uploadCompleteBoolean = true
},
changeStatus() {
this.uploadCompleteBoolean = !this.uploadCompleteBoolean
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
<button #click="changeStatus()">Toggle Status {{uploadCompleteBoolean}}</button>
<p>Status: {{uploadCompleteBoolean}}</p>
<upload-button :upload-complete="uploadCompleteBoolean" #start-upload="upload">
</upload-button>
</div>
The UploadButton component shouldn't have uploadingComplete as local state (data); this just complicates the component since you're trying to mix the uploadComplete prop and uploadingComplete data.
The visibility of the spinner should be driven by the parent component through the prop, the button itself should not be responsible for controlling the visibility of the spinner through local state in response to clicks of the button.
Just do something like this:
Vue.component('upload-button', {
template: '#upload-button',
props: ['uploading'],
});
new Vue({
el: '#app',
data: {
uploading1: false,
uploading2: false,
},
methods: {
upload1() {
this.uploading1 = true;
setTimeout(() => this.uploading1 = false, Math.random() * 1000);
},
upload2() {
this.uploading2 = true;
setTimeout(() => this.uploading2 = false, Math.random() * 1000);
},
},
});
<script src="https://rawgit.com/vuejs/vue/dev/dist/vue.js"></script>
<div id="app">
<upload-button :uploading="uploading1" #click="upload1">Upload 1</upload-button>
<upload-button :uploading="uploading2" #click="upload2">Upload 2</upload-button>
</div>
<template id="upload-button">
<button #click="$emit('click')">
<template v-if="uploading">Uploading...</template>
<slot v-else></slot>
</button>
</template>
Your question seems little bit ambiguë, You can use watch in that props object inside the child component like this:
watch:{
uploadComplete:{
handler(val){
//val gives you the updated value
}, deep:true
},
}
by adding deep to true it will watch for nested properties in that object, if one of properties changed you ll receive the new prop from val variable
for more information : https://v2.vuejs.org/v2/api/#vm-watch
if not what you wanted, i made a real quick example,
check it out hope this helps : https://jsfiddle.net/K_Younes/64d8mbs1/
My app consists of:
A component named
<consl :output="output" #submit-to-vue><consl>
which contains an input that calls a submit() method when enter key is pressed.
<div>
<output v-html="output"></output>
<div id="input-line" class="input-line">
<div class="prompt">{{ prompt }}</div>
<div>
<input class="cmdline" autofocus
v-model.trim="command"
#keyup.enter="submit"
:readonly="submited" />
</div>
</div>
Then the method submit() emits an event #submit-to-vue to parent method submitv() that create an instance of the same component and adds it to the DOM.
//........
methods: {
submit: function () {
this.$emit('submit-to-vue')
this.submited = true
}
},
and
//......
methods: {
submitv: function () {
var ComponentClass = Vue.extend(consl)
var instance = new ComponentClass({
propsData: { output: this.output }
})
instance.$mount() // pass nothing
this.$refs.container.appendChild(instance.$el)
What I want to accomplish ?
I want to create a new consl component and add it to the DOM every time the old one is submited. (I want my app to emulate a terminal)
The problem
When submitted the new created component does not contain the #submit-to-vue event listener, which make it unable to recall the submitv() method.
Questions
How can I solve this problem ?
Is this the proper way to do things in VueJs or is there a more elegent way ?
In parent component, declare one data property=childs, it will includes all childs already created.
So once parent component receives the event=submit-to-vue, then add one new child to this.childs
Finally uses v-for to render these child components.
The trick: always consider the data-driven way, doesn't manipulate dom directly as possible.
below is one simple demo :
Vue.config.productionTip = false
Vue.component('child', {
template: `
<div>
<div>Label:<span>{{output}}</span></div>
<div>Value:<span>{{command}}</span></div>
<div id="input-line" class="input-line">
<div class="prompt">{{ prompt }}</div>
<div>
<input class="cmdline" autofocus
v-model.trim="command"
#keyup.enter="submit"
:readonly="submited" />
</div>
</div>
</div>`,
props: ['output'],
data() {
return {
submited: false,
command: ''
}
},
computed: {
prompt: function () {
return this.submited ? 'Already submitted, input is ready-only now' : ''
}
},
methods: {
submit: function () {
this.$emit('submit-to-vue')
this.submited = true
}
}
})
app = new Vue({
el: "#app",
data: {
childs: [{'output':'default:'}]
},
methods: {
addChild: function () {
this.childs.push({'output': this.childs.length})
}
}
})
<script src="https://unpkg.com/vue#2.5.16/dist/vue.js"></script>
<div id="app">
<div>
<ul>
<li v-for="(child, index) in childs" :key="index">
<child :output="child.output" #submit-to-vue="addChild()"></child>
</li>
</ul>
</div>
</div>
I am a newbie to Vue.js. I am trying to emit an event from my grand-child component (card) to child component (hand) and from hand to parent component (main):
card(emit play event) => hand(listen to play event and emit
card-play event) => main(listen to card-play event)
play event should trigger card-play event
In card component, I am emitting "play" event when the card is clicked, then in my hand component I am listening to "play" event so that I can emit "card-play" event to the parent (main). But neither events are emitted nor elements are working (button element is disabled).
If I call card component in my main component directly everything is working fine, but when I try to put another component (hand) between them nothing is working.
Here is my code:
new Vue({
name: 'game',
el: '#app',
data: state,
template: `
<div id="#app">
<card :def="testCard" #click.native="handlePlay2" />
<transition name='hand'>
<hand :cards="testHand" v-if="!activeOverlay" #card-play="testPlayCard" />
</transition>
</div>
`,
methods: {
testPlayCard(card) {
console.log('You played a card!');
},
handlePlay2() {
console.log('You played a card!');
}
},
created() {
this.testHand = this.createTestHand();
},
computed: {
testCard () {
return cards.archers
},
}
});
Here are components:
/* ----- CARD COMPONENT ----- */
Vue.component('card', {
props: ['def'],
template: `
<div class="card" :class="'type-' + def.type" v-on:click.native="firePlayEvent">
<div class="title">{{ def.title }}</div>
<img class="separator" src="svg/card-separator.svg" />
<div class="description">
<div v-html="def.description"></div>
</div>
<div class="note" v-if="def.note">
<div v-html="def.note"></div>
</div>
<button>bos</button>
</div>
`,
methods: {
firePlayEvent: function() {
this.$emit('play');
console.log("play event is emitted???")
}
},
});
/* ----- HAND COMPONENT ----- */
Vue.component('hand', {
props: ['cards'],
template: `
<div class="hand">
<div class="wrapper">
<!-- Cards -->
<card v-for="card in cards" :key="card.uid" :def="card.def" #play=handlePlay(card) />
</div>
</div>
`,
methods: {
handlePlay(card) {
this.$emit('card-play', card);
console.log("custom event card-play>>");
}
},
});
I am keeping all data in state.js:
// Some usefull variables
var maxHealth = 10
var maxFood = 10
var handSize = 5
var cardUid = 0
var currentPlayingCard = null
// The consolidated state of our app
var state = {
// World
worldRatio: getWorldRatio(),
// TODO Other things
turn: 1,
//
players: [
{ name : 'Humoyun' },
{ name : 'Jamshid' },
],
//
currentPlayerIndex: Math.round(Math.random()),
//
testHand: [],
//
activeOverlay: null,
//
}
given your github link, here is what I did to make it work:
First, there is an element in your CSS that disallow click event on a card. So, to trigger correctly the event, you need to remove the pointer-events: none, line 239 of your css file.
Then, you don't need the .native event modifier on the click on your card:
<div class="card" :class="'type-' + def.type" #click="firePlayEvent">
When those two updates are made, on click on a card, the console shows the following:
custom event card-play>>
ui.js:43 play event is emitted???
And the card is removed as needed.