Buefy table not passing props in render function - vue.js

I'm trying to use Buefy table component in a render function but the props are not being passed.
import { createElement as h } from '#vue/composition-api';
import { BTableColumn, BTable } from 'buefy/dist/esm/table';
h(BTable, {
class: 'table--resource-table table is-fullwidth is-hoverable',
ref: 'table',
props: {
data: filteredList.value,
backendSorting: true,
backendPagination: true,
backendFiltering: true,
sortIcon: 'angle-up',
iconPack: 'far',
defaultSortDirection: 'asc',
defaultSort: [props.sort, props.dir],
checkable: props.checkable,
isRowCheckable: props.isRowCheckable,
checkedRows: props.selectedRecords,
},
scopedSlots: {
default: (row) => {
console.log(row);
}
}
})
my console.log always return an empty object
Is it that for some reason I'm translating their example wrong to a render function?
<b-table :data="myData">
<b-table-column field="name" label="Name" v-slot="props">
{{ props.row.name }}
</b-table-column>
<b-table-column field="age" label="Age">
<template v-slot:default="props">
{{ props.row.age }}
</template>
</b-table-column>
</b-table>

After taking a look at the type definitions for createElement function I discovered you can pass a function to it:
export type ScopedSlot = (props: any) => ScopedSlotReturnValue;
export type VNodeChildren = VNodeChildrenArrayContents | [ScopedSlot] | string | boolean | null | undefined;
export interface CreateElement {
(tag?: string | Component<any, any, any, any> | AsyncComponent<any, any, any, any> | (() => Component), children?: VNodeChildren): VNode;
(tag?: string | Component<any, any, any, any> | AsyncComponent<any, any, any, any> | (() => Component), data?: VNodeData, children?: VNodeChildren): VNode;
}
This can be solved with:
h(BTable, {...}, [
(rowProps) => h('div', {}, context.slots.default(rowProps))
])

Related

Vue3 Element Plus not rendaring the props inside the imported element when testing with the vitest

I have a very basic component called
ConfirmModal.vue
<template>
<el-dialog v-model="dialogVisible" :title="title" width="30%">
<span>{{ content }}</span>
<template #footer>
<span class="dialog-footer">
<el-button #click="dialogVisible = false">Cancel</el-button>
<el-button type="primary" #click="emit('onConfirm')">
Confirm
</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { computed } from "vue";
import { ElButton, ElDialog } from "element-plus";
const props = defineProps({
show: {
type: Boolean,
default: false,
},
content: {
type: String,
required: true,
},
title: {
type: String,
default: "Are you sure?",
},
});
const dialogVisible = computed({
get: () => props.show,
set: (v: boolean) => emit("update:show", v),
});
const emit = defineEmits<{
(emitName: "onConfirm"): void;
(emitName: "update:show", v: boolean): void;
}>();
</script>
and a test called
ConfirmModal.spec.ts
import { mount } from "#vue/test-utils";
import { describe, expect, it } from "vitest";
import ConfirmModal from "../ConfirmModal.vue";
describe("ConfirmModal.vue", () => {
it("render the modal", () => {
const wrapper = mount(ConfirmModal, {
props: {
show: true,
content: "something",
},
});
expect(wrapper.find("span").text()).toContain("something");
});
});
The test is failing like this
FAIL components/__tests__/ConfirmModal.spec.ts > ConfirmModal.vue > render the modal
Error: Cannot call text on an empty DOMWrapper.
❯ Object.get ../node_modules/#vue/test-utils/dist/vue-test-utils.esm-bundler.mjs:1513:27
❯ components/__tests__/ConfirmModal.spec.ts:14:31
12| });
13|
14| expect(wrapper.find("span").text()).toContain("something");
| ^
15| });
16| });
I'm not sure what is missing here.

Reset select values in v-autocomplete, in order to add multiple items

I'm creating an application that has a selection box to choose between some template data. However, the user should be able to select the same template option several times and, each time he selects the template, a new informational box appears in the screen.
My problem is that the v-autocomplete component doesn't enable this kind of behavior: we can select one option (or multiple options), but not the same option twice.
I thought about making something like this: every time the user selects the option A, the infobox would appear below and the component would reset to a empty option. Then, the user could choose option A again and, when he chooses it, another info box would appear, how many times the user needs it.
How could I do something like this using vue? I didn't found any component that would do something like this on default, so I think I'll have to tweak the component behavior, but I don't know exactly where to start.
My template:
<template>
<div class="select-wrapper" id="selectBox">
<v-autocomplete
class="select-input"
:items="items"
:name="label"
placeholder="select item"
solo
:value="value"
#change="$event => onChange($event, items)"
item-text="name"
item-value="value"
:required="required"
:rules="[
value =>
!required ||
!!value ||
"required"
]"
></v-autocomplete>
</div>
</template>
And my Vue code:
<script lang="ts">
import { defineComponent } from "#vue/composition-api";
import Vue from "vue";
interface SelectItem {
name: string,
value: string
}
interface SelectBoxProps {
items: SelectItem[];
value: string;
onSelect: ({ target }: { target?: SelectItem }) => void;
hasResetSelection: boolean;
}
export default defineComponent({
name: "SelectBox",
props: {
label: String,
items: Array,
value: [String, Number],
onSelect: Function,
disabled: Boolean,
required: {
type: Boolean,
default: false
},
hasError: Boolean,
errorMessage: String,
hasResetSelection: {
type: Boolean,
default: false
}
},
directives: {
ClickOutside
},
setup({ onSelect, hasResetSelection }: SelectBoxProps) {
const onChange = (selectedValue: string, itemsArr: SelectItem[]) => {
const targetItem = itemsArr.find(i => i.value === selectedValue);
if (hasResetSelection) {
Vue.nextTick(() => {
console.log("onselect should reset value");
return onSelect({ target: { name: "", value: "" } });
});
}
return onSelect({ target: targetItem });
};
return {
onChange
};
}
});
</script>
This was my last attempt with Vue.nextTick, I already tried to tweak the component with ref() and it didn't work as well. Do you have any suggestions?
You can use another variable just to hold the input for the autocomplete component Like this:
var app = new Vue({
el: '#app',
vuetify: new Vuetify(),
data: {
items: [{ name : 'hello', value : 1 }, { name : 'world', value : 2 }],
value : null,
values : []
},
methods: {
onChange() {
this.values.push(this.value)
this.$nextTick(() => {
this.value = null
})
},
}
})
<link href="https://cdn.jsdelivr.net/npm/vuetify#2.x/dist/vuetify.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/vue#2.6.0"></script>
<script src="https://cdn.jsdelivr.net/npm/vuetify#2.x/dist/vuetify.js"></script>
<div id="app">
<v-app>
<v-container>
Values : {{values}}
<v-autocomplete
:items="items"
placeholder="select item"
solo
v-model="value"
item-text="name"
item-value="value"
#change="onChange"
/>
</v-container>
</v-app>
</div>

Quasar custom input component field validation

I am trying to create Quasar custom select component with autocomplete. Everything works fine except the validation error, the validation error is showing only when I click the input box and leave without adding any value. But, the form is submitting even there are any errors.
Component code
<q-select
ref="members"
v-model="sModel"
use-input
:options="filteredOptions"
:multiple="multiple"
:use-chips="useChips"
:label="label"
:option-label="optionLabel"
:option-value="optionValue"
#filter="filterFn"
#input="handleInput"
emit-value
map-options
hint
dense
outlined
lazy-rules
:rules="rules"
>
<template v-slot:prepend>
<q-icon :name="icon" />
</template>
</q-select>
</template>
<script>
export default {
props: {
value: Array,
rules: Array,
icon: String,
label: String,
optionValue: String,
optionLabel: String,
options: Array,
multiple: Boolean,
useChips: Boolean
},
data () {
return {
filteredOptions: this.options,
sModel: this.value,
validationErrors:{
}
}
},
methods: {
filterFn (val, update) {
if (val === '') {
update(() => {
this.filteredOptions = this.options
// with Quasar v1.7.4+
// here you have access to "ref" which
// is the Vue reference of the QSelect
})
return
}
update(() => {
const needle = val.toLowerCase()
const optionLabel = this.optionLabel
this.filteredOptions = this.options.filter(function(v){
// optionLabel
return v[optionLabel].toLowerCase().indexOf(needle) > -1
})
})
},
handleInput (e) {
this.$emit('input', this.sModel)
}
},
}
</script>
In the parent component, this is how I am implementing it,
<AdvancedSelect
ref="members"
v-model="members"
:options="extAuditEmployees"
icon="people_outline"
multiple
use-chips
label="Team Members *"
option-label="formatted_name"
option-value="id"
:rules="[ val => val && val.length && !validationErrors.members > 0 || validationErrors.members ? validationErrors.members : 'Please enter Team members' ]">
</AdvancedSelect>
Try adding this method on select component methods:
validate(...args) {
return this.$refs.members.validate(...args);
}
It worked for me, apparently it sends the validation of the input to the parent
Source consulted: https://github.com/quasarframework/quasar/issues/7305
add ref to the form and try to validate the form.
you can give give props "greedy" to the form.

Watch props from children component vue.js

I have two components "number-iput" and "basket input". How i can watch props from number-input (value) in basket-input?
Number Input component:
<template lang="pug">
.field
.number-input
button.number-input__btn(#click.prevent="minus")
i.i.i-minus
input(type="number" v-model="value" #input="valuecheck")
button.number-input__btn(#click.prevent="plus")
i.i.i-plus
</template>
<script>
export default {
name: "number-input",
props: {
value: {
type: Number,
default: 1
},
min: {
type: Number,
default: 1
},
max: {
type: Number,
default: 999999999
},
current: {
type: Number,
default: 1
}
},
methods: {
plus() {
if(this.value < this.max) this.value++;
},
minus() {
if(this.value > this.min) this.value--;
},
valuecheck() {
if(this.value > this.max) this.value = this.max
}
},
watch: {
value: function() {
if(parseInt(this.value) > parseInt(this.max)) this.value = this.max
}
}
}
</script>
Basket-input
<template lang="pug">
.basket-item
a.basket-item__image(href="")
img(:src="image", :alt="title")
.basket-item__info
span.basket-item__caption {{ code }}
a.basket-item__title(href="") {{ title }}
span.basket-item__instock(v-if="instock && instock > 0") В наличии ({{ instock }} шт)
span.basket-item__instock(v-else) Нет в наличии
.basket-item__numbers
number-input(min="1" max="99" :value="numberofitems")
.basket-item__lastcol
.column-price(v-if="price")
b {{ numberofitems * price }} ₽
span(v-if="numberofitems > 1") {{ numberofitems }} × {{ price }} ₽
button.basket-item__remove Удалить товар
</template>
<script>
export default {
name: 'basketitem',
props: {
image: {
type: String,
required: true
},
title: {
type: String,
required: true
},
code: {
type: String,
required: true
},
instock: {
type: Number
},
price: {
type: Number
},
numberofitems: {
type: Number,
default: 1
}
},
}
</script>
In basket input i need to watch number-input(value) and write it in numberofitems prop..
Im trying all, but my knowledge of vue is too low for that (
Props are mechanism to pass data only in one way - that means you can pass a value from parent to child but child is not allowed to change the value. If you do, Vue will warn you.
Way around it to use events. The principle is wildly know as "props-down, events-up".
You pass value to Child via prop
When Child want to change the value, instead of changing it directly, it will emit the event with new value
Parent component needs to handle that event and change its internal value (change will propagate into child item via prop)
You can read about various ways to do it for example here
Computed properties can help with that:
<template>
<input type="number" v-model="internalValue" />
</template>
<script>
export default {
props: {
value: {
type: Number,
default: 1
}
},
computed: {
internalValue: {
get: function() {
return this.value
},
set: function(newValue) {
this.$emit('valueChanged', newValue)
}
}
}
}
</script>
And in your parent component:
<template>
<mycomponent :value="value" #valueChanged="value = $event"/>
<mycomponent :value="value" #valueChanged="onValueChanged"/>
</template>
<script>
export default {
data: {
value: 0
},
methods: {
onValueChanged(newValue) {
this.value = newValue;
}
}
}
</script>
Notes
with larger and more complicated applications it can be better to use some solutions with shared global state like Vuex
Your Basket-input component has same problem because its receives numberofitems via prop

Two way data binding with :model.sync when prop has get() and set()

I have a computed property that I use as v-model on an input. I've written it this way to get reactivity -- this calls my setText Vuex action which I then can get with my getter text. It looks like this:
text: {
get() {
return this.text;
},
set(value) {
this.setText(value);
},
},
and I use it in my input like this:
<input class="input" type="text" v-model="text" />
This works well. Now, I've put the input in question into a separate component which I use. This means I have to pass the text v-model as props, which I do with :model.sync, like so:
<myInput :model.sync="text"/>
and in the myInput component I use the props like so:
<input class="input" id="search-order" type="text" :value="model" #input="$emit('update:model', $event)">
But this doesn't seem to work at all, whenever I type into the input, the input says: [object InputEvent] and if I try to see and the value of model it's {isTrusted: true}. I'm assuming it's because of the getters and setters I have on my computed property. How do I pass these down to the child component?
Instead of using the .sync modifier you can support the v-model directive in your custom component. v-model is syntax sugar for a value prop and an input event.
To support v-model just make sure your custom component has a value prop and emits an input event with the new value: this.$emit('input', event.target.value).
Here is an example of a <BaseInput> component I use, it's written in TypeScript:
<template>
<input
:type="type"
:value="value"
class="input"
v-on="listeners"
>
</template>
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
name: 'BaseInput',
props: {
type: {
type: String,
default: 'text',
},
value: {
type: [String, Number],
default: '',
},
lazy: {
type: Boolean,
default: false,
},
number: {
type: Boolean,
default: false,
},
trim: {
type: Boolean,
default: false,
},
},
computed: {
modelEvent(): string {
return this.lazy ? 'change' : 'input'
},
parseModel(): (value: string) => string | number {
return (value: string) => {
if (this.type === 'number' || this.number) {
const res = Number.parseFloat(value)
// If the value cannot be parsed with parseFloat(),
// then the original value is returned.
return Number.isNaN(res) ? value : res
} else if (this.trim) {
return value.trim()
}
return value
}
},
listeners(): Record<string, Function | Function[]> {
return {
...this.$listeners,
[this.modelEvent]: (event: HTMLElementEvent<HTMLInputElement>) =>
this.$emit(this.modelEvent, this.parseModel(event.target.value)),
}
},
})
</script>
You can use it like so:
<BaseInput v-model="text" />