I'm trying to listen to a scroll event inside the specific element but it doesn't seem to be working inside the created function, below is my attempt and here's the fiddle.
Try to un-comment the scroll event outside the Vue instance and you'll know
new Vue({
el : '#vueapp',
data : {
showSearchBar : 0
},
created(){
const _self = this;
document.querySelector('#app').onscroll = function() {
alert();
if(document.querySelector('#app').scrollTop >= 10) {
_self.showSearchBar = 1;
}else{
_self.showSearchBar = 0;
}
}
// --
}
});
While moving the event handler to mounted (as suggested by Sovalina in the comments) works, it isn't the Vue way to do this.
With Vue, you should embed the event handler information inside the html template instead:
(Basing this on the code you posted on jsfiddle)
<div id="vueapp">
<div id="app" #scroll="handleScroll">
</div>
</div>
JavaScript:
new Vue({
el : '#vueapp',
data : {
showSearchBar : 0
},
methods: {
handleScroll(evt) {
if(evt.target.scrollTop >= 10) {
this.showSearchBar = 1;
}else{
this.showSearchBar = 0;
}
}
},
});
Related
Is there a way to add a click handler inside a Vue instance without writing on the markup? I'm loading an SVG via ajax, and I'd like to use Vue click events on it.
My Vue file looks like this:
<template>
<div>
<div class="map" v-html="map"></div>
</div>
</template>
<script>
export default{
data : function(){
return {
map : 'Loading'
};
},
methods : {
getMap : function(){
var thisMap = this;
axios.get('/img/world-map.svg').then(function (response) {
thisMap.map = response.data;
thisMap.loading = false;
}).catch(function (error) {
thisMap.loading = false;
console.log(error);
});
},
},
mounted : function(){
console.log('WorldMap mounted');
this.getMap();
}
}
</script>
The issue is that I'm loading a rather large SVG straight into the page. In a perfect world I would just do something like this:
<template>
<div>
<div class="map" >
<svg>
<g #click="groupClicked" class="asia" id="asia" xmlns:svg="http://www.w3.org/2000/svg">
<path stroke="#FFFFFF" d="M715.817,47.266c-4.45,1.25-8.903,2.497-13.357,3.739c-1.074,0.327-8.403,1.757-5.678,3.204
c-1.922,2.104-2.993,1.568-5.547,1.536c-1.547,1.333,0.981,1.22-0.558,2.421c-0.976,0.761-0.946,1.257-2.106,0.827
c-0.368-0.136-2.223-0.261-1.543,0.759c2.082,1.3,0.231,3.046-1.466,4.011c-1.831-0.38-3.271-1.611-5.245-1.293
c-1.229,0.196-2.104,0.763-3.176-0.205c-1.265-1.143,0.371-1.409,1.378-2.177c1.529-1.168,5.473-0.2,2.834-2.668
c1.061-0.979,2.07-0.946,3.206-1.736c-0.297-0.416-0.649-0.773-1.067-1.068c1.047-1.075,1.679-3.036,3.497-2.725
c1.441,0.249,2.046-1.318,3.182-2.137c1.121-0.811,2.4-1.266,3.771-1.402c1.656-0.165,3.271,0.134,4.347-1.427
c0.921-1.334,1.921-1.218,3.468-0.757c1.687,0.504,2.808-0.159,4.442-0.698c2.313-0.118,4.489-0.946,6.812-1.068
c1.043-1.941,2.354-2.07,4.375-2.331c0.653-0.085,6.433-0.678,4.774,1.792C721.041,46.198,718.024,46.605,715.817,47.266
C711.364,48.516,718.356,46.505,715.817,47.266z"/>
</g>
</svg>
</div>
</div>
</template>
However the SVG I'm loading is about 300kb big and I don't want to be carrying that around with all my JavaScript on every page load.
Any comments or solutions welcome.
Edit
Since asking I've actually got quite far with this approach, which isn't perfect but at the moment it seems pretty good.
var vueBus = new Vue({});
$('body').on('click', 'svg g', function(){
var name = $(this).attr('data-name');
vueBus.$emit('svgGroupClicked', name);
});
and then adding a listener in my .vue file
mounted() : function(){
vueBus.$on('svgGroupClicked', function(){ ... });
}
You can just add a click event via plain javascript after the GET request succeeds:
axios.get('/img/world-map.svg').then(function (response) {
thisMap.map = response.data;
thisMap.addClickHandler();
thisMap.loading = false;
})
Your addClickHandler method would look something like this:
methods: {
addClickHandler: function() {
var gEl = this.$el.getElementsByTagName('g')[0];
gEl.addEventListener('click', function() {
alert('I got clicked');
});
},
}
Or, if you're using jQuery:
methods: {
addClickHandler: function() {
$(this.$el).find('g')[0].click(function() {
alert('I got clicked');
});
},
}
I have a Vue app (and I'm relatively new to Vue), anyway I have a generic error modal which is displayed when any of my axios calls fail.
On the modal, I want to be able to retry the failed process when the 'Retry' button is clicked but I'm struggling a bit on how to achieve this. I don't think props will help me as the modal is triggered by
VueEvent.$emit('show-error-modal')
I have managed in my catch to pass the function which has failed by using
VueEvent.$emit('show-error-modal', (this.test));
Then in my modal, I have access to it using
created() {
VueEvent.$on('show-error-modal', (processFailed) => {
console.log('processFailed', processFailed)
this.processFailed = processFailed;
$('#errorModal').modal('show').on('shown.bs.modal', this.focus);
});
}
Using 'F12' it gives
test: function test() {
var _this2 = this;
var page = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
alert('boo');
this.loading = true;
var order_by = {
'ho_manufacturers.name': 4,
'services.servicename': 4
};
axios.post('/test/url', {
id: this.selectedManufacturers,
page: page,
order_by: order_by
}).then(function (response) {
var paginator = response.data.paginator;
_this2.$store.dispatch('paginateProducts', paginator);
_this2.$store.dispatch('setProductPaginator', paginator);
_this2.loading = false;
})["catch"](function (error) {
var processFailed = 'fetchProducts';
_this2.loading = false;
VueEvent.$emit('show-error-modal', _this2.test);
console.log(error);
});
},
I don't think this is the right way of doing it though as all this. are replaced with _this2 as shown above. I need the modal to be generic so I can reuse it but I can't figure it out how to retry the process when clicking the button.
The modals are registered in my app.vue file as
<template>
<div>
<!-- Other code here -->
<app-error-modal />
</div>
</template>
<script>
// Other code here
import ErrorModal from './components/ErrorModal';
export default {
name: 'App',
components: {
// Other code here
appErrorModal: ErrorModal
}
// Other code here
</script>
Modal button HTML
<button ref="retryButton" class="btn btn-success col-lg-2" type="submit" data-dismiss="modal" #click="retryProcess">Retry</button>
Modal script code
<script>
export default {
name: "ErrorModal",
created() {
VueEvent.$on('show-error-modal', (processFailed) => {
console.log('processFailed', processFailed)
this.processFailed = processFailed;
$('#errorModal').modal('show').on('shown.bs.modal', this.focus);
});
},
methods: {
focus() {
this.$refs.retryButton.focus();
},
retryProcess() {
//this.$parent.test(); TRIED THIS BUT DIDN'T WORK
}
}
}
</script>
I'd rather not have to the store.
Use custom event on your component
<div id="app">
<error-modal v-if="isError" v-on:retry-clicked="retry"></error-modal>
<button #click="isError = true">Make Error</button>
</div>
const errorModal = {
template : "<button #click=\"$emit('retry-clicked')\">Retry</button>"
}
new Vue({
el : "#app",
data : {
isError : false
},
components : {
errorModal
},
methods : {
retry : function(){
this.isError = false;
console.log("child component has called parent when retry clicked")
}
}
})
Custom event on component - VUEJS DOC
Everything you have there looks correct. You are passing the function to retry ("test") as "processFailed" - I would call that something different such as retryFn.
Then in your error modal component you just need:
<button #click="processFailed">Retry</button>
Don't worry about what the browser shows you in F12, that is the transpiled Javascript, it will work fine.
I need global variables for errors. But I don't want set input variable for every component.
How I can watch $errors in component ABC without input variable?
(without <abc :errors="$errors"></abc>)
index.js:
Vue.prototype.$errors = {};
new Vue({
el: '#app',
render: h => h(App),
}
App.vue:
...
name: 'App',
components: {
ABC
}
...
methods:{
getContent() {
this.$errors = ...from axis...
}
Component ABC:
<template>
<div>{{ error }}</div>
</template>
...
watch: {
???
}
Here's an example of how it could be done:
const errors = Vue.observable({ errors: {} })
Object.defineProperty(Vue.prototype, '$errors', {
get () {
return errors.errors
},
set (value) {
errors.errors = value
}
})
new Vue({
el: '#app',
methods: {
newErrors () {
// Generate some random errors
const errors = {}
for (const property of ['name', 'type', 'id']) {
if (Math.random() < 0.5) {
errors[property] = 'Invalid value'
}
}
this.$errors = errors
}
}
})
new Vue({
el: '#app2',
watch: {
$errors () {
console.log('$errors has changed')
}
}
});
<script src="https://unpkg.com/vue#2.6.10/dist/vue.js"></script>
<div id="app">
<pre>{{ $errors }}</pre>
<button #click="newErrors">New errors</button>
</div>
<div id="app2">
<pre>{{ $errors }}</pre>
</div>
I've created two Vue instances to illustrate that the value really is shared. Clicking the button in the first instance will update the value of $errors and the watch is triggered in the second instance.
There are a few tricks in play here.
Firstly, reactivity can only track the reading and writing of properties of an observable object. So the first thing we do is create a suitable object:
const errors = Vue.observable({ errors: {} })
We then need to wire this up to Vue.prototype.$errors. By defining a get and set for that property we can proxy through to the underlying property within our observable object.
All of this is pretty close to how data properties work behind the scenes. For the data properties the observable object is called $data. Vue then uses defineProperty with get and set to proxy though from the Vue instance to the $data object, just like in my example.
as Estradiaz said:
You can use Vuex and access the value outside of Vue like in this answer: https://stackoverflow.com/a/47575742/10219239
This is an addition to Skirtles answer:
You can access such variables via Vue.prototype.variable.
You can set them directly, or use Vue.set, it works either way.
My code (basically the same as Skirtless):
main.js
const mobile = Vue.observable({ mobile: {} });
Object.defineProperty(Vue.prototype, '$mobile', {
get() { return mobile.mobile; },
set(value) { mobile.mobile = value; }
});
function widthChanged() {
if (window.innerWidth <= 768) {
if (!Vue.prototype.$mobile) Vue.set(Vue.prototype, '$mobile', true);
} else if (Vue.prototype.$mobile) Vue.set(Vue.prototype, '$mobile', false);
}
window.addEventListener("resize", widthChanged);
widthChanged();
Home.vue:
watch: {
'$mobile'(newValue) {
// react to Change in width
}
}
We're in the process of retrofitting a mature website with some updated forms. We're replacing some systems entirely with SPAs and some just don't warrant it.
However, we have some system-global modal screens that we need to be available. Some of these have been ported to Vue and work well enough inside SPAs and we'd like to reap the benefits of heightened interactivity in our non-SPA pages.
So whereas before we would have had our event bound like so:
$('a.newBooking').on('click', function(ev){
// code to open bootstrap modal screen, download HTML, etc.
});
How do we spin up a component?
I know I haven't included any serious code here but our modals are basically just embellished versions of the documented example modal component. The difference is we don't have the button. We'll want to be able to launch these modals from all around the page.
My opinion:
For your Modal components:
use singleton pattern for your modal (because basically we only allow one modal popup at the same time), it will make the logic more simple.
customize one install function to add the Vue instances of your Modals to Vue.prototype, like _Vue.prototype.$my = yourModals
then register your plugins in demand like Vue.use(installFunc, {plugins: [SModal, AModal, BModal]})
At your JQuery (or other non-Vue) Apps:
Register Modals to Vue, then create Vue instance
show or hide your modals like vueInstance.$my.SModal.show
Below is one simple demo:
Vue.config.productionTip = false
/*---Modal Plugin---*/
let vm = null // the instance for your Vue modal
let timeout = null //async/delay popup
const SModal = {
isActive: false,
show ({
delay = 500,
message = '',
customClass = 'my-modal-class'
} = {}) {
if (this.isActive) {
vm && vm.$forceUpdate()
return
}
timeout = setTimeout(() => {
timeout = null
const node = document.createElement('div')
document.body.appendChild(node)
let staticClass = ''
vm = new this.__Vue({
name: 's-modal',
el: node,
render (h) { // uses render() which is a closer-to-the-compiler alternative to templates
return h('div', {
staticClass,
'class': customClass,
domProps: {
innerHTML: message
}
})
}
})
}, delay)
this.isActive = true
},
hide () {
if (!this.isActive) {
return
}
if (timeout) {
clearTimeout(timeout)
timeout = null
} else {
vm.$destroy()
document.body.removeChild(vm.$el)
vm = null
}
this.isActive = false
},
__Vue: null,
__installed: false,
install ({ $my, Vue }) {
if (this.__installed) { return }
this.__installed = true
$my.SModal = SModal // added your SModal object to $my
this.__Vue = Vue //get the Vue constructor
}
}
/*---Modal Plugin End---*/
/*---Custom Install Function in order to manage all modals---*/
let installFunc = function (_Vue, opts = {}) {
if (this.__installed) {
return
}
this.__installed = true
const $my = {
'memo': 'I am a plugin management.'
}
if (opts.plugins) {
Object.keys(opts.plugins).forEach(key => {
const p = opts.plugins[key]
if (typeof p.install === 'function') {
p.install({ $my, Vue: _Vue })
}
})
}
_Vue.prototype.$my = $my
}
/*---Install Plugins---*/
Vue.use(installFunc, {
plugins: [SModal]
})
let globalVue = new Vue({
el: '#vue-app'
})
$('#test').on('click', 'span', function () {
globalVue.$my.SModal.isActive ? globalVue.$my.SModal.hide() : globalVue.$my.SModal.show({'message':'test', 'delay':100})
})
span {
cursor:pointer;
color:red;
}
.my-modal-class {
position:absolute;
top:50px;
left:150px;
width:200px;
height:200px;
background-color:red;
z-index:9999;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="vue-app">
</div>
<div id="test">
<h3>One Example</h3>
<p><span>Hello</span>, how are you?</p>
<p>Me? I'm <span>good</span>.</p>
</div>
Description
I'm trying to take advantage of the keep-alive functionality of vue-js 2.3 so my AJAX call is made only once.
Problem
The second time I try to open the popup component I get this error :
Error in nextTick: "TypeError: Cannot read property 'insert' of undefined"
TypeError: Cannot read property 'insert' of undefined
Steps
Click on the button to display the popup
Wait for one second
Close the popup
Click again on the button
https://jsfiddle.net/4fwphqhv/
Minimal reproduction example
<div id="app">
<button #click="showDialog = true">Show Component PopUp</button>
<keep-alive>
<popup v-if="showDialog" :show-dialog.sync="showDialog"></popup>
</keep-alive>
</div>
<template id="popup">
<el-dialog :visible.sync="show" #visible-change="updateShowDialog">{{asyncData}}</el-dialog>
</template>
Vue.component('popup', {
template: '#popup',
props : ['showDialog'],
data(){
return {
show: this.showDialog,
asyncData: "Loading please wait"
}
},
methods: {
updateShowDialog(isVisible) {
if (isVisible) return false;
this.$emit('update:showDialog', false )
}
},
created:function (){
const _this = this
setTimeout(() => _this.asyncData = 'Async Data was loaded' , 1000)
},
});
var vm = new Vue({
el: '#app',
data: {
showDialog: false,
},
});
Real code of the popup component
<template>
<el-dialog title="Order in progress" size="large" :visible.sync="show" #visible-change="updateShowLoadOrder"></el-dialog>
</template>
<script>
let popUpData;
export default {
name: '',
data () {
return {
ordersInProgress: [],
show: this.showLoadOrder
}
},
props: ['showLoadOrder'],
methods: {
updateShowLoadOrder (isVisible) {
if (isVisible) return false;
this.$emit('update:showLoadOrder', false)
}
},
created () {
const _this = this;
if (!popUpData) {
axios.get('api/mtm/apiGetOrdersInProgress').then((response) => {
_this.ordersInProgress = popUpData = response.data;
});
} else {
this.ordersInProgress = popUpData;
}
}
}
</script>
Ok. So your problem here is the wrong life-cycle hook.
If you change created to activated... it should work. It did for me in your JS fiddle.
activated:function (){
setTimeout(() => this.asyncData = 'Async Data was loaded' , 1000)
}
There are two other hooks, activated and deactivated. These are for keep-alive components, a topic that is outside the scope of this article. Suffice it to say that they allow you to detect when a component that is wrapped in a tag is toggled on or off. You might use them to fetch data for your component or handle state changes, effectively behaving as created and beforeDestroy without the need to do a full component rebuild.
SOURCE: here