I'm trying to create a Perfect Scrollbar plugin for Vue3 but i'm stuck on an error when i initialize the object:
Error: no element is specified to initialize PerfectScrollbar
Scrollbar component:
import PerfectScrollbar from 'perfect-scrollbar'
import { isEmpty } from 'lodash'
import {
onMounted,
reactive,
ref,
h
} from 'vue'
export default {
name: 'VuePerfectScrollbar',
props: {
options: {
type: Object,
required: false,
default: () => {
}
},
tag: {
type: String,
required: false,
default: 'div'
}
},
setup(props, {emit, slots}) {
const el = ref(null)
let ps = reactive({})
onMounted(() => {
if (isEmpty(ps)) {
ps = new PerfectScrollbar(el, props.options)
}
})
return () => h(
props.tag,
{
class: 'ps',
ref: el
},
slots.default && slots.default()
)
}
}
I did a console.log right before the initialization and the element reference is there, so i'm not sure why the error pops up. According to this This error simply means that you are calling PerfectScrollbar on something that doesn't exist! so maybe the DOM hasn't been updated at that moment? I tried with nextTick too but it didn't work.
You should get access to the value attribute in the ref property as follows :
ps = new PerfectScrollbar(el.value, props.options)
Related
So I am using Lang plugin. This is how I install it:
onDomReady(() => {
appVue.use(Lang, {
lang: langService._lang,
});
appVue.mount('#app');
});
And this is install code for Lang:
const plugin = {
install(app, options) {
if (!app.config.globalProperties.hasOwnProperty('$lang')) {
app.config.globalProperties.$lang = options.lang;
}
app.config.globalProperties.$t = function (key, opt) {
const lang = this.$lang;
return lang.trans(key, opt);
};
},
};
export default plugin;
So now I should be able to access $t and $t works when it is used inside template tags for example.
<template>
<div>{{ $t('hello') }}<div>
<template>
It prints out translation, but when I use inside <script> tag for example:
props: {
placeholder: {
type: String,
default() {
return this.$t('hello');
},
}
}
It throws me error this.$t is not a function. What is the problem here? Would appreciate some help
I wrote a "loading state" mixin for Vue 2:
export default {
props: {
loading: {
type: Boolean,
default: false
},
},
data () {
return {
innerLoading: false,
}
},
mounted () {
this.innerLoading = !!this.loading
},
methods: {
startLoading () {
this.$emit('update:loading', this.innerLoading = true)
},
stopLoading () {
this.$emit('update:loading', this.innerLoading = false)
},
},
computed: {
isLoading () {
return !!this.innerLoading
},
isNotLoading () {
return !this.innerLoading
},
},
watch: {
loading (loading) {
this.innerLoading = !!loading
},
}
}
I use this mixin for other components to hold the loading state. For example for forms, buttons, tables etc.
Now, Im trying to rewrite this mixin to composition API style for Vue 3. Ideally, I would like to use my loading composable like this:
// components/Table.vue
import 'useLoading' from 'src/composables/loading'
export default defineComponent({
setup () {
const { startLoading, stopLoading, innerLoading } = useLoading()
// ...
return { startLoading, stopLoading, innerLoading, ... }
}
})
My question:
// How can I define the loading prop inside the setup() function?
props: {
loading: {
type: Boolean,
default: false
},
},
Of course I can define my component like this:
import 'useLoading' from 'src/composables/loading'
export default defineComponent({
props: {
loading: {
type: Boolean,
default: false
},
},
setup () {
const { startLoading, stopLoading, innerLoading } = useLoading();
}
})
But imagine, I have 20 components using this mixin/composable. So I want to define that loading prop only ONCE (like I did in mixin).
Is there a way how to do it with composition API?
you may be able to do something like this
import {withProps, useLoading} from "src/composables/loading";
export default defineComponent({
props: {
...withProps()
},
setup () {
const { startLoading, stopLoading, innerLoading } = useLoading();
}
})
where withProps is a function that would have your definitions
export const withProps = () => ({
loading: {
type: Boolean,
default: false
},
})
of course it doesn't need to be a function, but in some cases it may be helpful and preemptively making it a function can make api consistent.
Define an Object called loadingProps in separate file called makeLoadingProps:
export const loadingProps = {
loading: {
type: Boolean,
default: false
}
}
then import it inside your component defined using the script setup syntax:
<script setup lang="ts">
import {defineProps} from 'vue'
import { loadingProps } from 'src/composables/makeLoadingProps';
const props = defineProps({
...loadingProps,
//other props
})
const { startLoading, stopLoading, innerLoading } = useLoading(props)
</script>
In vuejs3 app I retrieve data from db with axios in method, like :
<script>
import appMixin from '#/appMixin'
import app from './../../App.vue' // eslint-disable-line
import axios from 'axios'
const emitter = mitt()
export default {
name: 'adminCategoriesList',
mixins: [appMixin],
data: function () {
return {
categoriesPerPage: 20,
currentPage: 1,
categoriesTotalCount: 0,
categories: []
}
},
components: {
},
setup () {
const adminCategoriesListInit = async () => {
this.loadCategories() // ERROR HERE
}
function onSubmit (credentials) {
alert(JSON.stringify(credentials, null, 2))
console.log('this::')
console.log(this)
console.log('app::')
}
onMounted(adminCategoriesListInit)
return {
// schema,
onSubmit
}
}, // setup
methods: {
loadCategories() {
...
}
and I got error in browser's console :
Cannot read property 'loadCategories' of undefined
If to remove “this.” in loadCategories call
I got error :
'loadCategories' is not defined
I need to make loadCategories as method, as I need to cal;l it from different places.
Which way is correct ?
Thanks!
You could use composition and options api in the same component but for different properties and methods, in your case the data properties could be defined inside setup hook using ref or reactive, the methods could be defined as plain js functions :
import {ref} from 'vue'
export default {
name: 'adminCategoriesList',
mixins: [appMixin],
components: {
},
setup () {
const categoriesPerPage= ref(20);
const currentPage=ref(1);
const categoriesTotalCount=ref(0),
const categories=ref[])
const adminCategoriesListInit = async () => {
loadCategories()
}
function onSubmit (credentials) {
alert(JSON.stringify(credentials, null, 2))
}
functions loadCategories(){
...
}
onMounted(adminCategoriesListInit)
return {
// schema,
onSubmit,
categoriesPerPage,
currentPage,
categoriesTotalCount,
categories
}
},
the properties defined by ref could be used/mutated by property.value and used in template like {{property}}
I am building an application which is using Vue 3 and I am providing a property in a parent component which I am subsequently injecting into multiple child components. Is there any way for a component which gets injected with this property to watch it for changes?
The parent component looks something like:
<template>
<child-component/>
<other-child-component #client-update="update_client" />
</template>
<script>
export default {
name: 'App',
data() {
return {
client: {}
}
},
methods: {
update_client(client) {
this.client = client
}
},
provide() {
return {
client: this.client
}
},
}
</script>
The child component looks like:
<script>
export default {
name: 'ChildComponent',
inject: ['client'],
watch: {
client(new_client, old_client) {
console.log('new client: ', new_client);
}
}
}
</script>
I am trying to accomplish that when the provided variable gets updated in the parent the children components where its being injected should get notified. For some reason the client watch method is not getting called when client gets updated.
Is there a better way of accomplishing this?
Update
After further testing I see that there is a bigger issue here, in the child component even after the client has been updated in the parent, the client property remains the original empty object and does not get updated. Since the provided property is reactive all places it is injected should automatically be updated.
Update
When using the Object API reactive definition (data(){return{client:{}}), even though the variable is reactive within the component, the injected value will be static. This is because provide will set it to the value that it is initially set to. To have the reactivity work, you will need to wrap it in a computed
provide(){
return {client: computed(()=>this.client)}
}
docs:
https://vuejs.org/guide/components/provide-inject.html#working-with-reactivity
You may also need to use deep for your watch
Example:
<script>
export default {
name: 'ChildComponent',
inject: ['client'],
watch: {
client: {
handler: (new_client, old_client) => {
console.log('new client: ', new_client);
},
deep: true
}
}
}
</script>
As described in official documentation ( https://v2.vuejs.org/v2/api/#provide-inject ), by default, provide and inject bindings are not reactive. But if you pass down an observed object, properties on that object remain reactive.
For objects, Vue cannot detect property addition or deletion. So the problem in your code might be here:
data() {
return {
client: {}
}
},
Since you change the client property of this object ( this.client.client = client ), you should declare this key in data, like this:
data() {
return {
client: { client: null }
}
},
Now it becomes reactive.
I did a code sandbox reproducing your code watching an injected property: https://codesandbox.io/s/vue-inject-watch-ffh2b
For some reason the only way I got this to work was by only updating properties of the initial injected object instead of replacing the whole object. I also was not able to get watch working with the injected property despite setting deep: true.
Updated parent component:
<template>
<child-component/>
<other-child-component #client-update="update_client" />
</template>
<script>
export default {
name: 'App',
data() {
return {
client: {}
}
},
methods: {
update_client(client) {
this.client.client = client
}
},
provide() {
return {
client: this.client
}
},
}
</script>
Updated child component:
<template>
<button #click="get_client">Get client</button>
</template>
<script>
export default {
name: 'ChildComponent',
inject: ['client'],
methods: {
get_client() {
console.log('updated client: ', client);
}
}
}
</script>
create a new value and reference the value from inject into it
inject: ['client'],
data: () => ({
value: null,
}),
created() {
this.value = this.client;
},
watch: {
value: {
handler() {
/* ... */
},
deep: true,
}
}
Now you can watch the value.
Note: "inject" must be an object
I ran into the same issue. But i just had to look more closely for details in the docs to make it work. In the end everything worked fine for me.
I built a vue plugin providing a Map together with some function as a readonly ref. Then it starts changing the Map contents once a second:
plugin.js
import { ref, readonly } from 'vue';
const rRuns = ref( new Map() );
let time = 0;
export default
{
install(app, defFile)
{
...
app.provide( "runs", readonly(
{ ref: rRuns,
get: (e) => rRuns.value.get( e ),
locationNames: () => rRuns.value.keys(),
size: () => rRuns.value.size,
} ) );
...
setInterval( () =>
{ time++;
const key = (time * 7) % 10;
console.log(" runs update", key, time);
rRuns.value.set( key.toString(), time )
}, 1000);
console.log(" time Interval start" );
}
}
main.js:
import { createApp } from 'vue'
import App from './App.vue'
import plugin from 'plugin.js';
const app = createApp(App);
app.config.unwrapInjectedRef = true;
app.use(game, 'gamedefs.json');
app.mount('#app');
runs.vue:
<template>
<h1>Runs:</h1>
<p v-if="!runs.size()">< no runs ></p>
<p v-else>runs: {{ runs.size() }}</p>
<button v-for="r of runs.locationNames()" :key="r" #click="display( r )">[{{ r }}]</button>
</template>
<script>
export default {
name: 'Runs',
inject:
{
runs: { from: 'runs' },
},
watch:
{
'runs.ref':
{
handler( v )
{
console.log("runs.ref watch", v );
},
immediate: true,
deep: true,
},
},
}
</script>
I can do
defineComponent({
props: {
name: { type: String as PropType<string> }
}
})
to tell vue3 that my props type is { name: string }, but if I have several component have the same props type, how can I share the defination?
If I define props in :
const props = {
name: String as PropType<string>
}
then use it like this:
defineComponent({
props: props,
})
It won't work, the type I got in setup function of props is not right.
this answer is purely addition to #Xinchao's answer.
one way is to destructure common props like following:
// taken from #Xinchao's answer
const commonProps = {
name: { type: String as PropType<string> }
}
defineComponent({
props:{
...commonProps,
extra: { }
}
})
another way is to write function which returns specific object like following
function getStringProp(required=false) {
return {
type: String as PropType<string>,
required,
default: '',
};
}
defineComponent({
props:{
name: getStringProp(true),
nickName: getStringProp(),
extra: { }
}
})
this case specifically come handy where prop is Array or Object; where we can cast the type like following
function getArrayProp<T>(required=false) {
return {
type: Array as PropType<T[]>,
required,
default: () => [],
};
}
defineComponent({
props:{
options: getArrayProp<Options>(true),
stringOptions: getArrayProp<string>(true),
}
})
The props options provided for defineComponent is a plain js object solely for type annotation purpose, so you can employ whatever technics in javascript for sharing structures between objects:
// in common.ts
export const commonProps = {
name: { type: String as PropType<string> }
}
// in your component.vue
import commonProps from "./common.ts";
defineComponent({
props:{
...commonProps,
extra: { }
}
})
If you're using the composition API with Vue 2 in preparation for switching to Vue 3, you have to use the PropType from that package instead of the vue package.
// Wrong for Vue 2
import Vue, { PropType } from 'vue'
import { defineComponent } from '#vue/composition-api'
// Right for Vue 2
import { defineComponent, PropType } from '#vue/composition-api'