I made a small library to track the loading of all my api calls.
It basically is an array which contains objects describing one loading process. I import this library into a Single File Component to make calls to it. I then pass the array from the library to a child component and want to render all my objects in the array (the loading statuses). I see the data updating in Vue Devtools on the child component, however it is not updated in the page.
What i tried:
passing the vue instance from the parent component and use $set to update the array
using array.splice to update the array
creating a new object with Object.assign, update the new object and then update the array (splice/$set)
passing a key and updating this key when updating thew object
calling methods in the library in the console
sample updating function:
function _finish(name, descFinished) {
const i = _loaders.findIndex(l => l.name == name);
if(i < 0) throw new NameNotFoundException(name);
let loader = Object.assign({}, _loaders[i]);
if(descFinished != undefined) loader.descFinished = descFinished;
loader.loading = false;
loader.error = false;
loader.endTime = new Date();
vm.$set(_loaders, i, loader);
return _loaders[i];
}
I pass to the child component like this
<loading-status v-if="loadstatus" :statuses="loadstatus.getAllLoaders()"></loading-status>
child component looks this this
<template>
<div v-if="statuses">
<div v-for="status in statuses" :key="status.name">
<div>
{{ status.loading == true ? 'true' : 'false' }}
</div>
</div>
</div>
</template>
<script>
export default {
name: 'loading-status',
props: {
statuses: {
type: Array,
required: true
}
}
}
image of the array not being reactive on the page
some more information that may be useful:
the library structure:
function loadmanager(vueinstance) {
//library internals here
// for example my array i want to render:
let _loaders = [];
var publicInterface() {
//public accesable functions here
//i call this to pass the array to my child component
getAllLoaders() {
return _loaders;
}
}
return publicInterface;
}
exports.makeLoadManager = loadmanager;
i import the library in the vue SFC and call it in the mounted hook
import loadhelper from "../../helpers/Loadstatus.js";
...
data() {
return {
loadstatus: null
}
}
...
mounted() {
this.loadstatus = loadhelper.makeLoadManager(this);
}
My question boils down to: How do I reactively render or correctly update an array from a js library in vue.
I would be happy to provide more information if that helps answering my question.
If your library is creating (and managing) an array of "loaders", simplest thing you can do is define empty reactive array in your Vue component's data and in mounted hook assign a reference to your's library array into it. Like this:
import LoadingStatus from "./components/LoadingStatus";
import loadhelper from "./helpers/LoadManager.js";
export default {
name: "App",
components: {
LoadingStatus
},
data() {
return {
statuses: []
};
}
mounted() {
this.statuses = loadhelper.getAllLoaders();
}
};
Vue takes the array provided by your library and makes it reactive. You can use template like this:
<LoadingStatus :statuses="statuses"/>
See the demo
As long as both Vue and your "load manager" are working with the same array (reference), its all good. I'v added setTimeout callback into LoadManager to demonstrate that changes "from the outside of Vue" to the array will make Vue component re-render....
Related
I'm trying to pass an array from async method in the controller to a vue component using props through the blade, but I'm getting undefined when I try to console.log it in the vue component, this is my code:
Controller:
public function index(){
$pool = Pool::create();
$pool[] = async(function () {
return Rfm::all();
})->then(function ($output) {
$this->results=$output;
});
await($pool);
return view ("/dashboard", ["data" => $this->results]);
Blade:
<div id="total_customers">
<total-customers :data="{{$data}}"></total-customers>
</div>
Vue component:
export default {
props: ['data'],
data() {
return {
};
},
mounted() {
console.log(this.data);
}
};
I've searched and read somewhere that using async in the controller would be the problem as the object is still empty when it render to vue component, but I'm not sure about that, am i doing it wrong?
Binding props that way might work with strings and integers but doesn't work with array/object data: https://laravel.com/docs/9.x/blade#blade-and-javascript-frameworks
The following should work:
<total-customers :data="{{ Js::from($data) }}"></total-customers>
Addition:
Might want to look into https://inertiajs.com/ if you bind data to vue components in blade a lot, its a nicer way to render vue components with props
I have a vue store which has the following
store.js
import Vue from 'vue'
import Vuex from 'vuex'
const state = {
supplementStore: {}
}
const actions = {
getDataFromApi ({dispatch, commit}) {
APIrunning.then(response => {
commit('SET_SUPPLEMENT', response)
})
}
}
const mutations = {
SET_SUPPLEMENT (state, data) {
state.supplementStore= data
}
}
const foodstore = {
namespaced: true,
state,
actions,
mutations
}
Vue.use(Vuex)
export default new Vuex.Store({
modules: {
foodstore
}
})
My vue component looks like this
Supp.vue
<template>
<input type="checkbox" v-model="supps.logged">
</template>
<script>
import {mapState, mapActions} from 'vuex'
import store from './store'
export default {
data () {
return {
supps: []
}
},
mounted () {
this.supps = this.supplementStore
},
computed: {
...mapState('foodstore', ['supplementStore'])
}
}
</script>
As you can see I have a component level state called supps which is assigned the value of supplementStore (which is a vuex state) as soon as it is mounted.
mounted () {
this.supps = this.supplementStore
},
supplementStore gets its value from the the API and it is a JSON object which looks like this
supplementStore = {
logged: true
}
Therefore, when my Supp.vue component is mounted my local state supps will become
supps = {
logged: true
}
supps is binded to an input field of type checkbox (Supp.vue) using the v-model directive.
What I want to achieve:
When I toggle the checkbox, supps.logged should toggle between true and false but, supplementStore.logged should remain unchanged (since I have not binded it to my input field).
What I observe in my Vue Devtools:
When I toggle the checkbox, both supps.logged AND supplementStore.logged are toggling in sync i.e both of them are toggling in sync between true and false, whereas I want only supps.logged to get toggled.
Can anyone help me?
In Javascript, object is passed by reference. (This is a reasonably good explanation => https://medium.com/nodesimplified/javascript-pass-by-value-and-pass-by-reference-in-javascript-fcf10305aa9c)
To avoid this problem, you can clone the object when assigning to supps.
mounted () {
this.supps = { ...this.supplementStore } // cloning the object using Javascript Spread syntax
},
Have you tried Object.assign instead? In JS objects are passed by reference. Assigning one to a variable will cause the original one to change if the variable is changed inside.
To clone an object, you may try this:
// this.assignedObj = new object.
// this.obj = original object.
this.assignedObj = Object.assign({}, this.obj);
JSFiddle: https://jsfiddle.net/mr7x4yn0/
Edit: As you can see from the demo, Vue.set or this.$set will not work for you (probably).
The data I was receiving from the API into supplementStore was in the form of an array of objects:
supplementStore = [
{
"logged": true
},
{
"logged": false
}
]
And as Jacob Goh and Yousof K. mentioned in their respective answers that objects and arrays get passed by reference in javascript, I decided to use the following code to assign the value of supplementStore to supps inside my mounted() hook:
mounted () {
let arr = []
for (let i = 0; i < this.supplementStore.length; i++) {
let obj = Object.assign({}, this.supplementStore[i])
arr.push(obj)
}
this.supps = arr
}
Now when I toggle my checkbox, supplementStore.logged remains unchanged while supps.logged toggles between true and false just the way I wanted.
I'm using onsen-ui to style a meteor app with React for the frontend. As I understand it, onsen-ui manages navigation by pushing pages to a stack, where each page has a unique identifier.
Here is how my pages are loaded in App.js
loadPage = (page) => {
const currentPage = this.navigator.pages.slice(-1)[0]
if(currentPage.key != page.name){
this.navigator.pushPage(
{
component: page,
props: { key: page.name }
},
{
animation: 'fade',
animationOptions: {duration: 0.3, timing: 'ease-in'}
},
);
}
}
So far everything works fine. But I have now included redux in my project, so I have some components which are connected to the store by react-redux's connect() function.
The problem is that for every component that connect wraps, the name property becomes Connect, so onsen-ui is having to deal with multiple pages with the same name in its stack.
As an example, let's say I have a component defined below
const ComponentName = props => {
return (
<p>Test component</p>
)
}
export default connect()(ComponentName)
ordinarily, ComponentName.name returns ComponentName but once its wrapped with connect, ComponentName.name returns Connect
Is it at all possible to modify the name value for the wrapped component?
Every suggestion is welcome.
Edit: Following Vlatko's lead, this is how I eventually solved the problem.
export const getPageKey = (page) => {
// returns a page key
let key;
if (page.name === 'Connect') {
key = page.displayName
// key = page.WrappedComponent.name
// react-redux connect returns a name Connect
// .displayName returns a name of form Connect(ComponentName)
return key.replace('(', '').replace(')', '')
}
else {
key = page.name
return key
}
}
So for every component I just get the key with getPageKey(ComponentName)
Edit 2. This approach doesn't work in production.
In production mode, I get single letters for page.displayName and those letters are hardly unique, which means I'm back where I started.
I need to find another approach. I'll update whatever I find.
This is happening because you are exporting your component through a higher order function (Connect), which is basically a closure in JavaScript.
The name of the HOC that you are using is Connect, and this is returned.
However, it is possible to get the name of the component passed into the connect HOC.
ComponentName.name // Connect
ComponentName.displayName // Connect(ComponentName)
ComponentName.WrappedComponent.name // ComponentName
I’m not familiar with onsen-ui, and it looks like Vlatko has you covered, but couldn’t you also give the new connected component a name? For example:
const ComponentName = props => {
return (
<p>Test component</p>
)
}
const ConnectedComponentName = connect()(ComponentName)
export default ConnectedComponentName;
Then hopefully you would be able to access the unique ConnectedComponentName
Usually we define in a Nuxt.js component something like this:
<script>
export default {
components: {
// components
}
data() {
return {
// key/value data
}
},
methods: {
// method definitions
}
}
</script>
Is there a way to read the components object as we read data() and methods ?
This is because I have several components and I want to loop on them to refactor parts of my code.
You can get Component data by using $options.
Try this.
created() {
console.log(this.$options.components)
}
it returns an object, keys are the component names, values are the contructors.
codepen - https://codesandbox.io/s/yk9km5m0wv
I would like to separate the business logic and the template of a component.
In React I would use the Container/Presentation pattern.
const Container = (props) => <Presentational ...props/>
But what is the equivalent with vue.js?
Say I have this all in one component (did not test this one, it just for example) :
<template>
<div id="app">
<div v-for="user in users">
{{user.name}}
</div>
</div>
</template>
<script>
Vue.component({
el: '#app',
props: {
filter: "foo"
},
data: {
users: [],
},
ready: function () {
this.$http.get('/path/to/end-point?filter='+this.props.filter)
.then(function (response) {
this.users = response.data
})
}
})
</script>
How I could extract a container with just the fetch logic?
I really don't see a need for a container component. Abstract your fetch code out into a vuex action and bind your store state within the component using computed properties.
You can extend any Vue component, which will allow you to override any methods from the parent. So, you can create a base component and then extend that:
const Base = Vue.component('base-comp', {
template: "#base",
data() {
return {
name: 'foo'
}
}
});
const Child = Base.extend({
data() {
return {
name: 'bar'
}
}
});
Vue.component('child-comp', Child)
You can check out the JSFiddle here: https://jsfiddle.net/tdgxdhz9/
If you are using single file components, then it's simply a case of importing the base component and extending it, which keeps the original template in tact:
import Base from './BaseComponent.vue'
Base.extend({
// Javascript code here
})
You can use vuex-connect to create container components like in React. Here an example of project : https://github.com/pocka/vue-container-component-example
What you are looking for is Vue Mixins.
You can write a mixins file which contains your business logic and import it into your Vue components.
Link:- https://v2.vuejs.org/v2/guide/mixins.html