Why is a variable declared in onBeforeMount() not known in the main section of <script>? - vue.js

I have a Vue3 component in which I use a pre-mount hook (this is a trimmed-down version to scope it to the problem):
<script setup lang="ts">
const hello = () => {
let a = [... allNotes.value]
}
onBeforeMount(() => {
let allNotes = ref([])
}
</script>
With this, I get a ReferenceError: allNotes is not defined on the line that defines a.
Why is it so? Isn't allNotes known to hello() after the mount?

Why are you defining a reactive variable from inside a lifecycle hook? If you want allNotes to be available in the component or in the rest of your script, you only need to declare it in the top level of <script setup>. Remember that the Composition API setup() function replaces the beforeCreate and created lifecycle hooks, so anything defined within setup() will be available everywhere. You can read more about that in the Vue Docs
Specifically the problem here is that allNotes is scoped to the onBeforeMount() function only, and as such isn't known to the rest of your script. Once onBeforeMount() is called and finished, it will destroy allNotes and it will no longer exist.
You can just do
<script setup>
const allNotes = ref([])
const hello = () => {
allNotes.value.push("Hello")
}
</script>
To illustrate the point with the OptionsAPI, what you're doing is the same as something like this:
export default {
beforeMount() {
const allNotes = [];
},
methods: {
hello() {
this.allNotes.push("Hello!");
}
}
}
That won't work, since allNotes only exists inside of the beforeMount() hook. You'd have to declare allNotes in the data() or computed() properties in order for your hello() method to be able to use it.
Also, as a small sidenote, you should declare reactive elements with const and not let. It's a bit weird because you technically are changing its value, but the Vue internals make it so you're actually changing a copy of it that exists inside of Vue. As such, you're not actually modifying the original value, so let is inappropriate.

Related

How to fire an $emit event from Vue Composable

I have a vue composable that needs to fire an event. I naively set it up as follows:
*// composable.js*
import { defineEmits } from "vue";
export default function useComposable() {
// Vars
let buffer = [];
let lastKeyTime = Date.now();
const emit = defineEmits(["updateState"]);
document.addEventListener("keydown", (e) => {
// code
emit("updateState", data);
}
// *App.vue*
<template>
<uses-composables
v-show="wirtleState.newGame"
#updateState="initVars"
></uses-composables>
</template>
<script setup>
const initVars = (data) => {
//code here
}
// usesComposable.vue
<template>
<button #click="resetBoard" class="reset-button">Play Again</button>
</template>
<script setup>
import { defineEmits } from "vue";
import useEasterEgg from "#/components/modules/wirdle_helpers/useEasterEgg.js";
useEasterEgg();
</script>
The error I get is "Uncaught TypeError: emit is not a function useEasterEgg.js:30:11
So obviously you can not use defineEmits in a .js file. I dont see anywhere in Vue docs where they specifically use this scenario. I dont see any other way to do this but using $emits but that is invoked in a template which my composable does not have. Any enlightenment much appreciated.
You can emit events from a composable, but it will need to know where the events should be fired from using context which can be accessed from the second prop passed to the setup function: https://vuejs.org/api/composition-api-setup.html#setup-context
Composable:
export default function useComposable(context) {
context.emit("some-event")
}
Component script:
<script>
import useComposable from "./useComposable"
export default {
emits: ["some-event"],
setup(props, context) {
useComposable(context)
}
}
</script>
To use it in a script setup, the best way I found was to declare the defineEmit first, and assigning it to a const, and pass it as a param to your composable :
const emit = defineEmit(['example']
useMyComposable(emit);
function useMyComposable(emit){
emit('example')
}
You can't access emit this way, as the doc says : defineProps and defineEmits are compiler macros only usable inside script setup. https://vuejs.org/api/sfc-script-setup.html
I'm not entirely sure of what you are trying to achieve but you can use vue-use composable library to listen to key strokes https://vueuse.org/core/onkeystroke/
Lx4
This question is a bit confusing. What is <uses-composable>? A composable generally is plan js/ts, with no template. If it had a template, it would just be a component. Even if it was a component, which I mean you could turn it into if thats what you wanted, I don't see anything there that suggests that would be a good idea.
So I guess the question is, why would you want to emit an event? If I'm following what you want, you can just do:
// inside useEasterEgg.js
document.addEventListener('keydown', () => {
// other code
const data = 'test';
updateStateCallback(data);
});
function useEasterEgg() {
function onUpdateState(callback) {
updateStateCallback = callback;
}
return {
onUpdateState,
}
}
// Put this wherever you want to listen to the event
const { onUpdateState } = useEasterEgg();
onUpdateState(data => console.log(data));
https://jsfiddle.net/tdfu3em1/3/
Alternatively, if you just want access to data, why not make it a ref and just use it where you want:
const data = ref();
document.addEventListener('keydown', () => {
// other code
data.value = resultOfOtherCode;
});
function useEasterEgg() {
return {
data,
}
}
// Anywhere you want to use it
const { data } = useEasterEgg();

Vue.js setup() and mounted()

I am fairly new to Vue.js and am currently working on a project. I currently have some constants declared in setup(), lets call one of the constant "c", and I want to get them from within mounted(). I realized that I can still console.log(this.c) within mounted() to get the constant printed out but when I try to console.log(this.c) within a function in mounted() I can no longer get it and I get an error Uncaught (in promise) TypeError: Cannot read properties of undefined.
Why is this so? And what work arounds are there?
Example:
setup() {
const c = ref(1)
},
mounted() {
console.log(this.c) // prints 1 on console
function doesSomething() {
console.log(this.c) // TypeError: Cannot read properties of undefined
...
}
}
The function doSomething in the mounted hook should be declared with an arrow function instead, since function declaration has its own this binding:
mounted() {
console.log(this.c)
const doesSomething = () => {
console.log(this.c) // Ok
}
}
mounted is an option in the options api which is different than the composition api using the setup hook, you could use onMounted hook in setup to replace mounted option:
import {onMounted} from 'vue';
setup(){
let c='some content'
onMounted(()=>{
console.log(c)// don't use this keyword
})
}

How to `emit` event out of `setup` method in vue3?

I know I can call the emit method from the setup method, but is there any way to emit event from any other functions without passing the emit method from setup method(not the the functions in the methods option, but a useXXX function) ?
setup function takes two arguments, First one is props.
And the second one is context which exposes three component properties, attrs, slots and emit.
You can access emit from context like:
export default {
setup(props, context) {
context.emit('event');
},
};
or
export default {
setup(props, { emit }) {
emit('event');
},
};
Source
in vue3 typescript setup
<script setup lang="ts">
const emit = defineEmits()
emit('type', 'data')
<script>
20220626
<script setup lang="ts">
const emit = defineEmits(['emit_a', 'emit_b'])
emit('emit_a')
emit('emit_b', 'emit_b_data')
<script>
With Vue 3 setup syntax sugar
<script setup lang="ts">
import { defineEmits } from 'vue'
const emit = defineEmits<{
(e: 'change', id: number): void
(e: 'update', value: string): void
}>()
function yourFunction (id: number) {
emit('change', id)
}
<script>
See docs: https://v3.vuejs.org/api/sfc-script-setup.html#typescript-only-features
Here's the proper way to emit events programmatically (using javascript) in vue3:
export default defineComponent({
// See: https://vuejs.org/guide/components/events.html#declaring-emitted-events=
emits: 'myEventName', // <--- don't forget to declare custom events emitted
setup(_, { emit }) {
emit('myEventName') // <--- emit custom event programmatically whenever we want
},
})
The emits function can just as easily be passed as a param to any function not declared inside setup.
Side-note regarding other answers: we should avoid using getCurrentInstance(), which was intended for library authors needing access to internals of vue components (a.k.a. this of vue v2), when there are better alternatives. Especially when those alternatives were designed explicitly for our use case.
methods: {
minhaFuncao(){
let data = "conteudo";
this.$emit("nomeDoMEuEvento", data);
}
}
SEE MORE AT :https://github.com/Carlos-Alexandre-Leutz/emitir-eventos-filho-pra-pai-com-dados-no-vue3
export const useEmit = () => {
const vm = getCurrentInstance()
const emitFactory = (event: string) => (...args: any[]) => vm.emit(event, ...args)
return {
emit: vm.emit,
emitModel: emitFactory('update:modelValue')
}
}
const useButtonHandlers = () => {
const { emit } = useEmit()
const onClick = () => emit('click')
return {
onClick
}
}
You can use getCurrentInstance from Vue. You can check it out in the docs.
Usage is like
function useFunctionThatEmitsSomething(){
const instance = getCurrentInstance();
// do something
instance.emit('event');
}
Edit: Even though this answer solves the author's problem, as per the linked docs, this method is intended only for ADVANCED use cases, e.g authoring a plugin or library. For common use cases, like building a simple SPA, using this is TOTALLY DISCOURAGED and should be avoided at all costs, since it can lead to unreadable and unmaintenable code. If you feel the need to use this in a case like that, you're probably doing something wrong.

How to access Vue Instance from mixins?

I would like to implement this kind of logic that assign this.$store.value to local data.
This is how I do in pages/index.vue for instance.
method: {
this.value = this.$store.value
}
I want to write it down into mixins because I actually have another logics around it and I use some pages.
However, I don't know how should I access this(VueInstnce) from mixins?
It is not supported by Vue because mixin runs first before component's code,
then mixin is bound (merged) by Vue to the component instance so it's easy to access mixin from component/instance scope, but not vice versa.
To achieve your need I think the mixin method (like created) should be run (for example) with a given reference to the instance of your component as a parameter, but it's not like that.
However, if you reorganize your code to run what you need from instance.created
accessing there methods and data of mixin is possible and passing arguments on your own:
var mixin = {
data: {mixin: 'mixin'},
created: function () {
console.log('mixin hook called')
},
methods: { test: function(arg){console.log(arg); } }
};
vm=new Vue({
data: {component: 'component'},
mixins: [mixin],
created: function () {
console.log('called hook of ' + this.component + ' and accessing ' + this.mixin)
},
});
vm.test(vm.mixin);
vm.test(vm.component); // no problem to run mixin's method with component's data
> mixin hook called
> called hook of component and accessing mixin
> mixin
> component
Okay so I don't know if it's considered a bad practice but I have managed to accomplish one-way data transmission without an event bus.
I am using vuejs 3 with composition api. Requirement: all components shall be able to access a global singleton component shown on top of whole app.
plugin.js - here we use the created event in mixin to get a reference of an component instance. In example below I always have just 1 tracker component instance (global popup). If you have a different more complex scenario I would recommend sticking with event bus solution instead..
import Tracker from "#/plugins/ProgressTracker/components/Tracker.vue";
export default {
install(app, params = {}) {
app.component("tracker", Tracker);
let instance = undefined;
app.mixin({
created() {
if (this.$options.name === "ProgressTrackerPopup") {
instance = this;
}
},
});
const progressTracker = () => {
//
};
progressTracker.show = function () {
instance.show();
};
app.config.globalProperties.$progressTracker = progressTracker;
},
};
useProgressTracker.js - globally reusable composable function that exposes show method
import { ref, computed, getCurrentInstance } from "vue";
export default function useProgressTracker() {
const internalInstance = getCurrentInstance();
const progressTracker = internalInstance.appContext.config.globalProperties.$progressTracker;
const show = () => {
progressTracker.show();
};
return {
show,
};
}
Tracker.vue - component that we need to globally access from any other component (methods of it).. name is important. It shall be set in order for the mixin to be able to detect your component creation
<template>
<div class="onTop" v-if="isShow">test</div>
</template>
<script>
import { ref } from "vue";
export default {
name: "ProgressTrackerPopup",
setup() {
var isShow = ref(false);
const show = () => {
isShow.value = true;
};
return {
isShow,
show,
};
},
};
</script>
<style scoped>
.onTop{
position: absolute;
z-index: 1999;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #0000004f;
}
</style>
this is it. Don't forget to register the plugin:
import ProgressTracker from "#/plugins/plugin.js";
// ..
app.use(ProgressTracker, {});
Now when you want the pop-up to be shown you invoke:
// <tracker />
import useProgressTracker from "#/plugins/ProgressTracker/use/useProgressTracker.js";
const tracker = useProgressTracker();
tracker.show();
The last line of code will basically invoke the show method on global component instance itself! Whereas if you used an event bus instead - you would have subscribed to the pop event on target component itself.
I find this solution to be useful when you don't want to deal with an event bus and the case is relatively trivial (you only have 1 global instance at all times). Though, you could of course use an array of instances and loop-invoke the methods on them in sequence.. :)

Decentralizing functions in vuejs

Am from Angular2 whereby i was used to services and injection of services hence reusing functions how do i achieve the same in vuejs
eg:
I would like to create only one function to set and retrieve localstorage data.
so am doing it this way:
In my Login Component
this.$axios.post('login')
.then((res)=>{
localstorage.setItem('access-token', res.data.access_token);
})
Now in another component when sending a post request
export default{
methods:{
getvals(){
localstorage.getItem('access-token') //do stuff after retrieve
}
}
}
Thats just one example, Imagine what could happen when setting multiple localstorage items when retrieving one can type the wrong key.
How can i centralize functionality eg: setting token(in angular2 would be services)
There are a few different ways to share functionality between components in Vue, but I believe the most commonly used are either mixins or custom modules.
Mixins
Mixins are a way to define reusable functionality that can be injected into the component utilizing the mixin. Below is a simple example from the official Vue documentation:
// define a mixin object
var myMixin = {
created: function () {
this.hello()
},
methods: {
hello: function () {
console.log('hello from mixin!')
}
}
}
// define a component that uses this mixin
var Component = Vue.extend({
mixins: [myMixin]
})
var component = new Component() // => "hello from mixin!"
Custom module
If there are a lot of shared functionality with a logical grouping it might make sense to instead create a custom module, and import that where you need it (like how you inject a service in angular).
// localStorageHandler.js
const localStorageHandler = {
setToken (token) {
localStorage.setItem('access-token', token)
},
getToken () {
localstorage.getItem('access-token')
}
}
export default localStorageHandler
And then in your component:
// yourcomponent.vue
import localStorageHandler from 'localStorageHandler'
export default{
methods:{
getvals(){
const token = localStorageHandler.getToken()
}
}
}
Modules are using the more modern syntax of JavaScript, which is not supported in all browsers, hence require you to preprocess your code. If you are using the vue-cli webpack template it should work out of the box.