Only about 6 weeks into using vuejs and haven't looked at components at all, so sorry if this is a dumb question.
I am attempting to make the b-spinner component for tabs from bootstrap-vue update dynamically inside a v-for loop, with the parameter from the v-for being past to a computed property.
It works fine when not in a v-for.
<b-tab>
<template v-slot:title>
<b-spinner type="border" small v-show="!updateDone"></b-spinner> <strong>Tab Title</strong>
</template>
...
</b-tab>
But when I try to pass the player property from the v-for, it breaks. When I try to pass player to the title slot, the entire parent component gets passed.
<b-tab :key="player['player-id']" v-for="player in updatePool">
<template v-slot:title="{player}">
<b-spinner type="border" small v-show="!updateDone(player)"></b-spinner> <strong>{{player['player-name']}}</strong>
</template>
...
</b-tab>
updateDone is a computed property as follows:
updateDone: function (player) {
return _.every(Object.values(this.someObject[player['player-name']]), Boolean)
},
But updateDone is never returned b/c player is an entire Vue object (presumably the parent component)
Hoping for some insight on this.
SOLUTION
Thanks to Antonio
<b-tab v-for="player in updatePickPool" :key="player['player-id']">
<template v-slot:title>
<b-spinner type="border" small v-show="!playerPathComplete(player)"></b-spinner><strong> {{player['player-name']}}</strong>
</template>
...
</b-tab>
According to your comments, I realized that updateDone is a computed property.
Do not use computed property in this case.
You have to use method instead of computed property.
Why?
computed properties are cached and change only when their dependencies change.
In your case, there's no change in player while looping to render tabs. That means it will show the same results of updateDone property. :(
But a method will evaluate every time it's called. :)
Related
Vue 3 removed the $children property and butchered $slots. However I can't find another solution for this scenario I'm having:
I have a component Checkbox. This component renders a checkbox. It also has a default slot. Depending on whether there was anything in that default slot I need to show/hide an element and apply some classes to the Checkbox' root element for styling and animation purposes of elements around it. I don't need access to the actual children, only the information whether there are any.
In Vue 2 I could write something like this:
if (this.$slots.default) {
// do something
}
The same thing in Vue 3 always is true, because it's a function now. However, calling that function returns nonsense:
console.log(this.$slots.default())
// Returns:
// [ Vnode ]
It doesn't matter whether I put something into the default slot or not, the end result is always the same, only with some elements buried in the Vnode tree having changed.
How am I supposed to find out whether there are any children in Vue 3?
For reference, the component template I used before:
<transition name="slide-down">
<div class="children" v-if="$slots.default">
<slot></slot>
</div>
</transition>
The above and below does not work in Vue 3 (the div always is being rendered):
<transition name="slide-down">
<div class="children" v-if="$slots.default()">
<slot></slot>
</div>
</transition>
Imagine that you have a user list component UserList that shows users in scrollable view. Each users details are represented with UserDetails component wrapped with Card component.
UserList
<template>
<Card>
<UserDetails />
</Card>
<Card>
<UserDetails />
</Card>
...
</template>
Now imagine that you're using this UserList component and want to re-implement the wrapper for UserDetails without modifying it's contents.
Adding a slot would work for replacing the wrapper but everything inside the wrapper would need to be re-implemented as well.
It would be nice if we could write Vue like this:
UserList
<template>
<slot name="wrapper">
<Card>
<template #content>
<UserDetails />
</template>
</Card>
</slot>
...
</template>
Consuming component:
<UserList>
<template #wrapper>
...
<NewImplementation>
<slot :name="content" />
</NewImplementation>
</template>
</UserList>
It would work by using slots in "reverse" way of how we're used to.
This isn't valid syntax but I bet someone else has thought about the same problem. The need to replace some content with slot but not all of it.
Wrapper component could be given as property but I think it's not the correct solution because we have slots to avoid doing just that.
Are there any good solutions?
If I understood right, what you're looking for is Teleports (previously called Portals) which is only available in version 3.0
What you want is problematic. Vue has no direct support for something like that.
Let me establish some naming conventions before I try to explain why:
Child - UserList component
Parent - component consuming Child
Main problem in this scenario is that slot content is completely generated in the consuming component and just passed to a Child component using
$slots - this is for normal slots. $slots.wrapper is just a static array of VNodes
$scopedSlots - for scoped slots. $scopedSlots.wrapper is a function. When Child renders it calls that function and renders returned an array of VNodes
(Note: In Vue 3 it is just $slots. All slots are functions)
What that means is that Child component has no way to somehow change the content of the slot (e.g. include something in the middle). It can pass some data into it (slot scope) but that's all...
Possible workarounds:
1. Just pass a wrapper component as a prop
Wrapper component could be given as property but I think it's not the correct solution because we have slots to avoid doing just that.
Well not really. Slots have a different purpose. If you want to change the wrapper and let the rest of the Child intact, this is pretty good and simple solution
2. Give a Parent the component it should render inside the wrapper using slot scope
This is something very well demonstrated in route-view component of Vue Router 4.x - see the docs
<UserList v-slot="{ UserComponent, UserData }">
<NewWrapper>
<component :is="UserComponent" v-bind:data="UserData" />
</NewWrapper>
</UserList>
So you have <component :is="UserComponent" /> instead of <UserDetails />. This is useful as the UserList component can now control what component is rendered inside the wrapper (and even change it dynamically)
I have a b-table which looks something like this:
<b-table :fields="fields" :items="tableRows">
<template #head(expand_all+)="data">
//would like to make this a button to expand all row-details
<div class="text-right">{{data.label}}</div>
</template>
...
<template #cell(expand_all+)="data">
<div class="expand-arrow-container":class="{ 'expanded': data.detailsShowing }" #click="data.toggleDetails">
<font-awesome-icon icon="chevron-right" />
</div>
</template>
...
<template #row-details="row">
//display data here
</template>
</b-table>
Now, all of this works fine and and my row details expand/contract as I would expect (see b-table row-details for more information).
The problem arises in that I want to be able to click one button and expand/contract all the row-details. My plan is to change template #head(expand_all+)="data" into a button which can be clicked to accomplish this. However, I do not know how to do this. I have searched and read the b-table documentation and have not found any way of accomplishing what I want. I've even taken a look at the table object to see if it had any references to its rows and I didn't see anything.
Do any of you know a way to accomplish this? Getting a reference to the rows would make this a simple problem, but as I mentioned I didn't see anything. I am also not looking to swap out the b-table with a bunch of b-collapse elements since I have put a lot of work into the table.
As described on the docs, you can set a _showDetails property on your items passed to the table. Which if set to true will expand that row's details. This means you could loop through all your items and set that property to true to expand all rows. And do the same with false to collapse them.
If the record has its _showDetails property set to true, and a row-details scoped slot exists, a new row will be shown just below the item, with the rendered contents of the row-details scoped slot.
Here's two example methods you could use.
We're using $set because we're adding a new property to an already existing object. You can read more about why here
expandAll() {
for(const item of this.tableRows) {
this.$set(item, '_showDetails', true)
}
},
collapseAll() {
for(const item of this.tableRows) {
this.$set(item, '_showDetails', false)
}
}
How can I update data object of parent whenever changes happen inside v-for. I have a child component that I use inside parent component.
ParentComponent.vue
<template>
....
....
<child-component
v-for="i in count"
ref="childComponent"
:key="i"
currentPage="i" // currentPage doesn't update.
:page="i"
/>
<q-header>
{{currentPage}} // always displays default value:1
</q-header>
</template>
<script>
data () {
return {
pageCount: 10,
currentPage: 1,
}
},
How can I update currentPage of data object whenever i changes inside v-for. I have tried using watch without much luck. I don't have access to child component and can't modify it.
Much appreciated!
There is some slight confusion with how v-for is working on the child-component here. Writing currentPage="i" as a property (which should actually be v-bind:currentPage in order for the i to be interpreted as JS) will simply declare the attribute on each child-component
How can I update currentPage of data object whenever i changes inside v-for
i doesn't "change" in the traditional context of running a for loop inside of a normal JavaScript application. In Vue, your rendering logic and application logic are separate, and rightly so, because running logic as part of the rendering doesn't really make sense.
For example, let's look at how your app will render the child-component:
<!-- Vue output -->
<child-component ... currentPage="1" />
<child-component ... currentPage="2" />
<child-component ... currentPage="3" />
So let's look at separating the rendering logic from the application logic.
I realise you don't have access to child-component, but based on the context I will assume it is some kind of tabbing functionality (based on you trying to set a value for the "current page" - feel free to be more specific and I can update my answer).
We need to bridge that gap between the rendering logic and the application logic and we can do that by using events:
<child-component
v-for="i in count"
:ref="`childComponent-${i}`" // ref should be unique so add the i as part of it
:key="i"
:page="i"
v-on:click="currentPage = i" // when the user clicks this child component, the current page will be updated
/>
You may have to utilise a different event other than click but I hope this gets you closer to what you are trying to achieve. For the value of currentPage to update there has to be some kind of user input, so just find out which event makes the most sense. Maybe the child-component library you are using has custom events that are more appropriate.
you should look into Custom Events.
https://v2.vuejs.org/v2/guide/components-custom-events.html
Idea is, that whenever there is some update of your desire in child component, you can execute this.$emit(“change”), which will throw an event.
On parent side you can catch this event by #change=“myMethod” as one of the attributes.
methods: {
myMethod() {
console.log("Testing")
}
}
<child-component
v-for="i in count"
ref="childComponent"
:key="i"
currentPage="i"
:page="i"
#change=“myMethod”
/>
Let me know if that helped.
In Laravel Nova, action modals are rendered in Vue by retrieving a list of fields to display through a dynamic component. I have replaced the action modal with own custom component, but am struggling to achieve the effect I want without also extending the entire set of components for rendering form fields.
I have my CustomResourceIndex.vue, containing a conditionally loaded (via v-if) ActionModal.vue, in which the form fields are rendered like so:
<div class="action" v-for="field in action.fields" :key="field.attribute">
<component
:is="'form-' + field.component"
:resource-name="resourceName"
:field="field"
/>
</div>
where the actual form field component is chosen based on the field.component value.
Those form fields (which I ideally do not want to have to extend and edit) are rendered like so:
<template>
<default-field :field="field" :errors="errors">
<template slot="field">
<input
class="w-full form-control form-input form-input-bordered"
:id="field.attribute"
:dusk="field.attribute"
v-model="value"
v-bind="extraAttributes"
:disabled="isReadonly"
/>
</template>
</default-field>
</template>
I would like to watch the value of specific fields and run methods when they change. Unfortunately due to a lack of ref attribute on the input elements or access to the value that the form element is bound to, I'm not sure how I can accomplish that from within ActionModal.vue.
I am hoping that because I have access to the ids still, there is some potential way for me to emulate this behavior.
Many resources I've found on my own have told me that anything with an ID is accessible via this.$refs but that does not seem to be true. I can only see elements that have an explicitly declared ref attribute in this.$refs, so I am not sure if I've misunderstood something there.
I would recommend looking into VueJS watch property.
You can listen to function calls, value changes etc.
watch: {
'field.component': function(newVal, oldVal) {
console.log('value changed from ' + oldVal + ' to ' + newVal);
},
},
Are those components triggering events? Try looking into the events tab of the Vue DevTools to see if some events are triggered from the default-field component when you update the value.
My guess is that you could write something like:
<div class="action" v-for="field in action.fields" :key="field.attribute">
<component
:is="'form-' + field.component"
:resource-name="resourceName"
:field="field"
#input="doSomething($event)"
/>
</div>
The $event value being the new value of the field.
Hit me on the comments if you have more info on the behavior of the default form fields (Are their complete code accessible somewhere?).