Vue3 - after provide/inject object inside component is loosing data - vue-router

I'm having small issue with provide/inject in my project.
In App.vue, I'm pulling data from DB and pushing it into object. With console log I checked and all data it's there.
<template>
<router-view />
</template>
<script>
export default {
provide() {
return {
user: this.user,
};
},
data() {
return {
user: '',
};
},
methods: {
///pulling data from DB
func() {
fetch("url")
.then((response) => {
if (response.ok) {
return response.json();
}
})
.then((data) => {
const user = [];
for (const id in data) {
user.push({
id: data[id].user_id,
firstName: data[id].user_firstname,
lastName: data[id].user_lastname,
email: data[id].user_email,
phone: data[id].user_phone,
address1: data[id].user_address_1,
address2: data[id].user_address_2,
address3: data[id].user_address_3,
address4: data[id].user_address_4,
group: data[id].user_group,
});
}
this.user = user;
})
.catch((error) => {
console.log(error);
});
},
},
created() {
this.func();
},
};
</script>
Console log of object user App.vue
Object { id: "3", firstName: "test", lastName: "test", … }
Next I'm injecting it into component. Object inside component exists, but empty - all data cease to exist.
<script>
export default {
inject: ["user"],
};
</script>
console log of object user in component
<empty string>
While in App.vue data is still there, in any components object appears to be empty, but it is there. Any idea why?
Thanks for help.

In short, this happens because you are reassigning user rather than changing user.
Let's say you have a Child component that consumes your inject data and renders the users in a list:
<template>
<div> Child </div>
<ul>
<li v-for="user in users" :key="user.id"> {{user.name}} </li>
</ul>
</template>
<script>
import {inject} from "vue";
export default {
name: "Child",
setup() {
const users = inject("users");
return {users};
}
}
</script>
To provide the users from parent component, all you need to ensure is that users itself is a reactive object, and you keep changing it from the parent rather than reassigning it.
I am going to use the composition api to illustrate what I mean. Compared to options api, everything in composition api is just plain javascript hence there is a lot less behind-the-scene magic. At the end I will tell you how options api is related to the composition api.
<template>
<button #click=generateUsers>
Generate Users
</button>
<Child/>
</template>
<script>
import {reactive, provide, toRefs} from "vue";
import Child from "./Child.vue";
export default {
name: "App",
components: {
Child
},
setup() {
const data = reactive({users: ""});
const generateUsers = () => {
// notice here you are REASSIGNING the users
data.users = [
{id: 1, name: "Alice"}, {id: 2, name: "Bob"}
];
console.log(data.users);
}
// this way of provide will NOT work
provide("users", data.users);
// this way works because of toRefs
const {users} = toRefs(data);
provide("users", users);
return {generateUsers};
}
}
</script>
A few things to note:
the data options in the options api is exactly the same as const data = reactive({users: ""}). Vue will run your data() method, from where you have to return a plain object. And then Vue will automatically call reactive to add reactivity to it.
provide, on the other hand, is not doing any magic - neither in options api, nor in the composition api. It just passes whatever it is given to the consuming component without any massaging.
the reason provide("users", data.users) does not work as you would expect is that the way you populate the users is not a change to the same data.users object (which actually is reactive), but a reassign all together.
the reason toRefs works is because toRefs links to the original parent.
With this understanding in mind, to fix your original code, you just need to ensure you change, instead of reassigning, the users. The simplest way is to define user as an array and push into it when you load data. (in contrast to defining it initially as a string and reassigning it later)
P.S. what also works in composition api, and is a lot simpler, is to:
<template>
<button #click=generateUsers>
Generate Users
</button>
<Child/>
</template>
<script>
import {ref, provide} from "vue";
import Child from "./Child.vue";
export default {
name: "App",
components: {
Child
},
setup() {
const users = ref();
const generateUsers = () => {
// notice here you are not reassigning the users
// but CHANGING its value
users.value = [
{id: 1, name: "Alice"}, {id: 2, name: "Bob"}
];
console.log(users.value);
}
provide("users", users);
return {generateUsers};
}
}
</script>

Related

Making request to Spring Boot Admin server from custom view?

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>

Fill vue multiselect dropdown from another component

I am currently working on a project and could use some help.
I have a backend with an endpoint which delivers an array of strings with approximately 13k entries. I created a component in DropdownSearch.vue which should be used on several different views with differing inputs. For this specific purpose I used vueform/multiselect. If I only try to add the dropdown without any information it works perfectly. Also if I access the endpoints and console.log() it it will work properly and deliver me an output. But if I try to initialize the output to the dropdown the whole page will stop working, the endpoint won't give me a response and the application freezes.
DropdownSearch.vue
<div>
<Multiselect
class="float-left"
v-model="valueDropdownOne"
mode="tags"
:placeholder="selectorOne"
:closeOnSelect="false"
:searchable="true"
:createTag="true"
:options="dropdownOne"
:groups="true"
/>
<Multiselect
class="float-left"
v-model="valueDropdownTwo"
mode="tags"
:placeholder="selectorTwo"
:closeOnSelect="false"
:searchable="true"
:createTag="true"
:options="dropdownTwo"
/>
<Multiselect
class="float-left"
v-model="valueDropdownThree"
mode="tags"
:placeholder="selectorThree"
:closeOnSelect="false"
:searchable="true"
:createTag="true"
:options="dropdownThree"
/>
</div>
</template>
<script>
import Multiselect from "#vueform/multiselect";
import { ref }from "vue"
export default {
name: "DropdownSearch",
components: { Multiselect },
props: {
selectorOne: {
type: String,
default: "<DEFAULT VALUE>",
required: true,
},
selectorTwo: {
type: String,
default: "<DEFAULT VALUE>",
required: true,
},
selectorThree: {
type: String,
default: "<DEFAULT VALUE>",
required: true,
},
dropdownOne: {
type: Array
}
,
dropdownTwo: {
type: Array
},
dropdownThree: {
type: Array
}
},
setup() {
const valueDropdownOne = ref()
const valueDropdownTwo = ref()
const valueDropdownThree = ref()
return {valueDropdownOne, valueDropdownTwo, valueDropdownThree}
}
};
</script>
<style src="#vueform/multiselect/themes/default.css"></style>
Datenbank.vue
<template>
<div>
<DropdownSearch
selectorOne="Merkmale auswählen"
:dropdownOne="dropdownOne"
selectorTwo="Monographien auswählen"
:dropdownTwo="dropdownTwo"
selectorThree="Orte auswählen"
:dropdownThree="dropdownThree"
></DropdownSearch>
</div>
</template>
<script>
import DropdownSearch from "../components/DropdownSearch.vue";
import { ref, onMounted } from "vue";
export default {
components: { DropdownSearch },
setup() {
const dropdownOne = ref([]);
const dropdownTwo = ref([]);
const dropdownThree = ref([]);
const getPlaces = async () => {
const response = await fetch("http://127.0.0.1:5000/project/get-places");
const places = await response.json();
return places;
};
onMounted(async () => {
const places = await getPlaces();
dropdownThree.value = places;
});
return {
dropdownOne,
dropdownTwo,
dropdownThree
};
},
};
</script>
<style lang="scss" scoped></style>
it is not the problem of vue
the library you used may not support virtual-list, when the amount of data becomes large, the actual dom element will also become large
you may need to find another library support virtual-list, only render dom in visual range or implement a custom component by a virtual-library
I found a solution to the given problem, as #math-chen already stated the problem is the amount of data which will resolve in the actual Dom becoming really large. Rather than using virtual-lists, you can limit the amount of entries displayed which can easily be done by adding
limit:"10"
to the multiselect component, filtering all items can easily be handled by javascript itself.

How to wrap the slot in a scoped slot at runtime

I have an very unusual scenario.
<WrapperComponent>
<InnerComponent propA="Static"></InnerComponent>
</WrapperComponent>
The WrapperComponent should manage all instances of the InnerComponent. However I don't know what will be compiled into the wrapper component.
Usually I would do something like this:
<WrapperComponent>
<template scoped-slot="{data}">
<InnerComponent v-for="(propB) in data" prop-b="Static" :prop-b="propB"></InnerComponent>
</template>
</WrapperComponent>
But I cannot do this for reasons!
I need to be able to create multiple instances of the slot content at runtime. I have created a code sandbox with what I got so far.
https://codesandbox.io/s/stupefied-framework-f3z5g?file=/src/App.vue:697-774
<template>
<div id="app">
<WrapperComponent>
<InnerComponent propA="Static"></InnerComponent>
</WrapperComponent>
</div>
</template>
<script>
import Vue from "vue";
const WrapperComponent = Vue.extend({
name: "WrapperComponent",
data() {
return {
valueMap: ["Child 1", "Child 2"]
}
},
render(h) {
return h("div", {}, [
this.valueMap.map(val => {
console.log(val);
for (const slot of this.$slots.default) {
// this is a read only slot. I can not change this.
// However, I want multiple instances of the slot
// => Inject a scoped-slot
slot.componentOptions.propsData.propB = val;
}
return h("div", {}, [this.$slots.default]);
})
])
}
});
const InnerComponent = Vue.extend({
name: "InnerComponent",
props: {
// This is a static configuration value.
propA: String,
// This is a runtime value. The parent component manages this component
propB: String
},
template: "<div>A: {{propA}} B: {{propB}}</div>"
});
export default {
name: "App",
components: {
WrapperComponent,
InnerComponent
}
};
</script>
This works perfectly fine with static information only, but I also have some data that differs per slot instance.
Because VNodes are readonly I cannot modify this.$slot.default.componentOptions.propsData. If I ignore the warning the result will just be the content that was passed to last instance of the slot.
<WrapperComponent>
<WrapperSubComponent v-for="(propB) in data" key="probB" :prop-b="prop-b">
<InnerComponent propA="Static"></InnerComponent>
</WrapperSubComponent>
</WrapperComponent>
Works after wrapping the component in another component and only executing the logic once.

vuejs treeselect - delay loading does not work via vuex action

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.

Where do I store shareable data in vuejs?

I am building an app with various pages, and when users goes to /orgs I have a template that I require
// routes.js
...
import Orgs from './components/Orgs.vue';
...
{
path: '/orgs',
component: Orgs,
meta: { requiresAuth: true }
},
from here I have a simple template in Orgs.vue that looks like:
<template lang="html">
<div> {{orgs}} </div>
</template>
<script>
export default {
data(){
return {
orgs: [];
}
},
created() {
//use axios to fetch orgs
this.orgs = response.data.orgs;
}
}
</script>
The problem is that if I want to show list of organizations in other pages, I am bound to duplicate the same code for other pages as well, but I am trying to find a solution that would call return organizations so I can use that in multiple page?
What is the solution for this?
To make data available across the application use Vuex.
It is state management library which stores all the application data in a single source tree.
If you don't want to you vuex for the above issue, you can try mixins.
Mixins are best way to share functionality.
For the above case you can try a mixin like this.
organisation.mixin.js
const OrganisationMixin = Vue.mixin({
data: function () {
return { orgs: [] }
},
methods: {
fetchOrgs: function() {
// api to fetch orgs
this.orgs = result_from_api
}
}
mounted: function() {
this.fetchOrgs()
}
});
export default OrganisationMixin
Now let's use the mixin we just created.
In whatever_name_component.vue:
<template lang="html">
<div> {{orgs}} </div>
</template>
<script>
import OrganisationMixin from 'path_to_organisation.mixin.js'
export default {
mixins: [OrganisationMixin]
data(){
return { orgs: [] }
},
mounted() {
console.log(this.orgs) //provided by mixin` and value is equal to api response from mixin.
}
}
</script>