I want to ask how do I rewrite vue js variable data when I use pusher on vue js.
In this case the pusher I have will change the data every 5 minutes but here I don't rewrite the previous variable.
Usually I only use:
<template>
<div class="animated fadeIn">
<b-card>
<b-card-header>
Record User
</b-card-header>
<b-card-body>
<div>
<h3>Name : {{ name }}</h3>
<h4>Email : {{ email }}</h4>
</div>
</b-card-body>
</b-card>
</div>
</template>
<script>
import Pusher from 'pusher-js'
export default {
name: 'Index',
data() {
return {
name: 'John Doe',
email: 'jdoe#gmail.com'
}
},
created () {
this.subscribe()
},
methods: {
subscribe () {
let pusher = new Pusher(process.env.VUE_APP_PUSHER_KEY, { cluster: 'ap1', forceTLS: true })
pusher.subscribe('users')
pusher.bind('list', data => {
console.log(data);
this.name = data.name
this.email = data.email
})
},
},
}
</script>
But it hasn't changed, please help.
Thank you
The problem is that pusher will append it's own context during bind. There is a way to get around it though
bind function allows you to pass the context as the 3rd parameter. You can pass this after the handler like this:
subscribe () {
let pusher = new Pusher(process.env.VUE_APP_PUSHER_KEY, { cluster: 'ap1', forceTLS: true })
pusher.subscribe('users')
pusher.bind('list', data => {
console.log(data);
this.name = data.name
this.email = data.email
}, this) // <=== pass this as context
},
ref: https://pusher.com/docs/channels/using_channels/events#binding-with-optional-this-context
if that doesn't work, you can also use the that var, which should escape the context issue.
subscribe () {
let that = this;
let pusher = new Pusher(process.env.VUE_APP_PUSHER_KEY, { cluster: 'ap1', forceTLS: true })
pusher.subscribe('users')
pusher.bind('list', data => {
console.log(data);
that.name = data.name
that.email = data.email
})
},
You might want to try the vue-pusher library which might handle the context to be more vue-friendly.
https://www.npmjs.com/package/vue-pusher
Why does that work?
there's nothing special about that, but in javascript this is a special variable that references the context. In some cases, when dealing with callback functions, the context changes. assigning this to a new variable that, stores the context of the vue method in a variable that you can then reference it even if, in this case, Pusher bind function binds a different context.
Related
The following code works and I can see the output as intended when use ref, but when using reactive, I see no changes in the DOM. If I console.log transaction, the data is there in both cases. Once transaction as a variable changes, should the changes not be reflected on the DOM in both cases?
I'm still trying to wrap my head around Vue 3's composition API and when to use ref and reactive. My understanding was that when dealing with objects, use reactive and use ref for primitive types.
Using ref it works:
<template>
{{ transaction }}
</template>
<script setup>
import { ref } from 'vue'
let transaction = ref({})
const getPayByLinkTransaction = () => {
axios({
method: "get",
url: "pay-by-link",
params: {
merchantUuid: import.meta.env.VITE_MERCHANT_UUID,
uuid: route.params.uuid,
},
})
.then((res) => {
transaction.value = res.data
})
.catch((e) => {
console.log(e)
})
}
getPayByLinkTransaction()
</script>
Using reactive it doesn't work:
<template>
{{ transaction }}
</template>
<script setup>
import { reactive } from 'vue'
let transaction = reactive({})
const getPayByLinkTransaction = () => {
axios({
method: "get",
url: "pay-by-link",
params: {
merchantUuid: import.meta.env.VITE_MERCHANT_UUID,
uuid: route.params.uuid,
},
})
.then((res) => {
transaction = { ...res.data }
})
.catch((e) => {
console.log(e)
})
}
getPayByLinkTransaction()
</script>
Oh, when you do transaction = { ...res.data } on the reactive object, you override it, like you would with any other variable reference.
What does work is assigning to the reactive object:
Object.assign(transaction, res.data)
Internally, the object is a Proxy which uses abstract getters and setters to trigger change events and map to the associated values. The setter can handle adding new properties.
A ref() on the other hand is not a Proxy, but it does the same thing with its .value getter and setter.
From what I understand, the idea of reactive() is not to make any individual object reactive, but rather to collect all your refs in one single reactive object (somewhat similar to the props object), while ref() is used for individual variables. In your case, that would mean to declare it as:
const refs = reactive({transaction: {}})
refs.transaction = { ...res.data }
The general recommendation seems to be to pick one and stick with it, and most people seem to prefer ref(). Ultimately it comes down to if you prefer the annoyance of having to write transaction.value in your script or always writing refs.transaction everywhere.
With transaction = { ...res.data } the variable transaction gets replaced with a new Object and loses reactivity.
You can omit it by changing the data sub-property directly or by using ref() instead of reactivity()
This works:
let transaction = ref({})
transaction.data = res.data;
Check the Reactivity in Depth and this great article on Medium Ref() vs Reactive() in Vue 3 to understand the details.
Playground
const { createApp, ref, reactive } = Vue;
const App = {
setup() {
const transaction1 = ref({});
let transaction2 = reactive({ data: {} });
const res = { data: { test: 'My Test Data'} };
const replace1 = () => {
transaction1.value = res.data;
}
const replace2 = () => {
transaction2.data = res.data;
}
const replace3 = () => {
transaction2.data = {};
}
return {transaction1, transaction2, replace1, replace2, replace3 }
}
}
const app = Vue.createApp(App);
app.mount('#app');
#app { line-height: 2; }
[v-cloak] { display: none; }
<div id="app">
transaction1: {{ transaction1 }}
<button type="button" #click="replace1()">1</button>
<br/>
transaction2: {{ transaction2 }}
<button type="button" #click="replace2()">2</button>
<button type="button" #click="replace3()">3</button>
</div>
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
Since reactive transaction is an object try to use Object.assign method as follows :
Object.assign(transaction, res.data)
I'm trying to add a custom view with some administrative utilities to Spring Boot Admin. The idea is to implement these as endpoints in Springboot Admin and call these endpoints from my custom view, but I don't know how to make a call to the server itself.
When a custom view has parent: 'instances' it will get an axios client for connecting to the current instance, but since the view I'm building isn't tied to a specific instance it doesn't have this. I'm aware I can install axios as a dependency, but I'd like to avoid that if possible to reduce build times. Since SBA itself depends on axios it seems I shouldn't have to install it myself.
Based on this sample, this is what I have right now:
index.js
/* global SBA */
import example from './example';
import exampleEndpoint from './example-endpoint';
SBA.use({
install({viewRegistry}) {
viewRegistry.addView({
name: 'example',
path: '/example',
component: example,
label: 'Example',
order: 1000,
});
}
});
example.vue
<template>
<div>
<h1>Example View</h1>
<p>
<b>GET /example:</b> <span v-text="exampleResponse" />
</p>
</div>
</template>
<script>
export default {
props: {
applications: {
type: Array,
required: true
}
},
data: () => ({ exampleResponse: "No response" }),
async created() {
const response = await this.axios.get("example");
this.exampleResponse = response.response;
},
};
</script>
ExampleController.kt
#RestController
#RequestMapping("/example")
class ExampleController {
#GetMapping
fun helloWorld() = mapOf("response" to "Hello world!")
}
Console says that it can't read property get of undefined (i.e. this.axios is undefined). Text reads "GET /example: No response"
I'm not sure if this is the best way to do it, but it is a way.
I noticed that I do have access to the desired axios instance within the SBA.use { install(...) { } } block, and learned that this can be passed as a property down to the view.
index.js
/* global SBA */
import example from './example';
import exampleEndpoint from './example-endpoint';
SBA.use({
install({viewRegistry, axios}) {
viewRegistry.addView({
name: 'example',
path: '/example',
component: example,
label: 'Example',
order: 1000,
// this is where we pass it down with the props
// first part is the name, second is the value
props: { "axios": axios },
});
}
});
example.vue
<template>
<div>
<h1>Example View</h1>
<p>
<b>GET /example:</b> <span v-text="exampleResponse" />
</p>
</div>
</template>
<script>
export default {
props: {
applications: { type: Array, required: true },
// this is where we retrieve the prop. the name of the field should
// correspond to the name given above
axios: { type: Object, required: true },
},
data: () => ({
exampleResponse: "No response",
}),
async created() {
// Now we can use our axios instance! And it will be correctly
// configured for talking to Springboot Admin
this.axios.get("example")
.then(r => { this.exampleResponse = r.data.response; })
.catch(() => { this.exampleResponse = "Request failed!" });
},
};
</script>
Based on the code given, it looks like you don't have axios initialized to how you want to use it.
You're calling it via this.axios but it's not in your component i.e
data() {
return {
axios: require("axios") // usually this is imported at the top
}
}
or exposed globally i.e
Vue.prototype.axios = require("axios")
You can simply just import axios and reference it.
<script>
import axios from 'axios';
export default {
created() {
axios.get()
}
}
</script>
I have typical scenario where I call REST API in vuex actions to fetch some data and then I commit that to mutation.
I use async/await syntax and try/catch/finally blocks. My vuex module looks something like this:
const state = {
users: null,
isProcessing: false,
operationError: null
}
const mutations = {
setOperationError (state, value) {
state.operationError = value
},
setIsProcessing (state, value) {
state.isProcessing = value
if (value) {
state.operationError = ''
}
},
setUsers(state, value) {
state.users= value
}
}
const actions = {
async fetchUsers ({ commit }) {
try {
commit('setIsProcessing', true)
const response = await api.fetchUsers()
commit('setUsers', response.result)
} catch (err) {
commit('setUsers', null)
commit('setOperationError', err.message)
} finally {
commit('setIsProcessing', false)
}
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
Notice that I handle catch(err) { } in vuex action and don’t rethrow that error. I just save error message in the state and then bind it in vue component to show it if operationError is truthy. This way I want to keep vue component clean from error handling code, like try/catch.
I am wondering is this right pattern to use? Is there a better way to handle this common scenario? Should I rethrow error in vuex action and let it propagate to the component?
What I usually do, is have a wrapper around the data being posted, that handles the api requests and stores errors. This way your users object can have the errors recorded on itself and you can use them in the components if any of them are present.
For example:
import { fetchUsers } from '#\Common\api'
import Form from '#\Utils\Form'
const state = {
isProcessing: false,
form: new Form({
users: null
})
}
const mutations = {
setIsProcessing(state, value) {
state.isProcessing = value
},
updateForm(state, [field, value]) {
state.form[field] = value
}
}
const actions = {
async fetchUsers ({ state: { form }, commit }) {
let users = null
commit('setIsProcessing', true)
try {
users = await form.get(fetchUsers);
} catch (err) {
// - handle error
}
commit('updateForm', ['users', users])
commit('setIsProcessing', false)
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
Then in the component you can use the errors object on the wrapper like so:
<template>
<div>
<div class="error" v-if="form.erros.has('users')">
{{ form.errors.get('users') }}
</div>
<ul v-if="users">
<li v-for="user in users" :key="user.id">{{ user.username }}</li>
</ul>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
computed: {
...mapState('module' ['form']),
users () {
return this.form.users
}
}
</script>
This is just my personal approach that I find very handy and it served me well up to now. Don't know if there are any standard patterns or if there is an explicit "correct way" to do this.
I like the wrapper approach, because then your errors become automatically reactive when a response from api returns an error.
You can re-use it outside vuex or even take it further and inject the errors into pre-defined error boundaries which act as wrapper components and use the provide/inject methods to propagate error data down the component tree and display them where ever you need them to show up.
Here's an example of error boundary component:
<template>
<div>
<slot></slot>
</div>
</template>
<script>
export default {
props: {
module: {
type: String,
required: true,
validator: function (value) {
return ['module1', 'module2'].indexOf(value) !== -1
}
},
form: {
type: String,
default: 'form'
}
},
provide () {
return {
errors: this.$store.state[this.module][this.form].errors
}
}
}
</script>
Wrap some part of the application that should receive the errors:
<template>
<div id="app">
<error-boundary :module="module1">
<router-view/>
</error-boundary>
</div>
</template>
Then you can use the errors from the users wrapper in child components like so:
If you have a global error like no response from api and want to display it in the i.e.: sidebar
<template>
<div id="sidebar">
<div v-if="errors.has('global')" class="error">
{{ errors.get('global').first() }}
</div>
...
</div>
</template>
<script>
export default {
inject: [
'errors'
],
...
}
</script>
And the same error object re-used somewhere inside a widget for an error on the users object validation:
<template>
<div id="user-list">
<div v-if="errors.has('users')" class="error">
{{ errors.get('users').first() }}
</div>
...
</div>
</template>
<script>
export default {
inject: [
'errors'
],
...
}
</script>
Jeffrey Way did a series on Vue2 a while ago and he proposed something similar. Here's a suggestion on the Form and Error objects that you can build upon: https://github.com/laracasts/Vue-Forms/blob/master/public/js/app.js
Using Vue TreeSelect Plugin to load a nested list of nodes from firebase backend. It's doc page says,
It's also possible to have root level options to be delayed loaded. If no options have been initially registered (options: null), vue-treeselect will attempt to load root options by calling loadOptions({ action, callback, instanceId }).
loadOptions (in my App.vue) dispatch vuex action_FolderNodesList, fetches (from firebase) formats (as required by vue-treeselect), and mutates the state folder_NodesList, then tries to update options this.options = this.get_FolderNodesList but this does not seems to work.
Here is the loadOptions method (in app.vue)
loadOptions() {
let getFolderListPromise = this.$store.dispatch("action_FolderNodesList");
getFolderListPromise.then(_ => {
this.options = this.get_FolderNodesList;
});
}
Vue errors out with Invalid prop: type check failed for prop "options". Expected Array, got String with value ""
I am not sure what am I doing wrong, why that does not work. A working Codesandbox demo
Source
App.vue
<template>
<div class="section">
<div class="columns">
<div class="column is-7">
<div class="field">
<Treeselect
:multiple="true"
:options="options"
:load-options="loadOptions"
:auto-load-root-options="false"
placeholder="Select your favourite(s)..."
v-model="value" />
<pre>{{ get_FolderNodesList }}</pre>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from "vuex";
import Treeselect from "#riophae/vue-treeselect";
import "#riophae/vue-treeselect/dist/vue-treeselect.css";
export default {
data() {
return {
value: null,
options: null,
called: false
};
},
components: {
Treeselect
},
computed: mapGetters(["get_FolderNodesList"]),
methods: {
loadOptions() {
let getFolderListPromise = this.$store.dispatch("action_FolderNodesList");
getFolderListPromise.then(_ => {
this.options = this.get_FolderNodesList;
});
}
}
};
</script>
Store.js
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export const store = new Vuex.Store({
state: {
folder_NodesList: ""
},
getters: {
get_FolderNodesList(state) {
return state.folder_NodesList;
}
},
mutations: {
mutate_FolderNodesList(state, payload) {
state.folder_NodesList = payload;
}
},
actions: {
action_FolderNodesList({ commit }) {
fmRef.once("value", snap => {
var testObj = snap.val();
var result = Object.keys(testObj).reduce((acc, cur) => {
acc.push({
id: cur,
label: cur,
children: recurseList(testObj[cur])
});
return acc;
}, []);
commit("mutate_FolderNodesList", result);
});
}
}
});
Any help is appreciated.
Thanks
It seems you are calling this.options which would update the entire element while only the current expanding option should be updated.
It seems loadOptions() is called with some arguments that you can use to update only the current childnode. The first argument seems to contain all the required assets so I wrote my loadTreeOptions function like this:
loadTreeOptions(node) {
// On initial load, I set the 'children' to NULL for nodes to contain children
// but inserted an 'action' string with an URL to retrieve the children
axios.get(node.parentNode.action).then(response => {
// Update current node's children
node.parentNode.children = response.data.children;
// notify tree to update structure
node.callback();
}).catch(
errors => this.onFail(errors.response.data)
);
},
Then I set :load-options="loadTreeOptions" on the <vue-treeselect> element on the page. Maybe you were only missing the callback() call which updates the structure. My installation seems simpler than yours but it works properly now.
When I update the parent's singleIssue variable, it does not get updated inside my <issue> component. I am passing it there using props. I have achieved this in other projects already, but I can't seem to get what I am doing wrong.
I have reduced my code to the relevant parts, so it is easier to understand.
IssueIndex.vue:
<template>
<div class="issue-overview">
<issue v-if="singleIssue" :issue="singleIssue"></issue>
<v-server-table url="api/v1/issues" :columns="columns" :options="options" ref="issuesTable">
<span slot="name" slot-scope="props">{{props.row.name}}</span>
<div slot="options" slot-scope="props" class="btn-group" role="group" aria-label="Order Controls">
<b-btn class="btn-success" v-b-modal.issueModal v-
on:click="showIssue(props.row)">Show</b-btn>
</div>
</v-server-table>
</div>
</template>
<script>
export default {
mounted() {
let app = this;
axios.get('api/v1/issues/')
.then(response => {
app.issues = response.data;
})
.catch(e => {
app.errors.push(e);
});
},
data: () => {
return {
issues: [],
singleIssue: undefined,
columns: ['name', 'creation_date', 'options'],
options: {
filterByColumn: true,
filterable: ['name', 'creation_date'],
sortable: ['name', 'creation_date'],
dateColumns: ['creation_date'],
toMomentFormat: 'YYYY-MM-DD HH:mm:ss',
orderBy: {
column: 'name',
ascending: true
},
initFilters: {
active: true,
}
}
}
},
methods: {
showIssue(issue) {
let app = this;
app.singleIssue = issue;
// This gets the action history of the card
axios.get('api/v1/issues/getCardAction/' + issue.id)
.then(response => {
app.singleIssue.actions = response.data;
})
.catch(error => {
// alert
});
}
}
}
</script>
Issue.vue:
<template>
<div>
{{ issue }}
</div>
</template>
<script>
export default {
props: ['issue']
}
</script>
So after showIssue() is triggered, it will get actions for the issue. But after then, I can't see the actions in the issue component.
If I update the issue-model in the issue component using form inputs, it will also start showing the actions. So I assume it's just in a weird state where it needs a refresh.
Thanks in advance!
If the singleIssue.actions property does not exist at the time when you're setting it, Vue will not be able to detect it. You need to use $set, or just define the property before you assign singleIssue to app.
Change this:
app.singleIssue = issue;
to this:
issue.actions = undefined;
app.singleIssue = issue;
The app.singleIssue property is reactive (because it was declared in the data section), so Vue will detect when this property is assigned to and make the new value reactive if it isn't already. At the time when issue is being assigned, it will be made reactive without the actions property, and Vue cannot detect when new properties are being added to reactive objects later on (hence why $set is required for those situations).