$refs is undefined when it's still working - javascript

I'm building a native hybrid app using Nuxt JS 2.9.1 and Vuetify JS. I have a notification component that is loaded into my default.vue layout just after the <nuxt /> tag, this component is loaded into every page and triggers notifications using a Cordova plugin.
On each page, I'm making a HTTP GET request to a server, but for development it's a local JSON file, I'm using Axios to do this, however, I need to access an individual object from an array of many objects via it's index on each page to send to the notification component.
Since I'm unable to access each individual object index within my methods, I'm creating a hidden <div> containing a reference to each object based on the URL that a user is on, and am using a ref to access this in my methods.
However, I get the following error, despite it working correctly:
Cannot read property '$refs' of undefined and Cannot read property 'innerText' of undefined
These errors seem to be inaccurate as it still appears to function.
<template>
<div>
<div v-for="(url, index) in dataUrls" :key="url.id">
<div ref="getId">{{ index }}</div>
<div ref="getUrl">{{ dataUrls[index].url }}</div>
<div ref="getName">{{ dataUrls[index].name }}</div>
<div ref="fetch">{{ dataUrls[index].fetchInterval }}</div>
<div ref="muteNotifications">{{ dataUrls[index].muteNotifications }}</div>
<div ref="notificationIcon">{{ dataUrls[index].notificationIcon }}</div>
<div ref="notificationID">{{ dataUrls[index].notificationID }}</div>
</div>
<div v-for="(interval, index) in intervalData" :key="interval.id">
<div ref="storedInterval">{{ intervalData[index].storedInterval }}</div>
<div ref="running">{{ intervalData[index].running }}</div>
<div ref="lastNotification">{{ intervalData[index].lastNotification }}</div>
<div ref="lastUpdated">{{ intervalData[index].lastUpdated }}</div>
</div>
</div>
</template>
<script>
export default {
data () {
return {
defaultNotification: []
}
},
mounted () {
document.addEventListener("deviceready", this.startFetchNotifications(), false)
},
methods: {
/**
* Fetch notifications
*/
fetchNotification(key) {
let self = this self.axios.get(self.$refs.getUrl[key].innerText).then(function(response) {
if (self.$refs.lastNotification[key].innerText === '') {
self.intervalData[parseInt(self.$refs.getId[key].innerText)].lastNotification = response.data.notification
}
if (self.$refs.lastNotification[key].innerText != response.data.notification) {
if (process.env.NODE_ENV === 'production') {
cordova.plugins.notification.local.schedule({
id: parseInt(self.$refs.notificationID[key].innerText),
title: response.data.notification,
text: self.$refs.getName[key].innerText,
vibrate: false,
priority: 1,
badge: 0,
foreground: true,
sticky: true,
sound: true,
icon: 'file://' + self.$refs.notificationIcon[key].innerText,
smallIcon: 'file://' + self.$refs.notificationIcon[key].innerText
});
} else {
console.info('Native notification: ' + response.data.notification + ', won\'t run in the browser')
}
self.intervalData[parseInt(self.$refs.getId[key].innerText)].lastNotification = response.data.notification ? response.data.notification : ''
self.intervalData[parseInt(self.$refs.getId[key].innerText)].lastUpdated = new Date()
}
})
},
/**
* Fetch new notifications
*/
autoFetchNotifications() {
let self = this
Object.keys(this.dataUrls).forEach(function(key, index) {
// Interval updated
if (parseInt(self.$refs.fetch[key].innerText) != parseInt(self.$refs.storedInterval[key].innerText)) {
self.intervalData[parseInt(self.$refs.getId[key].innerText)].storedInterval = parseInt(self.$refs.fetch[key].innerText)
// Stop running so we can start with new interval further down
if (self.$refs.running[key].innerText != 'false') {
clearInterval(parseInt(self.$refs.running[key].innerText))
self.intervalData[parseInt(self.$refs.getId[key].innerText)].running = 'false'
}
}
// If not running & not muted, then run
if (self.$refs.running[key].innerText === 'false' && self.$refs.muteNotifications[key].innerText === 'false' && parseInt(self.$refs.fetch[key].innerText) > 0) {
self.intervalData[parseInt(self.$refs.getId[key].innerText)].running = setInterval(() => {
self.fetchNotification(key)
}, parseInt(self.$refs.fetch[key].innerText))
}
// If running & muted, then stop
if (self.$refs.running[key].innerText != 'false' && self.$refs.muteNotifications[key].innerText === 'true') {
clearInterval(parseInt(self.$refs.running[key].innerText))
self.intervalData[parseInt(self.$refs.getId[key].innerText)].running = 'false'
}
})
}
},
computed: {
dataUrls () {
return this.$store.state.localStorage.dataUrls
},
intervalData () {
return this.$store.state.localStorage.intervalData
}
},
watch: {
dataUrls: {
handler: function (val, Oldval) {
setTimeout(function () {
console.log('fetch')
this.autoFetchNotifications()
}.bind(this), 10)
},
deep: true
}
}
}
</script>
Above is my notification component, loaded in every view.
How can I suppress this error, or what alternative can I implement here.

Try to replace
document.addEventListener("deviceready", this.startFetchNotifications(), false)
by
document.addEventListener("deviceready", this.startFetchNotifications, false)
to call the startFetchNotifications function when deviceready event listener is triggered, not when the event is created.

you should avoid using $ref in vuejs ...
$ref is populated after the first dom render, so $ref is empty in mouted()
cf: https://v2.vuejs.org/v2/api/?#ref

Related

How to use two #click $emit function in vue.js with electron

I'm developing login page with connect with SSHFS drive.
And some of the functions are working but, I'm confusing with my codes. I analyze my code but I don't know why it's working, and why this is working.
My application looks like this.
It's just simple login web app with Electron. So When I click login and if the ID & and password is right, it will mount SSHFS drive and goes to system tray icon to hide. After that it should open the explorer that mounted folder.
So I thought When I click login button, and two #click with $emit functions should work like this.
<div class="form-item">
<button class="login" v-show="showConnectButton" :class="{ 'success': isConnected, 'connecting-disconnecting': isConnectingOrDisconnecting }" :disabled="isConnectingOrDisconnecting" #click="[$emit(isConnected ? 'disconnect' : 'connect', conn), $emit('open', conn.mountPoint)]">{{isConnected ? 'disconnect' : 'login'}}</button>
</div>
I use this code but it seems the code tries mount the drive early then $emit(isConnected) function so It shows the error that Cannot find 'E' Drive that I set in another code.
It's an error that 'System cannot find 'E' drive in my computer'
I tried to put mount function to another function but it doesn't work. Only the 'Hide to tray' and 'Mount Drive function' works well. So I think it should fix only a few code and it will work but I don't know where should I fix the code.
And I already searched in stackoverflow questions but it seems doesn't fit in my question.
Here's my part of my full code. Please check the code and give me some advise. Thanks.
My ConnectionItem.vue full Code
<template>
<div class="item">
<div class="form-item">
<button class="login" v-show="showConnectButton" :class="{ 'success': isConnected, 'connecting-disconnecting': isConnectingOrDisconnecting }" :disabled="isConnectingOrDisconnecting" #click="[$emit(isConnected ? 'disconnect' : 'connect', conn)]">{{isConnected ? 'disconnect' : 'login'}}</button>
</div>
</div>
</template>
<script>
import { remote } from 'electron'
import Icon from '#/components/Icon'
const window = remote.getCurrentWindow()
export default {
name: 'connection-item',
components: {
Icon
},
props: {
conn: {
type: Object,
required: true
},
mode: {
type: String,
required: false,
default: 'none'
}
},
computed: {
isConnected (conn) {
// this.isActive = !this.enable
this.$emit('open', conn.mountPoint)
window.hide()
return this.conn.status === 'connected'
},
isConnectingOrDisconnecting () {
return this.conn.status === 'connecting' || this.conn.status === 'disconnecting'
},
showConnectButton () {
// window.hide()
// openLocal(path);
// this.$emit('open', conn.mountPoint)
return this.mode === 'none' || this.isConnected || this.isConnectingOrDisconnecting
},
isEditing () {
return this.mode === 'edit' && !this.isConnected && !this.isConnectingOrDisconnecting
},
showDeleteButton () {
return this.mode === 'delete' && !this.isConnected && !this.isConnectingOrDisconnecting
},
showRunningInBackgroundNotification () {
if (!this.runningInBackgroundNotificationShowed) {
if (this.$store.state.Settings.settings.displayTrayMessageOnClose) {
this.notify('program runs in background (network connected)') // 1
this.notify('Click Quit button when you disconnect drive') // 2
this.runningInBackgroundNotificationShowed = true
}
}
}
}
}
</script>

VueJS - Run Code after event is successfully emitted

I'm trying to clear up the form in the child component after the event containing the entered form data has been successfully passed from the child to parent component. However, I notice that the form gets cleared before the data gets propagated via the event to the parent component, such that the event passes empty values to the parent. I tried delaying the clearForm() using a timeout, but it didn't help. Is there a way to modify the behavior such that the clearForm() happens only after the event completes and the data has been saved?
Attached is the code.
Child Component
<template>
<!-- Contains a form -- >
</template>
<script>
export default {
data() {
return {
additionalInfo:
{
id: new Date().toISOString(),
fullName: '',
preAuthorize: '',
serviceAddress: ''
},
validation: {
fullNameIsValid: true,
serviceAddressIsValid: true
},
formIsValid: true,
addServiceButtonText: '+ Add Service Notes (Optional)',
serviceNotes: [],
showServiceNotes: false,
enteredServiceNote: '', //service notes addendum
}
},
computed : {
// something
},
methods: {
setServiceNotes(){
this.showServiceNotes = !this.showServiceNotes;
},
addAnotherParty(){
this.validateForm();
if(!this.formIsValid){
return;
}
this.$emit('add-parties', this.additionalInfo); //event
console.log(this.clearForm);
},
clearForm(){
this.additionalInfo.fullName = '';
this.additionalInfo.serviceAddress = '';
this.additionalInfo.preAuthorize = false;
}
}
}
</script>
Parent Component
<template>
<div>
<base-card
ref="childComponent"
#add-parties="updateAdditionalInfoList">
<!-- Wrapper for the `Parties Being Served` component-->
<template v-slot:title>
<slot></slot>
</template>
</base-card>
</div>
</template>
<script>
export default {
data() {
return {
hasElement: false,
selectedComponent: 'base-card',
additionalInfoList : [],
clearForm: false
}
},
methods: {
updateAdditionalInfoList(additionalInfo){ //save changes passed via event
this.additionalInfoList.push(additionalInfo);
console.log('emitted');
console.log(this.additionalInfoList);
setTimeout(() => {
this.$refs.childComponent.clearForm(); //clear the form in child
}, 2000);
}
}
}
</script>
Try this
addAnotherParty(){
this.validateForm();
if(!this.formIsValid){
return;
}
let emitObj = JSON.parse(JSON.stringify(this.additionalInfo));
this.$emit('add-parties', emitObj); //event
console.log(this.clearForm);
}
If your object is not deep then you can use
let emitObj = Object.assign({}, this.additionalInfo);
instead of stringify and parse

v-on click, add handler only if condition has been met

After some research the following suggestion by Mr. Evan You was found:
https://github.com/vuejs/vue/issues/7349#issuecomment-354937350
So without any hesitation I gave it a try:
Component template
<template>
<div v-on='{ click: dataType === `section` ? toggleSectionElements : null }'>
... magic
</div>
<template>
JS Logic
<script>
export default {
name: `product-section`,
props: [`section`, `sectionName`, `depth`],
methods: {
toggleSectionElements() {
... magic
}
},
computed: {
dataType() {
if (this.$props.section.sections || this.$props.depth === 0) {
return `section`
} else {
return `element`
}
}
}
}
</script>
But for described case it results in error during rendering:
[Vue warn]: Invalid handler for event "click": got null
Can someone please suggest what has been done wrong? :thinking:
Update
The way Data Model looks like:
DataModel: {
mainSectionA: {
sections: {
sectionA: {
sections: {
elementA: { values: { ... } },
elementB: { values: { ... } }
}
values: { ... }
}
sectionB: {
elementA: { values: { ... } },
elementB: { values: { ... } }
}
},
values: { ... }
},
mainSectionB: {
sections: {
elementA: { values: { ... } },
elementB: { values: { ... } },
elementC: { values: { ... } },
... elements
},
values: { ... }
}
}
Just change it to the below and it will work
v-on="condition ? { mouseover: handler } : {}"
or, if your handler is called mouseover
v-on="condition ? { mouseover } : {}"
Instead of polluting your template with ternary logic, you should actually perform the check inside the click handler instead. It not only makes your template more readable, but also makes maintaining the code easier since all logic has been abstracted and delegated to the event handler's callback instead.
Quick solution
Therefore the quick solution is to actually ensure that the toggleSectionElements() will only work when a correct dataType is present. This can be achieved by using a guard clause:
toggleSectionElements() {
// Guard clause to prevent further code execution
if (this.dataType() !== 'section')
return;
// Magic here
}
Even better, is that if separate handlers should be assigned to each dataType: you can then create a factory function for that purpose:
methods: {
// This is just a factory function
toggleElements() {
switch (this.dataType()) {
case 'section':
return this.toggleSectionElements;
case 'element':
// Something else...
}
},
toggleSectionElements() {
// Magic for section element
}
}
Suggestion: using atomic components
Since it might be costly to bind click event handlers to elements that end up doing nothing, you can also break down your component to be more atomic. The collection element will be responsible of receiving an array of "section" or "element", and each "section"/"element" will have its own component, something like this:
You have a collection component, say <my-collection>, that holds all "section" and "element" components
"section" component will use the <my-section> component
"element" component will use the <my-element> component
This is when VueJS becomes really powerful: you can use dynamic component inside <my-collection> to determine which component to use depending on the dataType encountered.
This is done by running a v-for through the collection, and then using v-bind:is="..." to determine whether a specific collection item should be using "section" or "element". I understand that this is probably going to go out of scope of your original question, but it's a worthwhile design to consider:
const collectionComponent = Vue.component('my-collection', {
template: '#my-collection-component',
data: function() {
return {
collection: [{
dataType: 'section',
description: 'Hello I am section 1'
}, {
dataType: 'element',
description: 'Hello I am element 1'
}, {
dataType: 'section',
description: 'Hello I am section 2'
}, {
dataType: 'element',
description: 'Hello I am element 2'
}]
}
},
methods: {
componentToUse(dataType) {
return 'my-' + dataType;
}
}
});
const sectionComponent = Vue.component('my-section', {
template: '#my-section-component',
props: ['itemData'],
methods: {
toggle() {
console.log('Doing some magic.');
}
}
});
const elementComponent = Vue.component('my-element', {
template: '#my-element-component',
props: ['itemData']
});
new Vue({
el: '#app'
});
.box {
border: 1px solid #999;
cursor: pointer;
margin: 10px;
padding: 10px;
}
.box:hover {
background-color: #eee;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<my-collection />
</div>
<script type="text/x-template" id="my-collection-component">
<div>
<component
v-for="(item, i) in collection"
v-bind:key="i"
v-bind:is="componentToUse(item.dataType)"
v-bind:itemData="item" />
</div>
</script>
<script type="text/x-template" id="my-section-component">
<div #click="toggle" class="box">
<h1>{{ itemData.dataType }}</h1>
<p>{{ itemData.description }}</p>
<p>Clicking on me will invoke a section-specific logic</p>
</div>
</script>
<script type="text/x-template" id="my-element-component">
<div class="box">
<h1>{{ itemData.dataType }}</h1>
<p>{{ itemData.description }}</p>
<p>Clicking on me will do nothing</p>
</div>
</script>
here:
click: dataType === `section` ? toggleSectionElements : null
in the not-equal case you pass null, but the value on click expects a function. you can try an emptry function:
click: dataType === `section` ? toggleSectionElements : ()=>{}
In Vue 3 you can pass null to the listener. Combining it with optional chaining you can do this:
#click="handler?.() || null"
Same for old browsers:
#click="handler ? handler() : null"

Vue Js: Issue with scoped slots and IE11

My component looks like this:
<template>
<div>
<div v-if="!loaded">
<p><i class="fas fa-spinner fa-spin"></i> Loading feed</p>
</div>
<div v-else>
<div data-slider ref="feedSlider" v-if="length > 0">
<div class="swiper-wrapper">
<div class="slide" v-for="record in records" :key="record.id">
<slot :record="record"></slot>
</div>
</div>
</div>
<div v-else>
<p>There are no records available.</p>
</div>
</div>
</div>
</template>
<script>
import Swiper from 'swiper';
import AjaxCaller from '../../mixins/AjaxCaller';
export default {
mixins: [AjaxCaller],
data() {
return {
loaded: false,
records: [],
length: 0,
}
},
mounted() {
this.makeCall(this.success, this.failure);
},
methods: {
success(response) {
this.loaded = true;
if (!response.data.records) {
return;
}
this.records = response.data.records;
this.length = this.records.length;
if (this.length < 2) {
return;
}
setTimeout(() => {
this.initiateSlider();
}, 1000);
},
initiateSlider() {
(new Swiper(this.$refs.feedSlider, {
effect: 'slide',
slideClass: 'slide',
slideActiveClass: 'slide-active',
slideVisibleClass: 'slide-visible',
slideDuplicateClass: 'slide-duplicate',
slidesPerView: 1,
spaceBetween: 0,
loop: true,
speed: 2000,
autoplay: {
delay: 5000,
},
autoplayDisableOnInteraction: false,
}));
},
failure(error) {
this.stopProcessing();
console.log(error);
}
}
}
</script>
The imported mixin AjaxCaller, which works fine with any other component:
<script>
export default {
props: {
url: {
type: String,
required: true
},
method: {
type: String,
default: 'post'
}
},
data() {
return {
processing: false
}
},
computed: {
getMethodParams() {
if (this.method === 'post') {
return {};
}
return this.requestData();
},
postMethodData() {
if (this.method === 'get') {
return {};
}
return this.requestData();
}
},
methods: {
requestData() {
return {};
},
startProcessing() {
this.processing = true;
this.startProcessingEvent();
},
stopProcessing() {
this.processing = false;
this.stopProcessingEvent();
},
startProcessingEvent() {},
stopProcessingEvent() {},
makeCall(success, failure) {
this.startProcessing();
window.axios.request({
url: this.url,
method: this.method,
params: this.getMethodParams,
data: this.postMethodData
})
.then(success)
.catch(failure);
}
}
}
</script>
And here's how I call it from within the view:
<feed-wrapper url="{{ route('front.news.feed') }}">
<div slot-scope="{ record }">
<p>
<a :href="record.uri" v-text="record.name"></a><br />
<span v-text="record.excerpt"></span>
</p>
</div>
</feed-wrapper>
Everything works fine in any browser other than IE 11 (and lower).
It even works in Edge - no issues what so ever.
In IE I get
[Vue warn]: Failed to generate render function:
Syntax Error: Expected identifier in ...
It doesn't even get to execute method call from within the mounted segment.
I use laravel-mix with Laravel so everything is compiled using webpack with babel so it's not ES6 related issue.
I've already spent whole night trying to un-puzzle this so any help would be much appreciated.
I know you've already said that you don't believe it's an ES6 issue but the evidence suggests it is.
IE11 doesn't support destructuring. If you type something like var {record} = {} into your IE11 console you'll see this same error message, 'Expected identifier'.
Try doing a search through the compiled code in your original error message and look for the word record. I suspect you'll find something like this:
fn:function({ record })
If you see that it means that the destructuring has made it to the browser without being compiled through Babel.
Exactly why this is happening depends on where you're using that scoped slot template. If you're using it inside a single-file component it should be going through Babel but if you aren't then it may be making it to the browser without transpiling. You said that you're calling it 'from within the view' but that doesn't clarify exactly how you're using it. There's a note about this in the docs, for what it's worth:
https://v2.vuejs.org/v2/guide/components-slots.html#Destructuring-slot-scope
Assuming you aren't able to fix the transpiling problem directly (e.g. by moving the template to somewhere it'll go through Babel) you can just remove the ES6 destructuring. So something like:
<div slot-scope="slotProps">
and then using slotProps.record instead of record in the code that follows.

Vue.js uncaught reference error

Learning Vue and stuck. I have a brand new Laravel 5.4 project that I am using to learn Vue concepts, using Pusher/Echo. All is working in terms of message broadcasting, and the messages are fetched from the server and displayed on page load as expected. I want to programatically (from somewhere else in the project) send a message into the queue.
I am using this example as guide to accessing the Vue method outside the instance.
Why can I not access the instance method from my main JS file? The project is compiled with webpack FYI.
My Vue.js file:
$(document).ready(function()
{
Vue.component('chat-system', require('../components/chat-system.vue'));
var chatSystem = new Vue({
el: '#system-chat',
data: {
sysmessages: []
},
created() {
this.fetchMessages();
Echo.private(sys_channel)
.listen('SystemMessageSent', (e) => {
this.sysmessages.unshift({
sysmessage: e.message.message,
player: e.player
});
});
},
methods: {
fetchMessages() {
axios.get(sys_get_route)
.then(response => {
this.sysmessages = response.data;
});
},
addMessage(sysmessage) {
this.sysmessages.unshift(sysmessage);
this.$nextTick(() => {
this.$refs.sysmessages.scrollToTop();
});
axios.post(sys_send_route, sysmessage)
.then(response => {
console.log(response.data);
});
},
sendMessage(sysmessage) {
if (sysmessage !== '') {
this.$emit('systemmessagesent', {
player: this.player,
message: sysmessage
});
}
}
}
});
});
My Vue.js component:
<template>
<div id="round-status-message" class="round-status-message">
<div class="row">
<div class="col-xs-12" v-for="sysmessage in sysmessages">
{{ sysmessage.message }}
</div>
</div>
</div>
</template>
<script>
export default {
props: ['player', 'sysmessages'],
data() {
return {
newSysMessage: ''
}
},
methods: {
scrollToTop () {
this.$el.scrollTop = 0
},
sendMessage() {
this.$emit('systemmessagesent', {
player: this.player,
message: this.newSysMessage
});
this.newSysMessage = ''
}
}
};
</script>
I want to send a message into the queue programatically, so in my app.js, to test, I do:
// TESTING SYSTEM MESSAGES - DELETE
window.setInterval(function(){
var resp = {};
resp.data = {
id: 1,
message: "She hastily put down yet, before the end of half.",
progress_id: 1,
created_at: "2017-08-17 14:01:11",
updated_at: "2017-08-17 14:01:11"
};
chatSystem.$refs.sysmessages.sendMessage(resp);
console.log(resp);
}, 3000);
// TESTING SYSTEM MESSAGES - DELETE
But I get Uncaught ReferenceError: chatSystem is not defined
All I needed was to make the method name available to the global scope?
global.chatSystem = chatSystem; // App variable globally
This seems to work now...

Categories