v-on:click not working bootstrap - javascript

Initially I thought this was an issue with how I was using the #click directive according to this question. I added the .native to the directive and my method is still not getting invoked.
I know this is bootstrap as if I use a normal <button> then the method is invoked as expected.
There are no errors in the logs so it is just as if the element is not registering the directive?
UpcomingBirthdays.vue
<template>
<div>
<h1>{{ section_title }}</h1>
<b-card style="max-width: 20rem;"
v-for="birthday in birthdays.birthdays"
:key="birthday.name"
:title="birthday.name"
:sub-title="birthday.birthday">
<b-button href="#"
#click.native="toWatch(birthday, $event)"
variant="primary">Watch
</b-button>
</b-card>
</div>
</template>
<script>
import { mapState } from "vuex";
export default {
name: "UpcomingBirthdays",
data: () => {
return {
section_title: "Upcoming Birthdays",
};
},
methods: {
toWatch: (birthday, event) => {
event.preventDefault();
console.log("watched called");
console.log(birthday.name);
console.log(`BEFORE: ${birthday.watch}`);
birthday.watch = !birthday.watch;
console.log(`AFTER: ${birthday.watch}`);
}
},
computed: mapState([
"birthdays",
]),
};
</script>
<style>
</style>
EDIT
Worth mentioning that when using HTML5 <button>, I do not have to append the .native property to the #click directive.
EDIT 2
Here is my codesandbox I created to replicate this error. I would expect an error here to say BirthdaysApi is not defined but I am not getting anything when the button is clicked.

Just remove the href="#" from your buttons (this makes the Bootstrap b-button component render your buttons as anchors) and it's working as expected:
https://codesandbox.io/s/w0yj3vwll7
Edit:
Apparently this is intentional behaviour from the authors, a decision I disagree upon. What they are doing is apparently executing event.stopImmediatePropagation() so any additional listener isn't triggered.
https://github.com/bootstrap-vue/bootstrap-vue/issues/1146

Related

Vue JS emit event to child component from parent component not triggering

I'm working in a Nuxt JS project. I have several components, and one of my components called ComplianceOptoutRawText includes a child component called ComplianceOptoutViaExternalService, and it's this child component where I'm using a v-on to listen for an $emit event from the ComplianceOptoutRawText so I can toggle a variable, but I'm not seeing it at all in my console logs and need to know what I'm doing wrong and need to change.
Here's my markup with everything that's appropriate:
ComplianceOptoutRawText
<template>
<div>
<div>
<div class="tw-hidden md:tw-block tw-font-bold tw-mb-4">
<ComplianceOptoutViaExternalService>
<template #trigger>
<button #click="$emit('shown-optout-intent', true)" type="button">here</button>
</template>
</ComplianceOptoutViaExternalService>
</div>
</div>
</div>
</template>
And then the child component itself:
ComplianceOptoutViaExternalService
<template>
<div>
<slot name="trigger"></slot>
<article v-show="wantsToOptOut" v-on:shown-optout-intent="hasShownOptoutIntent" class="tw-bg-white tw-p-4 md:tw-p-6 tw-rounded-xl tw-text-gray-800 tw-border tw-border-gray-300 tw-text-left tw-mt-4">
<validation-observer v-if="!didOptOut" ref="optoutServiceForm" key="optoutServiceForm" v-slot="{ handleSubmit }" tag="section">
<form>
...
</form>
</validation-observer>
</article>
</div>
</template>
<script>
export default {
data () {
return {
wantsToOptOut: false,
isOptingOut: false,
didOptOut: false
}
},
methods: {
/*
** User has shown opt out intent
*/
hasShownOptoutIntent (value) {
this.wantsToOptOut = !this.wantsToOptOut
}
}
}
</script>
Note that my child component uses a slot, this is so I can position everything as needed in the parent component, but at it's core, I have a parent component emitting a value and then listening for it via v-on:shown-optout-intent="hasShownOptoutIntent" which runs the hasShownOptoutIntent method.
But I never see anything from the button, even if I console.log this. What am I missing?
I'm doing something similar with an embedded component, so it's perhaps worth trying:
<button #click="$emit('update:shown-optout-intent', true)" type="button">here</button>
... and then:
v-on:shown-optout-intent.sync="hasShownOptoutIntent"
As an aside, it's safe to remove v-on and use: :shown-optout-intent.

Vue2: Use form component with input type textarea to display AND edit data (without directly manipulating props)

I am building an MVP and this is the first time I do web development. I am using Vue2 and Firebase and so far, things go well.
However, I ran into a problem I cannot solve alone. I have an idea how it SHOULD work but cannot write it into code and hope you guys can help untangle my mind. By now I am incredibly confused and increasingly frustrated :D
So lets see what I got:
Child Component
I have built a child component which is a form with three text-areas. To keep it simple, only one is included it my code snippets.
<template>
<div class="wrap">
<form class="form">
<p class="label">Headline</p>
<textarea rows="2"
v-model="propHeadline"
:readonly="readonly">
</textarea>
// To switch between read and edit
<button
v-if="readonly"
#click.prevent="togglemode()">
edit
</button>
<button
v-else
type="submit"
#click.prevent="togglemode(), updatePost()"
>
save
</button>
</form>
</div>
</template>
<script>
export default {
name: 'PostComponent'
data() {
return {
readonly: true
}
},
props: {
propHeadline: {
type: String,
required: true
}
},
methods: {
togglemode() {
if (this.readonly) {
this.readonly = false
} else {
this.readonly = true
}
},
updatePost() {
// updates it to the API - that works
}
}
}
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
And my parent component:
<template>
<div class="wrap">
<PostComponent
v-for="post in posts"
:key="post.id"
:knugHeadline="post.headline"
/>
</div>
</template>
<script>
import PostComponent from '#/components/PostComponent.vue'
export default {
components: { PostComponent },
data() {
return {
posts: []
}
},
created() {
// Gets all posts from DB and pushes them in array "posts"
}
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
Current Status
So far, everything works. I can display all posts and when clicking on "edit" I can make changes and save them. Everything gets updated to Firebase - great!
Problem / Error Message
I get the following error message:
[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value.
As the error says I should use a computed property based on the props value. But how can I achieve that?
Solution Approach
I believe I have to use a computed getter to return the prop value - how to do that?
And then I have to use the setter to emit an event to the parent to update the value so the prop passes it back down - how to do that?
I have found bits and pieces online but by now all I see is happy families passing around small packages of data...
Would be really thankful for a suggestion on how to solve this one! :)
Thanks a lot!
This error shows because of your v-model on texterea which mutate the prop, but in vue it is illegal to mutate props :
<textarea rows="2"
v-model="propHeadline"
:readonly="readonly">
</textarea>
So, what you could do is to use this created() lifecycle hook and set the propHeadline prop as data :
<script>
export default {
name: 'PostComponent'
data() {
return {
readonly: true,
headline: ""
}
},
props: {
propHeadline: {
type: String,
required: true
}
},
created() {
this.headline = this.propHeadline
}
}
</script>
An then, update the new variable on your textarea :
<textarea rows="2"
v-model="headline"
:readonly="readonly">
</textarea>

Vue prevent event propagation to children conditionally

This is the example of ColorButtonGroup Vue component (template only) that serves as group of checkbox/toggle buttons and has limit of max. options selected (in this case 4 colors).
It uses ToggleButton component which works as simple toggle selection with styles apply and it's one of common components we have to use in projects.
<template>
<div
class="color-button-group"
:class="[typeClass, variationClass]">
<ToggleButton
v-for="(item, index) in items"
:key="index"
:color="item.color"
:type="type"
#click.native="validateClick"
#change="onChange(item.id)" />
</div>
</template>
I've implemented all logic needed through methods and event handlers and everything works fine but it's also possible to toggle buttons visually after max. selection reached.
Current behavior:
Desired behavior:
How to prevent event propagation to children element(s) conditionally?
stopPropagation and preventDefault as bubbling up and default action prevention were not helpful.
When max. colors selected, level bellow toggle button shouldn't be triggered (disabled state is not allowed for use).
Bottom ToggleButton component has to be modified by adding the new prop toggleable which is true by default and upper ColorButtonGroup controls when it will become false.
<template>
<div
class="color-button-group"
:class="[typeClass, variationClass]">
<ToggleButton
v-for="(item, index) in items"
:key="index"
:color="item.color"
:type="type"
:toggleable="isValidSelection()"
#change="onChange(item.id)" />
</div>
</template>
<script>
export default {
data() {
return {
selected: [],
};
},
methods: {
isValidSelection() {
if (this.selected.length < this.maxSelect)
return true;
return false;
},
onChange(item) {
if (this.isSelected(item)) {
this.selected = this.selected.filter(val => val !== item);
} else if (this.isValidSelection()) {
this.selected.push(item);
}
},
},
};
</script>

How to implement communication between two arbitrary elements in Vue?

I'm currently building an app using the Vue framework and came across a strange issue that I was unable to find a great solution for so far:
What I'm trying to do is add a class to a parent container in case a specific element inside the container (input, select, textarea etc.) gets focus. Here's the example code:
<div class="form-group placeholder-label">
<label for="desc"><span>Description</span></label>
<div class="input">
<input id="desc" type="text" />
</div>
</div>
In Vanilla JS of course, this is easily done:
const parent = document.querySelector('.placeholder-label');
const input = parent.querySelector('input');
input.addEventListener('focus', (e) => {
parent.classList.add('active');
});
In the same way, you could loop through all .placeholder-label elements and add the event to their child inputs/selects etc. to add this basic functionality. There are two moving parts here:
You don't know the type of the parent element, just that it has .placeholder-label on it.
You don't know the type of the child element, just that it is some sort of HTML form element inside the parent element.
Can I build a Vue component that toggles a class on a given parent element based on focus/blur of a given child element? The best I could come up with is use slots for the child elements, but then I still need to build a component for each parent. Even when using mixins for the reused parts it's still quite a mess compared to the five lines of code I need to write in pure JS.
My template:
<template>
<div
class="form-group"
:class="{ 'active': active }"
>
<label :for="inputID"><span>{{ inputLabel }}</span></label>
<slot
name="input"
:focusFunc="makeActive"
:blurFunc="makeInactive"
:inputLabel="inputLabel"
:inputID="inputID"
/>
</div>
</template>
<script>
export default {
name: 'TestInput',
props: {
inputLabel: {
type: String,
default: '',
},
inputID: {
type: String,
required: true,
},
},
// data could be put into a mixin
data() {
return {
active: false,
};
},
// methods could be put into a mixin
methods: {
makeActive() {
this.active = true;
},
makeInactive() {
this.active = false;
},
},
};
</script>
Usage:
<test-input
:input-i-d="'input-2'"
:input-label="'Description'"
>
<template v-slot:input="scopeVars">
<!-- this is a bootstrap vue input component -->
<b-form-input
:id="scopeVars.inputID"
:state="false"
:placeholder="scopeVars.inputLabel"
#blur="scopeVars.blurFunc"
#focus="scopeVars.focusFunc"
/>
</template>
</test-input>
I guess I'm simply missing something or is this a problem that Vue just can't solve elegantly?
Edit: In case you're looking for an approach to bubble events, here you go. I don't think this works with slots however, which is necessary to solve my issue with components.
For those wondering here are two solutions. Seems like I did overthink the issue a bit with slots and everything. Initially I felt like building a component for a given element that receives a class based on a given child element's focus was a bit too much. Turns out it indeed is and you can easily solve this within the template or css.
CSS: Thanks to #Davide Castellini for bringing up the :focus-within pseudo-selector. I haven't heard of that one before. It works on newer browsers and has a polyfill available.
TEMPLATE I wrote a small custom directive that can be applied to the child element and handles everything.
Usage:
v-toggle-parent-class="{ selector: '.placeholder-label', className: 'active' }"
Directive:
directives: {
toggleParentClass: {
inserted(el, { value }) {
const parent = el.closest(value.selector);
if (parent !== null) {
el.addEventListener('focus', () => {
parent.classList.add(value.className);
});
el.addEventListener('blur', () => {
parent.classList.remove(value.className);
});
}
},
},
},
try using $emit
child:
<input v-on:keyup="emitToParent" />
-------
methods: {
emitToParent (event) {
this.$emit('childToParent', this.childMessage)
}
}
Parent:
<child v-on:childToParent="onChildClick">
--------
methods: {
// Triggered when `childToParent` event is emitted by the child.
onChildClick (value) {
this.fromChild = value
}
}
use this pattern to set a property that you use to change the class
hope this helps. let me know if I misunderstood or need to better explain!

Jump to position on click (access class from other Vue component)

Explanation of problem
If a user clicks on the login link the view shall jump down to the login window where a user can type in userdata.
I am aware how to do this within a single file using document.getElementById('login-window').scrollIntoView()
However, I have a project with various single Vue.js component files. The login-link is within one "label" component. But the actual login-window is located in another component called "loginWindow", thus also the id / class "login-window" is stored in "loginWindow".
I tried to grab the "login-window" element with getElementById within my "label" component, but I believe it cannot access it since it is in another component.
This is the template code from "loginWindow"
<template>
<LoginGrid class="login-window" :as-grid="true" :class="classes" autocomplete="off">
<OCard class="login-card" :border="false">
<div class="login-headline f-copy f-bold l-color-primary">{{ t('headline') }}</div>
<!-- online state -->
<template v-if="isLogged">
<OButton color="secondary" #click="onClickLogout">{{ t('logout-label') }}</OButton>
</template>
<!-- offline state -->
<template v-else>
<div class="login-inputs">
<LoginInput
class="login-input-card-number" />
...
</div>
...
<OLink
type="secondary"
class="login-mode-link f-regular f-copy-small"
:no-router="true"
#click="onSwitchMode"
>
{{ modeLabel }}
</OLink>
...
</template>
</OCard>
</LoginGrid>
</template>
Here is what I've tried exactly
Within my "label" component I have implemented this method with an else-statement:
export default {
name: 'loginWindow',
...
methods: {
onClick() {
if (this.isLogged) {
...
} else {
if (isBrowser) {
document.getElementById("login-window").scrollIntoView();
}
}
},
},
}
So if the user is not logged-in, then onClick() it should scroll to the id of "login-window".
However, this solution does not work. I get an error saying "Cannot read property 'scrollIntoView' of null".
Any ideas how to do this with JavaScript within a Vue.js component?
login-window is Class not ID in your HTML. Try this:
document.querySelector(".login-window").scrollIntoView();

Categories

Resources