Bootstrap 5 tooltip || popover - vue.js

I have a little problem with use tooltip or popover in vue app from bootstrap.
<template>
...
<i :title="person.jobTitle" class="fa fa-tag" data-bs-placement="left" data-bs-toggle="tooltip" #click="showDetails"></i>
...
</template>
method:
<script>
import {Popover} from "bootstrap";
...
methods: {
showDetails(event) {
new Popover(event.target)
}
}
...
</script>
Im 100% sure im doing something wrong :).
I have to click twice to showup popover, and it's not closing.
While I create example:
<button id="tolek" ref="tolek" title="aha" class="btn btn-secondary">
Popover
</button>
and in mounted:
new Popover(this.$refs.tolek)
it's all work beautiful.
Edit:
looks like create directive do the job:
<i title="Job Title" class="fa fa-tag" v-popover:click="person.jobTitle"></i>
directive:
directives: {
popover: {
beforeMount(el, binding) {
new Popover(el, {content: binding.value})
}
}
},

Don't mix the BS data-bs- attributes with Vue. Make sure to use the show() method after the popover is instantiated...
methods: {
showDetails(event) {
this.bsPopover = new Popover(event.target, {placement: 'left', trigger: 'click'})
this.bsPopover.show()
}
}
Demo

For Vue3 and Bootstrap 5 you could use Directives
My answer is inspired on the answer from Sergio Azócar and I used TypeScript instead of JavaScript.
Create file bootstrap.ts
import { Tooltip, Popover } from 'bootstrap'
export const tooltip = {
mounted(el: HTMLElement) {
const tooltip = new Tooltip(el)
}
}
export const popover = {
mounted(el: HTMLElement) {
const popover = new Popover(el)
}
}
In main.ts add
import { tooltip } from 'your_path/tooltip'
app
.directive('tooltip', tooltip)
.directive('popover', popover)
.mount("#app");
And use it
<button v-tooltip title="Hello world from a button!">
I am a button
</button>
<div v-popover title="Hello world from a div!" data-bs-content="And with some amazing content">
I am a div
</div>

Related

Open file dialogbox in vue composition API

I am trying to open the Select file dialog box when clicking on the button, It is possible using this.$refs.fileInput.click() in VUE, but this is not working in composition API.
Here is the code for reference: https://codepen.io/imjatin/pen/zYvGpBq
Script
const { ref, computed, watch, onMounted, context } = vueCompositionApi;
Vue.config.productionTip = false;
Vue.use(vueCompositionApi.default);
new Vue({
setup(context) {
const fileInput = ref(null);
const trigger = () => {
fileInput.click()
};
// lifecycle
onMounted(() => {
});
// expose bindings on render context
return {
trigger,fileInput
};
}
}).$mount('#app');
Template
<div id="app">
<div>
<div #click="trigger" class="trigger">Click me</div>
<input type="file" ref="fileInput"/>
</div>
</div>
Thank you.
Have you tried to access it using context.refs.fileInput.click();?
Don't forget that it's setup(props, context) and not setup(context).
Try my edit here: https://codepen.io/ziszo/pen/oNxbvWW
Good luck! :)
I'm working in Vue 3 CLI and have tried several different recommendations and found the following to be the most reliable.
<template>
<input class="btnFileLoad" type="file" ref="oFileHandler" #change="LoadMethod($event)" />
<button class="btn" #click="fileLoad">Load</button>
</template>
<script>
import {ref} from "vue";
export default{
setup(){
const hiddenFileElement = ref({})
return {hiddenFileElement }
}
methods:{
fileLoad() {
this.hiddenFileElement = this.$refs.oFileHandler;
this.hiddenFileElement.click();
},
}
}
</script>
<style>
.btn{ background-color:blue; color:white; }
.btnFileLoad{ display:none }
</style>
I also discovered in Chrome that if the call from the button element to the hidden file handler takes to long, an error message that reads "File chooser dialog can only be shown with a user activation." is displayed in the source view. By defining the hiddenFileElement in setup the problem went away.

How to include a local JS file into Vue template

I want to import a JS file to be run along with a template in browser. I tried this, but it didn't work because I need everything loaded before my script can run.
Let me show you the problematic vue file:
<template>
<div id="canvaspage">
<canvas id="canvas"></canvas>
<div id="buttonlist">
<h5>Select your action:</h5>
<div class="col">
<button id="btn1">JS file custom action 1</button>
<button id="btn2">JS file custom action 2</button>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'CanvasPage'
}
</script>
...
See that canvas and buttons on template? I want to interact with it using pure JS.
Here is an example of what the JS file is trying to do:
let canvas = document.getElementById('canvas')
let button1 = document.getElementById('btn1')
let button2 = document.getElementById('btn2')
canvas.addEventListener('click', () => {
console.log('Canvas clicked')
})
button1.addEventListener('click', () => {
console.log('Button 1 clicked')
})
button2.addEventListener('click', () => {
console.log('Button 2 clicked')
})
If I try the solution linked above, what happens is that 'canvas', 'button1' and 'button2' are all null, because JS cannot find them. How can I make it work on Vue?
I don't see a reason- in this example- why you want to do anything in external js file, why not just interact with dom the vue way - I mean, proper way? Vue can destroy or replace your element with any v-if or rerender action. You can always link to your elements with this.$refs if you want to interact with DOM directly which is lots better than querySelector thingy. But anyway, here's a dummy example:
// external js file - ./extfile.js
export function canvasClick(...args) {
console.log('canvas clicked with: ', args);
}
export function button1Click(...args) {
console.log('button1 clicked with: ', args);
}
export function button2Click(...args) {
console.log('button2 clicked with: ', args);
}
// vue component
<template>
<div id="canvaspage">
<canvas id="canvas" #click="canvasAction"></canvas>
<div id="buttonlist">
<h5>Select your action:</h5>
<div class="col">
<button id="btn1" #click.prevent="button1Action">JS file custom action 1</button>
<button id="btn2" #click.prevent="button2Action">JS file custom action 2</button>
</div>
</div>
</div>
</template>
<script>
import { canvasClick, button1Click, button2Click } from './extfile';
export default {
name: 'CanvasPage',
methods: {
canvasAction(event) { canvasClick(event, this) },
button1Action(event) { button1Click(event, this) },
button2Action(event) { button2Click(event, this) },
}
}
</script>
Objects managed by Vue are create/destroyed according to Vue' lifecycle. This means that any external code you use to query vue-managed elements should be somewhat coupled to Vue's lifecycle.
This means that, ideally, you should use Vue itself to add the behaviour you want. You should, for instance, add this new function you want into a Vue component. This guarantees a simpler design.
Alternative: If the Vue components are from third-parties, perhaps from another team which you can't count on, you could hook those event listeners to the document and check the target's id attribute instead of hooking the event listeners directly to the canvas element (which may be destroyed by Vue and the hooks lost).
document.body.addEventListener('click', (event) => {
switch (event.target.id) {
case 'canvas':
console.log('Canvas clicked');
break;
case 'btn1':
console.log('Button 1 clicked');
break;
case 'btn2':
console.log('Button 2 clicked');
break;
}
}, true);
This code makes it very obvious that if you have more than one element in the DOM with those IDs, all of them will trigger the code.
Demo:
const CanvasComponent = Vue.component('canvas-component', {
template: `#canvas-component`,
});
const BlankComponent = Vue.component('blank-component', {
template: `<div><h3>Now click back to canvas and see that the listeners still work.</h3></div>`,
});
var router = new VueRouter({
routes: [{
path: '/',
component: {template: '<div>Click one link above</div>'}
},{
path: '/blank',
component: BlankComponent,
name: 'blank'
},
{
path: '/canvas',
component: CanvasComponent,
name: 'canvas'
}
]
});
var app = new Vue({
el: '#app',
router: router,
template: `
<div>
<router-link :to="{name: 'canvas'}">canvas</router-link> |
<router-link :to="{name: 'blank'}">blank</router-link>
<router-view></router-view>
</div>
`
});
document.body.addEventListener('click', (event) => {
switch (event.target.id) {
case 'canvas':
console.log('Canvas clicked');
break;
case 'btn1':
console.log('Button 1 clicked');
break;
case 'btn2':
console.log('Button 2 clicked');
break;
}
}, true);
<script src="//unpkg.com/vue#2.6.9/dist/vue.min.js"></script>
<script src="//unpkg.com/vue-router#3.1.3/dist/vue-router.min.js"></script>
<div id="app">
<canvas-component></canvas-component>
</div>
<template id="canvas-component">
<div id="canvaspage">
<canvas id="canvas"></canvas>
<div id="buttonlist">
<h5>Select your action:</h5>
<div class="col">
<button id="btn1">JS file custom action 1</button>
<button id="btn2">JS file custom action 2</button>
</div>
</div>
</div>
</template>

How to control the flow of vue application

I'm trying to make a game using vue and I designed the following structure:
<template>
<div v-if="status==='beforegamestart'" key="beforegamestart">
<button v-on:click="startGame()">Start</button>
</div>
<div v-else-if="status==='inprocess'" key="inprocess">
<h1>Game in process</h1>
<button v-on:click="finishGame()">Finish</button>
</div>
<div v-else-if="status==='gameover'" key="gameover">
<h2>Game over</h2>
</div>
<div v-else>
<h1>Else</h1>
</div>
</template>
<script>
export default {
name: 'GameComponent',
data() {
return {
status: 'beforegamestart'
}
},
methods: {
startGame: function() {
this.status = "inprocess";
},
finishGame: function() {
this.status = "gameover";
}
}
}
</script>
<style>
</style>
I wonder what is the common way to make a game like flappy bird in vue, which has a start menu, game in process, and a finish view? Is it OK to do it in this way? I just wonder how to change different views properly.
If I was was you, I'd do the following:
Step 1: Split the start, in-progress, and game-over screens into their own components. and import them into App.vue(or whatever you want) like so:
import Start from './Start.vue'
import Game from './Game.vue'
import GameOver from './GameOver.vue'
Step 2: Use a dynamic component in the template. See https://v2.vuejs.org/v2/guide/components.html#Dynamic-Components
<component :is="currentState"/>
Step 3: Have a method/computed property(or for bonus points, use vuex) that handles the current state of the game and have to components render based on that value.
computed: {
currentState: function() {
switch(status){
case "beforeGameStart":
return "Start";
break;
...
}
}
}

VueJS - trigger Modal from materializecss

I am trying to trigger a modal from the materializecss framework within a VueJS-instance.
Both, VueJS and Materializecss, are implemented correct. On their own both frameworks work fine.
Clicking the open-button results in an error:
Uncaught TypeError: data[option] is not a function
at HTMLDivElement. (adminarea.js:24562)
at Function.each (adminarea.js:10567)
at jQuery.fn.init.each (adminarea.js:10356)
at jQuery.fn.init.Plugin [as modal] (adminarea.js:24556)
at Vue$3.showLoader (adminarea.js:21396)
at boundFn (adminarea.js:54956)
at HTMLButtonElement.invoker (adminarea.js:56467)
This is my Vue-Instance:
const app = new Vue({
el: '#app',
data: {
activeUser: {
username: '',
email: ''
},
},
methods: {
showLoader(){
$('#loaderModal').modal('open');
},
closeLoader(){
$('#loaderModal').modal('close');
}
},
mounted() {
// Get current User
axios.get('/api/currentUser')
.then(response => {
this.activeUser.username = response.data.username;
this.activeUser.email = response.data.email;
});
},
components: {
Admindashboard
}
});
And here is the part of my html-file with the modal structure:
<!-- Modal Structure -->
<div id="loaderModal" class="modal">
<div class="modal-content">
<h4>Fetching data..</h4>
<div class="progress">
<div class="indeterminate"></div>
</div>
</div>
</div>
<button class="btn cyan waves-effect waves-cyan" v-on:click="showLoader">Open</button>
Any ideas? Thanks!
It seems I found an solution:
Nice to know for Laravel-users: for my current project I use Laravel 5.5 with Materializecss, VueJS and VueRouter but I think the solution is universal. Materializecss was installed via npm and has to be included into your application. I've required the css-framework within my ressources/assets/js/bootstrap.js:
...// more code
try {
window.$ = window.jQuery = require('jquery');
window.materialize = require('materialize-css');
} catch (e) {
console.log(e);
}
...// more code
Now you have to initialize the Modal-function on the mounted-event of your wrapping Vue-instance:
const app = new Vue({
router,
data: {
...
},
methods: {
testClick: function(){
console.log('Testklick-Call');
$('#modal1').modal('open');
}
},
mounted: function(){
console.log('Instance mounted');
$('.modal').modal();
}
}).$mount('#app');
The code above is placed within my ressources/assets/js/app.js and is packed by default by Laravel Mix but I think this is universal and also usable without Laravel Mix/Webpack etc.
Now you can call every modal programmatically from where ever you want. I've tested it in my main instance on a click-event. Function is placed in my Vue-instance (see above). HTML-Code see below:
<button v-on:click="testClick">Open Modal</button>
But you can also make use of the modal within a mounted-function or any other function of any component:
<template>
<div>
<p>I am an component!</p>
</div>
</template>
<script>
export default {
mounted() {
console.log('Component mounted!');
$('#modal1').modal('open');
}
}
</script>
This also works, if the component becomes only visible after clicked on a link (using VueRouter).
Hopefully this helps someone except me :)
As suggested here, you need to add following code in the mounted block:
mounted() {
$('#loaderModal').modal(); //New line to be added
// Get current User
axios.get('/api/currentUser')
.then(response => {
this.activeUser.username = response.data.username;
this.activeUser.email = response.data.email;
});
},

Handle Bootstrap modal hide event in Vue JS

Is there a decent way in Vue (2) to handle a Bootstrap (3) modal hide-event?
I found this as a JQuery way but I can't figure out how to capture this event in Vue:
$('#myModal').on('hidden.bs.modal', function () {
// do something…
})
Adding something like v-on:hide.bs.modal="alert('hide') doesn't seem to work.
Bootstrap uses JQuery to trigger the custom event hidden.bs.modal so it is not easily caught by Vue (which I believe uses native events under the hood).
Since you have to have JQuery on a the page to use Bootstrap's native modal, just use JQuery to catch it. Assuming you add a ref="vuemodal" to your Bootstrap modal you can do something like this.
new Vue({
el:"#app",
data:{
},
methods:{
doSomethingOnHidden(){
//do something
}
},
mounted(){
$(this.$refs.vuemodal).on("hidden.bs.modal", this.doSomethingOnHidden)
}
})
Working example.
Please see https://bootstrap-vue.js.org/docs/components/modal#overview
There you can find event "hide" or "hidden"
So you can bind this event:
<b-modal ref="someModal" #hide="doSometing">
One option is to tie it to a variable:
data: function(){
return {
showModal: false
//starts as false. Set as true when modal opens. Set as false on close, which triggers the watch function.
},
watch: {
showModal: function(){
if(this.showModal == false){
// do something
},
}
HTML
<button id="show-modal" #click="showModal = true">Show Modal</button>
//later if using a component
<modal v-if="showModal" #close="showModal = false">
// or alternatively in the bootstrap structure
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal" #click="showModal = false">Close</button>
</div>
This may be late but another way if you are using a custom modal component (Modal.vue) you have created is to
create a method in mounted to catch the event of closure (doesn't have to be the same name as below)
mounted: function(){
this.triggerHidden();
}
create the method
methods: {
triggerHidden: function(){
var self = this;
if( $('#getModal').length ){
$('#getModal').on('hidden.bs.modal', function(){
//catch the native bootstrap close event and trigger yours
self.#emit('modal-close');
});
}
}
}
now call use your custom event with your custom/reusable modal component
<custom-modal #modal-close="doSomething"></custom-modal>
The method doSomething will be called when the modal closes. You can also use the approach to hijack the other jquery event so its a little more manageable.
Maybe creating a Custom Vue Directive can help:
Vue.directive('bsevent', {
bind: function bsEventCreate(el, binding, vnode) {
let method = binding.value || (() => { });
$(el).on(binding.arg.replaceAll(/_/g, "."), (event) => { method(event); });
},
unbind(el, binding) {
$(el).off(binding.arg.replace(/_/, "."));
},
});
And then just use it on the element you wish (this example is on a bootstrap collapsible, but you could use it to any other bootstrap event):
<div id="myCollapsible" class="collapse" v-bsevent:hidden_bs_collapse="methodToCall">
...
</div>
The only thing to remember is to register the event with underscores instead of dots (show.bs.modal => show_bs_modal).
If working with bootstrap-vue then below code snippet will be helpful:
export default {
mounted() {
this.$root.$on('bv::modal::hide', (bvEvent, modalId) => {
console.log('Modal is about to be shown', bvEvent, modalId)
})
}
}
for other events please refer to the official docs.
Just use native addEventListener (Vue 3, Composition API)
template:
<div ref="modalElement" class="modal">
...
</div>
script:
import { Modal } from "bootstrap"
import { onMounted, ref } from "vue";
const modalElement = ref(null)
let modal = null;
onMounted(() => {
modal = new Modal(modalElement.value)
modalElement.value.addEventListener("hidden.bs.modal", onHidden)
})
function onHidden() {
// do something…
}
We can also use this simple approach like this example
<template>
<div>
<button #click="openModal = true">Open Modal</button>
<div v-if="openModal">
<div class="modal-background"></div>
<div class="modal-content">
<button #click="openModal = false">Close Modal</button>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
openModal: false
}
}
}
</script>