Vue.js infinite loop with prop array - vue.js

In my App.vue:
I'm using updated() for getting user info after callback and each time I change the route and getting info and
mounted(), if I used F5 or go to my website already connected with SSO
The problem is that in my updated(), I set an array which is passed to a component. It make infinity loop.
How can I avoid it ?
App.vue :
<template>
<Sidebar v-if="isLoggedIn" :displayName="currentUser" v-bind:role="rolesT" />
</template>
<script lang="ts">
public rolesT: ReadonlyArray<String> = ["test"];
public initUser() {
this.auth = Vue.prototype.$auth;
this.auth.getUser().then((user) => {
if (user !== null) {
this.currentUser = user.profile.name!;
this.accessTokenExpired = user.expired;
this.rolesT = []; // if I use only this one, I have infinite loop
this.rolesT = user.profile.roles ; // if I use only this one, I have infinite loop
this.rolesT = this.rolesT; // if I use only this one, I dont have infinite loop, but its useless, it was just for test
console.log(this.rolesT);
}
this.isLoggedIn = (user !== null && !user.expired);
});
}
public mounted() {
console.log("mounted");
this.initUser();
}
public updated() {
console.log("updated");
this.initUser();
}
}
</script>
Sidebar.vue (I do nothing with the array for now, but still loop) :
[...]
<script lang="ts">
import { Component, Vue, Prop } from 'vue-property-decorator';
#Component
export default class Sidebar extends Vue {
#Prop({default: 'Unknow name'})
private displayName!: string;
#Prop({default: 'Unknow username'})
private username!: string;
#Prop()
private role;
}
</script>

Related

Vue3 custom element into Vue2 app using external framework

I have an application written in Vue2 which is not really ready to be upgraded to Vue3. But, I would like to start writing a component library in Vue3 and import the components back in Vue2 to eventually make the upgrade once it's ready.
Vue 3.2+ introduced defineCustomElement which works nicely but once I use a framework in the Vue3 environment (for example Quasar) that attaches to the Vue instance, it starts throwing errors in the Vue2 app, possibly because the result of defineCustomElement(SomeComponent) tries to use something from the framework that should be attached to the app.
I've thought about extending the HTMLElement and mounting the app on connectedCallback but then I lose the reactivity and have to manually handle all props/emits/.. like so:
class TestQuasarComponentCE extends HTMLElement {
// get init props
const prop1 = this.getAttribute('prop1')
// handle changes
// Mutation observer here probably...
const app = createApp(TestQuasarComponent, { prop1 }).use(Quasar)
app.mount(this)
}
customElements.define('test-quasar-component-ce', TestQuasarComponentCE);
So finally the question is - is it possible to somehow combine the defineCustomElement with a framework that attaches to the app?
So, after a bit of digging, I came up with the following.
First, let's create a component that uses our external library (Quasar in my case)
// SomeComponent.vue (Vue3 project)
<template>
<div class="container">
// This is the quasar component, it should get included in the build automatically if you use Vite/Vue-cli
<q-input
:model-value="message"
filled
rounded
#update:model-value="$emit('update:message', $event)"
/>
</div>
</template>
<script setup lang="ts>
defineProps({
message: { type: String }
})
defineEmits<{
(e: 'update:message', payload: string | number | null): void
}>()
</script>
Then we prepare the component to be built (this is where the magic happens)
// build.ts
import SomeComponent from 'path/to/SomeComponent.vue'
import { reactive } from 'vue'
import { Quasar } from 'quasar' // or any other external lib
const createCustomEvent = (name: string, args: any = []) => {
return new CustomEvent(name, {
bubbles: false,
composed: true,
cancelable: false,
detail: !args.length
? self
: args.length === 1
? args[0]
: args
});
};
class VueCustomComponent extends HTMLElement {
private _def: any;
private _props = reactive<Record<string, any>>({});
private _numberProps: string[];
constructor() {
super()
this._numberProps = [];
this._def = SomeComponent;
}
// Helper function to set the props based on the element's attributes (for primitive values) or properties (for arrays & objects)
private setAttr(attrName: string) {
// #ts-ignore
let val: string | number | null = this[attrName] || this.getAttribute(attrName);
if (val !== undefined && this._numberProps.includes(attrName)) {
val = Number(val);
}
this._props[attrName] = val;
}
// Mutation observer to handle attribute changes, basically two-way binding
private connectObserver() {
return new MutationObserver(mutations => {
mutations.forEach(mutation => {
if (mutation.type === "attributes") {
const attrName = mutation.attributeName as string;
this.setAttr(attrName);
}
});
});
}
// Make emits available at the parent element
private createEventProxies() {
const eventNames = this._def.emits as string[];
if (eventNames) {
eventNames.forEach(evName => {
const handlerName = `on${evName[0].toUpperCase()}${evName.substring(1)}`;
this._props[handlerName] = (...args: any[]) => {
this.dispatchEvent(createCustomEvent(evName, args));
};
});
}
}
// Create the application instance and render the component
private createApp() {
const self = this;
const app = createApp({
render() {
return h(self._def, self._props);
}
})
.use(Quasar);
// USE ANYTHING YOU NEED HERE
app.mount(this);
}
// Handle element being inserted into DOM
connectedCallback() {
const componentProps = Object.entries(SomeComponent.props);
componentProps.forEach(([propName, propDetail]) => {
// #ts-ignore
if (propDetail.type === Number) {
this._numberProps.push(propName);
}
this.setAttr(propName);
});
this.createEventProxies();
this.createApp();
this.connectObserver().observe(this, { attributes: true });
}
}
// Register as custom element
customElements.define('some-component-ce', VueCustomElement);
Now, we need to build it as library (I use Vite, but should work for vue-cli as well)
// vite.config.ts
export default defineConfig({
...your config here...,
build: {
lib: {
entry: 'path/to/build.ts',
name: 'ComponentsLib',
fileName: format => `components-lib.${format}.js`
}
}
})
Now we need to import the built library in a context that has Vue3, in my case index.html works fine.
// index.html (Vue2 project)
<!DOCTYPE html>
<html lang="">
<head>
// Vue3
<script src="https://cdn.jsdelivr.net/npm/vue#3/dist/vue.global.prod.js"></script>
// Quasar styles
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons" rel="stylesheet" type="text/css">
<link href="https://cdn.jsdelivr.net/npm/quasar#2.4.3/dist/quasar.prod.css" rel="stylesheet" type="text/css">
// Our built component
<script src="path/to/components-lib.umd.js"></script>
</head>
...rest of your html...
</html>
Now we are ready to use our component within our Vue2 (or any other) codebase same way we are used to with some minor changes, check comments below.
// App.vue (Vue2 project)
<template>
<some-component-ce
:message="message" // For primitive values
:obj.prop="obj" // Notice the .prop there -> for arrays & objects
#update:message="message = $event.detail" // Notice the .detail here
/>
</template>
<script>
export default {
data() {
return {
message: 'Some message here',
obj: { x: 1, y: 2 },
}
}
}
</script>
Now, you can use Vue3 components in Vue2 :)

Vue 3 does not update getter

I am in the process of updating vue2 to vue3 but encounter this problem.
I have a service called TService
// T.ts
class T {
public obj = { value: false };
constructor() {
setInterval(() => {
this.obj.value = !this.obj.value;
}, 1000);
}
}
const t = new T();
export { t as TService };
The service is very simple, it update it's obj value every 1 second.
Now come to the fun part
On vue2, I can do this:
<template>
<div> {{ test }} </div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
import { TService } from './T;
#Component
export default class HelloWorld extends Vue {
public obj = TService.obj;
get test() {
return this.obj.value;
}
}
</script>
The test value updated on screen every 1sec and works as expected.
However, when I changed to vue3 with the below code, it does not work any more
<template>
<div>{{ test }}</div>
</template>
<script lang="ts">
import { Options, Vue } from "vue-class-component";
import { TService } from './T';
#Options({})
export default class HelloWorld extends Vue {
public obj = TService.obj;
get test() {
return this.obj.value;
}
}
</script>
Not sure what is going on and appreciate if anyone can fix my code.
I am using latest vue 3.1.5 and vue-class-component 8.0.0-rc.1
You probably should make it reactive so Vue knows its value can be updated, see here
The anwser can be found in this post:
Changes made to an object created outside of Vue component are not detected by Vue 3
Basically I will need to wrap reactive around my object in my service
import { reactive } from 'vue';
// T.ts
class T {
public obj = reactive({ value: false });
constructor() {
setInterval(() => {
this.obj.value = !this.obj.value;
}, 1000);
}
}
const t = new T();
export { t as TService };

Vue.js Dynamically extend/replace child component method at runtime with access to parent scope

Is it possible to extend child component function at runtime in vue? I want to limit/stop child component function call based on parent scope logic (I want to avoid passing props in this specific case).
Overriding a component method is not a runtime solution/I can't have access to parent scope.
What I have tried and it does not working:
// Foo.vue
<template>
<button #click="func">Click me</button>
</template>
export default {
methods: {
func() {
console.log('some xhr')
}
}
}
// Bar.vue
<template>
<Foo ref="foo"/>
</template>
export default {
components: {Foo}
mounted() {
this.$nextTick(() => {
this.$refs.foo.func = function() {
console.log('some conditional logic')
this.$refs.foo.func()
}
})
}
}
For this usecase a better implementation would be defining the function in the parent itself and passing it through props. Since props are by default reactive you can easily control it from parent.
// Foo.vue
<template>
<button #click="clickFunction.handler">Click me</button>
</template>
export default {
name: 'Foo',
props: {
clickFunction: {
type: Object,
required: true
}
}
}
// Bar.vue
<template>
<Foo :clickFunction="propObject"/>
</template>
export default {
components: {Foo},
data() {
return {
propObject: {
handler: null;
}
};
}
mounted() {
this.$nextTick(() => {
if(some condition) {
this.propObject.handler = this.func();
} else this.propObject.handler = null;
})
},
methods: {
func() {
console.log('some xhr')
}
}
}
From what I managed to realize:
the solution in the code posted in the question really replaces the func() method in the child component. It's just that Vue has already attached the old method to the html element. Replacing it at the source will have no impact.
I was looking for a way to re-attach the eventListeners to html component. Re-rendering using an index key would not help because it will re-render the component with its original definition. You can hide the item in question for a split second, and when it appears you will receive an updated eventListener. However, this involves an intervention in the logic of the child component (which I avoid).
The solution is the $forceUpdate() method.
Thus, my code becomes the following:
// Foo.vue
<template>
<button #click="func">Click me</button>
</template>
export default {
methods: {
func() {
console.log('some xhr')
}
}
}
// Bar.vue
<template>
<Foo ref="foo"/>
</template>
export default {
components: {Foo}
mounted() {
this.$nextTick(() => {
let original = this.$refs.foo.func; // preserve original function
this.$refs.foo.func = function() {
console.log('some conditional logic')
original()
}
this.$refs.btn.$forceUpdate(); // will re-evaluate visual logic of child component
})
}
}

How to implement debounce in vue3

I have a filter input field and want to filter a list of items. The list is large so I want to use debounce to delay the filter being applied until the user has stopped typing for improved user experience. This is my input field and it's bound to filterText that is used to filter the list.
<input type="text" v-model="state.filterText" />
I didn't find any nice solution as I wanted to see my binding in my template so I decided to share my solution. I wrote a simple debounce function and use the following syntax to bind the behavior:
setup() {
...
function createDebounce() {
let timeout = null;
return function (fnc, delayMs) {
clearTimeout(timeout);
timeout = setTimeout(() => {
fnc();
}, delayMs || 500);
};
}
return {
state,
debounce: createDebounce(),
};
},
And the template syntax:
<input
type="text"
:value="state.filterText"
#input="debounce(() => { state.filterText = $event.target.value })"
/>
Hi first time answering something here, so correct my answer as much as you want, I'd appreciate it.
I think that the prettiest and lightest solution is to create a directive globally that you can use as much as you want in all of your forms.
you first create the file with your directive, eg.
debouncer.js
and you create the function for the debouncing
//debouncer.js
/*
This is the typical debouncer function that receives
the "callback" and the time it will wait to emit the event
*/
function debouncer (fn, delay) {
var timeoutID = null
return function () {
clearTimeout(timeoutID)
var args = arguments
var that = this
timeoutID = setTimeout(function () {
fn.apply(that, args)
}, delay)
}
}
/*
this function receives the element where the directive
will be set in and also the value set in it
if the value has changed then it will rebind the event
it has a default timeout of 500 milliseconds
*/
module.exports = function debounce(el, binding) {
if(binding.value !== binding.oldValue) {
el.oninput = debouncer(function(){
el.dispatchEvent(new Event('change'))
}, parseInt(binding.value) || 500)
}
}
After you define this file you can go to your main.js import it and use the exported function.
//main.js
import { createApp } from 'vue'
import debounce from './directives/debounce' // file being imported
const app = createApp(App)
//defining the directive
app.directive('debounce', (el,binding) => debounce(el,binding))
app.mount('#app')
And its done, when you want to use the directive on an input you simply do it like this, no imports or anything.
//Component.vue
<input
:placeholder="filter by name"
v-model.lazy="filter.value" v-debounce="400"
/>
The v-model.lazy directive is important if you choose to do it this way, because by default it will update your binded property on the input event, but setting this will make it wait for a change event instead, which is the event we are emitting in our debounce function. Doing this will stop the v-model updating itself until you stop writing or the timeout runs out (which you can set in the value of the directive).
I hope this was understandable.
<template>
<input type="text" :value="name" #input="test" />
<span>{{ name }}</span>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
function debounce<T> (fn: T, wait: number) {
let timer: ReturnType<typeof setTimeout>
return (event: Event) => {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
if (typeof fn === 'function') {
fn(event)
}
}, wait)
}
}
export default defineComponent({
setup () {
const name = ref('test')
function setInputValue (event: Event) {
const target = event.target as HTMLInputElement
name.value = target.value
}
const test = debounce(setInputValue, 1000)
return { name, test }
}
})
</script>
With Lodash, you have an easier solution:
<template>
<input type="text" :value="name" #input="onInput" />
<span>{{ name }}</span>
</template>
<script>
import debounce from "lodash/debounce"
export default {
setup () {
const onInput = debounce(() => {
console.log('debug')
}, 500)
return { onInput }
}
}
</script>
<input #input="updateValue"/>
const updateValue = (event) => {
const timeoutId = window.setTimeout(() => {}, 0);
for (let id = timeoutId; id >= 0; id -= 1) {
window.clearTimeout(id);
}
setTimeout(() => {
console.log(event.target.value)
}, 500);
};
You can try this one
Here's an example with Lodash and script setup syntax using a watcher to fire the debounced api call:
<script setup>
import { ref, watch } from 'vue'
import debounce from 'lodash.debounce'
const searchTerms = ref('')
const getFilteredResults = async () => {
try {
console.log('filter changed')
// make axios call here using searchTerms.value
} catch (err) {
throw new Error(`Problem filtering results: ${err}.`)
}
}
const debouncedFilter = debounce(getFilteredResults, 250) // 250ms delay
watch(() => searchTerms.value, debouncedFilter)
</script>
<template>
<input v-model="searchTerms" />
</template>
https://www.npmjs.com/package/vue-debounce now works for vue 3
It can be registered also with composition API like this
setup() {
...
},
directives: {
debounce: vue3Debounce({ lock: true })
}

Vue 3 Composition API reuse in multiple components

I have these files
App.vue, Header.vue, search.js and Search.vue
App.vue is normal and just adding different views
Header.vue has an input box
<input type="text" v-model="searchPin" #keyup="searchResults" />
<div>{{searchPin}}</div>
and script:
import useSearch from "#/compositions/search";
export default {
name: "Header",
setup() {
const { searchPin, searchResults } = useSearch();
return {
searchPin,
searchResults
};
}
};
search.js has the reusable code
import { ref } from "vue";
export default function useSearch() {
const searchPin = ref("");
function searchResults() {
return searchPin.value;
}
return {
searchPin,
searchResults
};
}
Now, this is working well.. once you add something on the input box, it is showing in the div below.
The thing I have not understood is how to use this code to a third component like Search.vue.
I have this, but its not working.
<template>
<div>
<h1 class="mt-3">Search</h1>
<div>{{ searchPin }}</div>
</div>
</template>
<script>
import useSearch from "#/compositions/search";
export default {
name: "Search",
setup() {
const { searchPin, searchResults } = useSearch();
return {
searchPin,
searchResults
};
}
};
</script>
What am I missing? Thanks.
The fix for this is very simple
instead of
import { ref } from "vue";
export default function useSearch() {
const searchPin = ref("");
function searchResults() {
return searchPin.value;
}
return {
searchPin,
searchResults
};
}
use
import { ref } from "vue";
const searchPin = ref("");
export default function useSearch() {
function searchResults() {
return searchPin.value;
}
return {
searchPin,
searchResults
};
}
The problem is that the searchPin is scoped to the function, so every time you call the function, it gets a new ref. This is a desirable effect in some cases, but in your case, you'll need to take it out.
Here is an example that uses both, hope it clears it up.
const {
defineComponent,
createApp,
ref
} = Vue
const searchPin = ref("");
function useSearch() {
const searchPinLoc = ref("");
function searchResults() {
return searchPin.value + "|" + searchPinLoc.value;
}
return {
searchPin,
searchPinLoc,
searchResults
};
}
const HeaderComponent = defineComponent({
template: document.getElementById("Header").innerHTML,
setup() {
return useSearch();
},
})
const SearchComponent = defineComponent({
template: document.getElementById("Search").innerHTML,
setup() {
return useSearch();
}
})
createApp({
el: '#app',
components: {
HeaderComponent, SearchComponent
},
setup() {}
}).mount('#app')
<script src="https://unpkg.com/vue#3.0.0-rc.9/dist/vue.global.js"></script>
<div id="app">
<header-component></header-component>
<search-component></search-component>
</div>
<template id="Header">
searchPin : <input type="text" v-model="searchPin" #keyup="searchResults" />
searchPinLoc : <input type="text" v-model="searchPinLoc" #keyup="searchResults" />
<div>both: {{searchResults()}}</div>
</template>
<template id="Search">
<div>
<h1 class="mt-3">Search</h1>
<div>both: {{searchResults()}}</div>
</div>
</template>
Adding flavor to #Daniel 's answer.
This is exactly what I'm struggling with regarding to best practices ATM and came to some conclusions:
Pulling the Ref outside of the composition fn would fix your problem but if you think about it, it's like sharing a single instance of a data property used in multiple places. You should be very careful with this, since ref is mutable for whoever pulls it, and will easily break unidirectional data flow.
For e.g. sharing a single Ref instance between a parent component and a child components can be compared to passing it down from parent's data to child's props, and as I assume we all know we should avoid mutating props directly
So classical answer for your question would be, move it to Vuex state and read it from there.
But if you have a small application, don't want a state manager, or simply want to take full advantage of the composition API, then my suggestion would be to at least do something of this pattern
import { ref, computed } from "vue";
const _searchPin = ref(""); // Mutable persistant prop
const searchPin = computed(() => _searchPin.value); // Readonly computed prop to expose
export default function useSearch() {
function searchResults() {
return searchPin.value;
}
return {
searchPin,
searchResults
};
}
Not more than ONE component should mutate the persistent Ref while others could only listen to the computed one.
If you find that more than one component needs access to change the ref, then that's probably a sign you should find another way to implement this (Vuex, props and events, etc...)
As I said, I am still trying to make sense of this myself and am not sure this is a good enough pattern either, but it's definitely better then simply exposing the instance.
Another option for code arrangement would be to encapsulate in 2 different access hooks
import { ref, readonly } from "vue";
const searchPin = ref(""); // Mutable persistant prop
export const useSearchSharedLogic() {
return readonly({
searchPin
})
}
const useSearchWriteLogic() {
return {
searchPin
}
}
// ----------- In another file -----------
export default function useSearch() {
const { searchPin } = useSearchSharedLogic()
function searchResults() {
return searchPin.value;
}
return {
searchPin,
searchResults
};
}
Or something of this sort (Not even sure this would work correctly as written).
Point is, don't expose a single instance directly
Another point worth mentioning is that this answer takes measure to preserve unidirectional data flow pattern. Although this is a basic proven pattern for years, it's not carved in stone. As composition patterns get clearer in the close time, IMO we might see people trying to challenge this concept and returning in some sense to bidirectional pattern like in Angular 1, which at the time caused many problems and wasn't implemented well