Is there a way to share reactive data between random components in Vue 3 Composition API? - vue.js

Having some reactive const in "Component A," which may update after some user action, how could this data be imported into another component?
For example:
const MyComponent = {
import { computed, ref } from "vue";
setup() {
name: "Component A",
setup() {
const foo = ref(null);
const updateFoo = computed(() => foo.value = "bar");
return { foo }
}
}
}
Could the updated value of 'foo' be used in another Component without using provide/inject?
I am pretty new in the Vue ecosystem; kind apologies if this is something obvious that I am missing here.

One of the best things about composition API is that we can create reusable logic and use that all across the App. You create a composable functions in which you can create the logic and then import that into the components where you want to use it. Not only does this make your component much cleaner but also your APP much more maintainable. Below is a simple example of counter to show how they can be used. You can find working demo here:
Create a composable function for counter:
import { ref, computed } from "vue";
const counter = ref(0);
export const getCounter = () => {
const incrementCounter = () => counter.value++;
const decrementCounter = () => counter.value--;
const counterPositiveOrNegitive = computed(() =>
counter.value >= 0 ? " Positive" : "Negitive"
);
return {
counter,
incrementCounter,
decrementCounter,
counterPositiveOrNegitive
};
};
Then you can import this function into your components and get the function or you want to use. Component to increment counter.
<template>
<div class="hello">
<h1>Component To Increment Counter</h1>
<button #click="incrementCounter">Increment</button>
</div>
</template>
<script>
import { getCounter } from "../composables/counterExample";
export default {
name: "IncrementCounter",
setup() {
const { incrementCounter } = getCounter();
return { incrementCounter };
},
};
</script>
Component to decrement counter:
<template>
<div class="hello">
<h1>Component To Decrement Counter</h1>
<button #click="decrementCounter">Decrement</button>
</div>
</template>
<script>
import { getCounter } from "../composables/counterExample";
export default {
name: "DecrementCounter",
setup() {
const { decrementCounter } = getCounter();
return { decrementCounter };
},
};
</script>
Then in the main component, you can show the counter value.
<template>
<img alt="Vue logo" src="./assets/logo.png" />
<div class="counters">
<IncrementCounter />
<DecrementCounter />
</div>
<h3>Main Component </h3>
<p>Counter: {{ counter }}</p>
<p>{{ counterPositiveOrNegitive }}</p>
</template>
<script>
import IncrementCounter from "./components/IncrementCounter.vue";
import DecrementCounter from "./components/DecrementCounter.vue";
import { getCounter } from "./composables/counterExample";
export default {
name: "App",
components: {
IncrementCounter: IncrementCounter,
DecrementCounter: DecrementCounter,
},
setup() {
const { counter, counterPositiveOrNegitive } = getCounter();
return { counter, counterPositiveOrNegitive };
},
};
Hope this was somewhat helpful. You can find a working example here:
https://codesandbox.io/s/vue3-composition-api-blfpj

Related

Can't get v-model work with Composition API and Vuex

I've read several posts on stackoverflow and other websites, but still can't figure out what's going wrong in my case.
I'm building an app following composition api approach and using a variable called modelStartDate (which I initiate at Jan 3, 2022). This is how my store looks:
import { createStore } from 'vuex'
export default createStore({
state: {
modelStartDate: new Date(2022, 0, 3)
},
mutations: {
modelStartDateMutation(state, newDate) {
state.modelStartDate = newDate
}
},
actions: {
},
getters: {
},
modules: {
}
})
In the relevant Vue file, I have the following code snippet:
<template>
<nav class="left-bar">
<div class="block" id="modelStartDate">
<label>Model start date</label>
<input type="date" v-model="modelStartDateProxy" />
</div>
<p>{{ modelStartDate }}</p>
</nav>
</template>
<script>
import { ref } from '#vue/reactivity'
import { useStore } from 'vuex'
import { computed } from '#vue/runtime-core'
export default {
setup() {
const store = useStore()
const modelStartDateProxy = computed({
get: () => store.state.modelStartDate,
set: (newDate) => store.commit("modelStartDateMutation", newDate)
})
const modelStartDate = store.state.modelStartDate
return { modelStartDateProxy, modelStartDate }
}
}
</script>
When I run the page, the paragraph tag prints the right date, however the input tag, where the user can change the date, is empty (I was expecting Jan 3, 2022 to be pre-selected). When the date is changed, nothing seems to change in the app. I'm getting no errors. Any idea what I'm doing incorrectly?
Also, can I access store's modelStartDate state without having to define it separately (redundantly?) in the vue setup() section?
First, I don't know which tutorial you read. But to me, the problem is here:
const modelStartDateProxy = computed({
get: () => store.state.modelStartDate,
set: (newDate) => store.commit("modelStartDateMutation", newDate)
})
const modelStartDate = store.state.modelStartDate
The snippet
const modelStartDateProxy = computed({
get: () => store.state.modelStartDate,
set: (newDate) => store.commit("modelStartDateMutation", newDate)
})
is weird to me.
Duplicate of store.state.modelStartDate. DRY.
<p>{{ modelStartDate }}</p> render data from const modelStartDate = store.state.modelStartDate. But the data was only assign once. So the new value was not render on input was changed.
Solution:
const modelStartDate = computed(() => store.state.modelStartDate);
You can take a look at this playground.
The html element input returns a string: "YYYY-MM-DD". Therefore you need the syntax new Date(value)
Take a look at this playground
<template>
<label>Model start date</label>
<input type="date" v-model="modelStartDateProxy" />
<p>{{ modelStartDateProxy }}</p>
</template>
<script>
import { store } from './store.js' //mock-up store
import { ref, computed } from 'vue'
export default {
setup() {
const modelStartDateProxy = computed({
get: () => store.state.modelStartDate,
set: (newDate) => store.commit(newDate) // Use real Vuex syntax
})
return { modelStartDateProxy }
}
}
</script>
//Mock-up Store (not real vuex)
import {reactive} from 'vue'
export const store = reactive({
state: {
modelStartDate: new Date(2022, 0, 3)
},
commit: (value) => store.state.modelStartDate = new Date(value) // new Date(value)
})

Vue 3 - template ref with computed?

How may I focus on this simple input example?
Should I create one more variable const nameRef = ref(null) or there is more beauty way to resolve this?
<template>
<input ref="name" :value="name" />
</template>
<script>
import {ref, computed} from 'vue';
export default {
props: ['name'],
setup(props) {
const name = computed(() => someTextPrepare(props.name));
// how can I do name.value.focus() for example?
return { name }
}
}
</script>
Try to wrap the name value and ref in reactive property :
<template>
<input :ref="theName.ref" :value="theName.value" />
</template>
<script>
import {ref, computed,reactive} from 'vue';
export default {
props: ['name'],
setup(props) {
const theName=reactive({
value:computed(() => someTextPrepare(props.name)),
ref: ref(null)
})
return { theName }
}
}
</script>

Watch doesn't get fire/trigger when global property change

I’m very new to Vue and I begin with Vue 3. I was trying to migrate a template from Vue 2 to Vue 3 so I can start with my project.
I have this plugin file.
Sidebar\Index.ts
import SidebarPlugComp from './SidebarPlugComp.vue'
import SidebarLinkPlugComp from './SidebarLinkPlugComp.vue'
// tiny internal plugin store
const SidebarStore = {
showSidebar: false,
sidebarLinks: [],
displaySidebar (value: boolean) {
this.showSidebar = value
}
}
const SidebarPlugin = {
install (app: any) {
app.config.globalProperties.$sidebar = SidebarStore
app.component('side-bar-plug-comp', SidebarPlugComp)
app.component('sidebar-link-plug-comp', SidebarLinkPlugComp)
}
}
export default SidebarPlugin
Also I have a BaseTopNavLay layout file so I can toggle the sidebar with handleSidebarToggle onclick button method
<template>
\\...
<div class="navbar-toggle d-inline" :class="{toggled: $sidebar.showSidebar}">
<button type="button"
class="navbar-toggler"
aria-label="Navbar toggle button"
#click.prevent="handleSidebarToggle">
<span class="navbar-toggler-bar bar1"></span>
<span class="navbar-toggler-bar bar2"></span>
<span class="navbar-toggler-bar bar3"></span>
</button>
\\ ...
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { ModalComp } from '../components'
export default defineComponent({
name: 'BaseTopNavLay',
components: {
ModalComp
},
// ...
methods: {
handleSidebarToggle (): void {
this.$sidebar.displaySidebar(!this.$sidebar.showSidebar)
},
handleHideSideBar (): void {
this.$sidebar.displaySidebar(false)
},
}
})
</script>
And here is the watch in the App.vue file
<template>
<component :is="this.$route.meta.layout || 'div'">
<router-view />
</component>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'Application',
methods: {
toggleNavOpen () {
console.log('here')
let root = document.getElementsByTagName('html')[0]
root.classList.toggle('nav-open')
}
},
/*watch: {
'$sidebar.showSidebar': function(newVal, oldVal) {
console.log(newVal, oldVal)
}
}*/
mounted () {
//#ts-ignore
this.$watch('$sidebar.showSidebar', this.toggleNavOpen)
}
})
</script>
Wherever I test the var this.$sidebar.showSidebar I can access to its value properly. Also, the onclick method is changing the SidebarStore object in Sidebar\Index.ts plugin file.
Can anyone give me a hint what am I missing here? Why the watch doesn't get fired. Thanks in advance.
The problem is that you have not made your $sidebar reactive, and a watch needs to use a reactive variable.
You can keep the store where you have it, but I'd put it into a separate file (store.js) and import where needed, no need to put it on app.config.globalProperties.$sidebar (but that might be a personal preference
// store.js
// using reactive (all values)
export const SidebarStore = Vue.reactive({
showSidebar: false,
sidebarLinks: [],
})
// or using ref (one for each)
// export const showSidebar = Vue.ref(false);
export const displaySidebar = (value: boolean) => {
SidebarStore.showSidebar.value = value;
}
this will make SidebarStore and displaySidebar available anywhere in your code
use like this
<template>
\\...
<div class="navbar-toggle d-inline" :class="{toggled: $sidebar.showSidebar}">
<button type="button"
class="navbar-toggler"
aria-label="Navbar toggle button"
#click.prevent="handleSidebarToggle">
<span class="navbar-toggler-bar bar1"></span>
<span class="navbar-toggler-bar bar2"></span>
<span class="navbar-toggler-bar bar3"></span>
</button>
\\ ...
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { ModalComp } from '../components'
import { SidebarStore, displaySidebar } from '../store'
export default defineComponent({
name: 'BaseTopNavLay',
components: {
ModalComp
},
// ...
methods: {
handleSidebarToggle (): void {
displaySidebar(!SidebarStore.showSidebar)
},
handleHideSideBar (): void {
displaySidebar(false)
},
}
})
</script>

How to use InfiniteLoading in vuex (error)

I'm a beginner to vuex and vue, I need to use a infinite-loading in vuex project, but it is not working, I need some experts' help. I will provide details as below:
my vuex code in index.js
import axios from 'axios';
const state = {
productItems_bottom: []
}
const mutations = {
UPDATE_PRODUCT_ITEMS_bottom (state, payload) {
state.productItems_bottom = payload;
}
}
const actions = {
getProductItems_bottom ({ commit },lastpage) {
axios.get('http://127.0.0.1:8000/api/standstop?page='+lastpage).then((response) => {
commit('UPDATE_PRODUCT_ITEMS_bottom', response.data)
});
}
}
const getters = {
productItems_bottom: state => state.productItems_bottom,
last_page: state=> state.productItems_bottom.last_page
}
const index_page_Bottom_Module = {
state,
mutations,
actions,
getters
}
export default index_page_Bottom_Module;
And my vue code display here, but something's wrong.
<template>
<section id="product-item_bottom" v-if="productItems_bottom">
<div class="columns is-multiline is-mobile">
<div v-for="productItem_bottom in productItems_bottom" :key="productItem_bottom.id" class="column is-one-third">
<figure class="image"><img v-bind:src="'http://localhost/plategea%20laravel%20and%20vue/plategea/plategea/public/storage/'+productItem_bottom.src">
<span class="tag is-primary">{{ productItem_bottom.title }}</span>
</figure>
</div>
</div>
<infinite-loading #distance="1" #infinite="infiniteHandler"/>
</section>
</template>
<script>
import InfiniteLoading from 'vue-infinite-loading';
import {mapGetters } from 'vuex';
let lastpage=1;
export default {
name:'ProductItem_bottom',
computed: {
...mapGetters(['productItems_bottom','last_page'])
},
created(){
},
methods:{
infiniteHandler($state) {
this.$store.dispatch('getProductItems_bottom',lastpage);
this.$store.commit({ type:'UPDATE_PRODUCT_ITEMS_bottom' });
console.log(this.last_page);
if (lastpage<=this.last_page) {
$state.loaded();
lastpage +=1;
}
if(lastpage > this.last_page){
$state.complete();
}
}
},
components: {
InfiniteLoading,
}
}
</script>
And my api
public function showStandstop()
{
$stands=Stands::where('state',1)->orderBy('trend', 'desc')->paginate(3);
return response()->json($stands, 200);
}
but InfiniteLoading not working! I don't know what is the problem & where should I look for, do you have any idea what shall I change & work on?
thank you in advance I'm looking forward to seeing your responds.
I think your not assigning the payload to the productItems_bottom.last_page anywhere.
UPDATE_PRODUCT_ITEMS_bottom (state, payload) {
state.productItems_bottom = payload;
}
last_page: state=> state.productItems_bottom.last_page
can you check that and let me know if is working...

Is that the right way to set an id in Vue.js component?

I am trying to integrate Phaser 3 with Vue.js 2.
My goal is to create a Vue.js component associated with a game canvas.
My initial solution was:
<template>
<div :id="id">
</div>
</template>
<script>
import Phaser from 'phaser'
export default {
data () {
return {
id: null,
game: null
}
},
mounted () {
this.id = 'game' + this._uid
var config = {
parent: this.id,
type: Phaser.CANVAS
}
this.game = new Phaser.Game(config)
....
}
}
</script>
This code attach the game canvas to my template. But to my surprise it only worked 'sometimes'.
I figured out, after hours of debugging, that my div element in the DOM wasn't updated with the id when I was instantiating my new Game.
So I came up with the solution of instantiating the id in the beforeMount () method as follow:
<template>
<div :id="id">
</div>
</template>
<script>
import Phaser from 'phaser'
export default {
data () {
return {
id: null,
game: null
}
},
beforeMount () {
this.id = 'game' + this._uid
},
mounted () {
var config = {
parent: this.id,
type: Phaser.CANVAS
}
this.game = new Phaser.Game(config)
....
}
}
</script>
It is working, but I would like to know if there is a more simple and elegant solution ?
One better solution for integrating Phaser.Game into the application is directly passing the config the HTML element, a configuration supported by Phaser.Game.
To get a reference to a HTML element in vue, you can use refs, these are basically id's, but local to the component itself, so there is not risk in creating conflicts.
<template>
<div ref="myDiv">
</div>
</template>
<script>
import Phaser from 'phaser'
export default {
data () {
return {
game: null
}
},
mounted () {
var config = {
parent: this.$refs.myDiv,
type: Phaser.CANVAS
}
this.game = new Phaser.Game(config)
....
}
}
</script>
Vue3 sample:
<script setup>
import { ref,onMounted } from 'vue';
import Phaser from 'phaser'
const myDiv = ref(null)
let canvasWidth = 750;
let canvasHeight = 1450;
onMounted(() => {
const config = {
type: Phaser.AUTO,
parent: popWrap.value,
width: canvasWidth,
height: canvasHeight,
scene: {
preload: preload,
create: create,
update: update
}
};
const game = new Phaser.Game(config);
})
</script>
<template>
<div ref="myDiv">
</div>
</template>