Dynamic binding of v-model to computed property - javascript

I have a component where i'd like to iterate over elements with a computed property.
Under normal circumstances you'd do something like this:
// Computed property
acquiredPrice: {
get () {
return value
},
set (value) {
// set the value with some vuex magic
},
},
And then reference it in the template like this:
<v-text-field
v-model="acquiredPrice"
>
</v-text-field>
And this works just as expected. However i would like to do the following
// computed property
steps () {
return [
{
allowed: true,
text: 'Some question?',
type: 'integer',
model: this.acquiredPrice, // reference to the computed property
},
]
},
<!-- inside template -->
<template v-for="step in steps">
<v-row
:key="step.text"
>
<v-col>
<h4>{{step.text}}</h4>
<!-- This does not work. Only in retrieving the value -->
<v-text-field
v-model="step.model"
>
</v-text-field>
</v-col>
</v-row>
</template>
So the core issue is that when i iterate over the steps and use the step.model to reference the computed property, i loose the setter. I.e when typing into the field the setter method is never hit.
Maybe there is some way to access computed properties by string names, so i can avoid the actual value in the dict?
I.e something like (this is just pseudo code for what i want) v-model=$computed['acquiredPrice']
A full PoC to illustrate the issue can be seen here:
<template>
<div class="">
<template v-for="step in steps">
<v-row
:key="step.text"
>
<v-col>
<h4>{{step.text}}</h4>
<!-- This does not work. Only in retrieving the value -->
<v-text-field
v-model="step.model"
>
</v-text-field>
</v-col>
</v-row>
</template>
<!-- This works just as expected -->
<v-text-field
v-model="acquiredPrice"
>
</v-text-field>
</div>
</template>
<script>
export default {
name: 'redacted',
props: {
},
data: () => ({
}),
computed: {
acquiredPrice: {
get () {
return value
},
set (value) {
// set the value with some vuex magic
// THIS IS NEVER HIT WHEN IT IS REFERENCED FROM step.model ON LINE 13
},
},
steps () {
return [
{
allowed: true,
text: 'Some question?',
type: 'integer',
model: this.acquiredPrice,
},
]
},
},
components: {
},
methods: {
},
mounted () {
},
}
</script>

Root issue is in this line:
model: this.acquiredPrice, // reference to the computed property
Because you are not assigning reference to acquiredPrice computed property into model, you are calling its getter and assigning value it returns...
There is nothing like $computed because its not needed - computed prop is just another member of your component so you can access it like this["acquiredPrice"] and since this is not available in templates, you can use a little trick using the method and returning this from it....
Whole solution:
new Vue({
data() {
return {
counter: 10
};
},
computed: {
acquiredPrice: {
get() {
return this.counter;
},
set(value) {
// just placeholder as we dont have Vuex here
this.counter = value;
console.log("Updating counter", value)
}
},
steps() {
return [{
allowed: true,
text: 'Some question?',
type: 'integer',
model: 'acquiredPrice'
}, ]
},
},
methods: {
self() {
return this
}
}
}).$mount("#app");
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
{{ counter }}
<div v-for="step in steps" :key="step.text">
<h4>{{step.text}}</h4>
<input v-model="self()[step.model]" />
</div>
</div>

Related

v-for with model vuejs

I need to execute a v-for, however I do not know how to add a v-model for each different component inside of v-for;
<template>
<ProfilePasswordField
v-for="(item, index) in profilePasswordItems"
:key="index"
:profilePasswordItem="item"
v-model="???"
>
</template>
This v-for will always be three items and I want to name the v-model's as: ['passaword', 'newPassword', confirmNewPassword']
How can I add those names dinamically for the v-model inside v-for?
I tried to do a list inside data() but it did not work. Something like that:
data (){
return{
listPassword: ['passaword', 'newPassword', 'confirmNewPassword']
}
},
methods: {
method1 () {
console.log(this.passaword)
console.log(this.newPassword)
console.log(this.confirmNewPassword)
}
}
The v-model directives cannot update the iteration variable itself therefore we should not use a linear array item in for-loop as the v-model variable.
There is another method you can try like this-
In the parent component-
<template>
<div>
<ProfilePasswordField
v-for="(item, index) in listPassword"
:key="index"
:profilePasswordItem="item"
v-model="item.model"
/>
<button #click="method1()">Click to see changes</button>
</div>
</template>
<script>
export default {
name: "SomeParentComponent",
data() {
return {
listPassword: [
{ model: "passaword" },
{ model: "newPassword" },
{ model: "confirmNewPassword" },
],
};
},
methods: {
method1() {
this.listPassword.forEach((item) => {
console.log(item.model);
});
},
},
}
</script>
And in your ProfilePasswordField you can emit the input event to listen to the respected changes in v-model binding. For example, the ProfilePasswordField component could be like this-
<template>
<v-text-field :value="value" #input="$emit('input', $event)"/>
</template>
<script>
export default {
name: "ProfilePasswordField",
props: {
value: {
required: true
}
}
</script>
In the parent component's console, you should see the changes made by the child component.

Vue.js - loop through simple Object and send Object when user clicks a checkbox

I am new to Vue.js and would like to understand how v-model works with a checkbox.
I am working with vuetify checkboxes.
My components gets an object as value prop and I would like to display checkboxes according to each key value pair that would look like that
this.value = {property1: true, property2: false}
So here i want to display a checkbox with lable property1 and the checkbox should be checked as the value is true. The second checkbox would be unchecked.
When checking a checkbox I want to send an object with the key and the value in order to save it. I am only able to get the value for now, but what can I do with it ?
If I try to get the key and the value, the issue is that when i uncheck it sends null instead of the key, value pair, how should i manage this ?
<template>
<div class="d-flex flex-wrap justify-space-between">
<div class="d-flex flex-wrap flex-column">
<v-checkbox
class="add-checkbox"
ref="additionalDetails"
v-for="(value, key) in additionalDetails"
type="checkbox"
:key="key"
:value="{key, value}"
v-model="additionalDetails"
:label="key"
><template v-slot:label
><span class="body-2"
>{{
key
}}
</span>
</template></v-checkbox
>
</div>
</div>
</template>
<script>
export default {
name: "additional-details",
props: {
value: Object,
},
components: {},
data: function () {
return {
newAdditionalDetails: [],
};
},
computed: {
additionalDetails: {
get: function () {
return this.value;
},
set: function ({ key, value}) {
let newObject = { ...this.value };
newObject[key] = value;
this.newAdditionalDetails = newObject
},
},
},
methods: {},
beforeMount() {},
};
</script>
v-model should be same type of value you want to track, in this case an object, but actually because of the v-for an array of objects, initialized with the values you expect the checkboxes to hold/emit. The v-checkbox component will also need to be explicitely told what object key-value equals 'truthy' vs 'falsy'. Another tip: since you're also looping over object properties, a third alias in the v-for will give you the index. Something like this will work:
<template>
<div>
<v-checkbox
v-for="(value, key, index) in initValues" :key=index
:label=key
v-model=checkboxes[index]
:value="{ key: key, value: value }"
:false-value="{ key: key, value: false }"
:true-value="{ key: key, value: true }"
#change="log(checkboxes[index])"
></v-checkbox>
</div>
</template>
<script>
export default {
data: () => ({
initValues: {
'hi': true,
'bye': false,
'ok': true
},
checkboxes: []
}),
methods: {
log(newValue) {
console.log(newValue)
}
},
created() {
// initialize array of objects where each object is a key-value pair from initValues
this.checkboxes = Object.entries(this.initValues).map(( [k, v] ) => ({ [k]: v }))
}
};
</script>
It's a bit tricky, and is honestly not how I would've done it. A simpler solution would be to track just the value and create/send the key-value pair as the checkbox input changes, e.g.
<template>
<div>
<v-checkbox
v-for="(value, key, index) in initValues" :key=index
:label="key"
v-model=checkboxes[index]
#change="log(key, checkboxes[index])"
></v-checkbox>
</div>
</template>
<script>
export default {
data: () => ({
initValues: {
'hi': true,
'bye': false,
'ok': true
},
checkboxes: []
}),
methods: {
log(key, newValue) {
console.log({ [key]: newValue })
}
},
created() {
this.checkboxes = Object.values(this.initValues)
}
};
</script>
Instead of writing this much complex logic, You can simply achieve that by binding object property in v-model as per the additionalDetails.
Live Demo :
new Vue({
el: '#app',
vuetify: new Vuetify(),
data () {
return {
additionalDetails: [{
property1: true
}, {
property2: false
}]
}
},
methods: {
getChecboxValues() {
console.log('additionalDetails', JSON.stringify(this.additionalDetails))
}
}
})
<script src="https://unpkg.com/vue#2.x/dist/vue.js"></script>
<script src="https://unpkg.com/vuetify#2.6.8/dist/vuetify.min.js"></script>
<link rel="stylesheet" href="https://unpkg.com/vuetify#2.6.8/dist/vuetify.min.css"/>
<link rel="stylesheet" href="https://unpkg.com/#mdi/font#6.x/css/materialdesignicons.min.css"/>
<div id="app">
<v-app id="inspire">
<v-container
class="px-0"
fluid
>
<v-checkbox
v-for="(value, key) in additionalDetails"
:key="key"
:label="Object.keys(value)[0]"
v-model="value[Object.keys(value)[0]]"
></v-checkbox>
<v-btn depressed color="primary" #click="getChecboxValues">
Submit
</v-btn>
</v-container>
</v-app>
</div>
Finally what I did I converted to an array the initail object (that comes in stringified by the way here) and added the checked prop to update in the v-model. I also had to add a method to set the new object (The example code below is a bit different from the original one sorry for that) Somehow the setter of the computed additionalDetails never triggers I have no clue why, if someone has, thanks in advance for sharing toughts.
<template>
<div class="d-flex flex-wrap justify-space-between">
<div class="d-flex flex-wrap flex-column">
<v-checkbox class="add-checkbox" ref="additionalDetails" v-for="(prop, index) in additionalDetails" type="checkbox" :label="prop.label" :key="index" :dataIndex="index" v-model="prop.checked" #change="setAdditionalDetails(prop)"><template v-slot:label><span class="body-2">{{ prop.label }} </span>
</template></v-checkbox>
</div>
</div>
</template>
<script>
export default {
name: "additional-details",
props: {
value: Object,
},
computed: {
additionalDetails: {
get: function() {
if (this.value.JsonAdditionalFields !== null) {
let res = [];
let parsedJsonAdditionalFields = JSON.parse(
this.value.JsonAdditionalFields
);
for (const [key, value] of Object.entries(
parsedJsonAdditionalFields
)) {
res = [
...res,
{
[key]: value,
checked: value,
label: "label"
},
];
}
return res;
} else {
return [];
}
},
},
},
methods: {
setAdditionalDetails(val) {
let newObject = { ...this.value
};
newObject.JsonAdditionalFields = JSON.parse(
newObject.JsonAdditionalFields
);
newObject.JsonAdditionalFields[Object.keys(val)[0]] = val.checked;
this.value.JsonAdditionalFields = JSON.stringify(
newObject.JsonAdditionalFields
);
},
},
watch: {
additionalDetails: {
handler(newVal) {
console.log("additionalDetails new Value:", newVal);
},
deep: true,
},
},
};
</script>

Vue2: Unable to bind a prop to a named slot

I'm not sure why this isn't working... should be straight forward. I'm unable to bind a simple object to a named slot in one of my components:
I should be able to do the following:
Create a named slot and then bind a property to it:
<slot name="actions" :item="item" />
data(){
return {
item: {val1: 1, val2: 2}
}
}
Use it in this fashion:
<template #actions="{ item }">
<pre>{{ item }}</pre>
</template>
However, when I do this, this slot will not even render...
Below is my entire component code:
<template>
<v-dialog v-model="dialog" :persistent="persistent" :width="width">
<template #activator="{ on: dialogActivator, attrs: dialogAttrs }">
<v-tooltip bottom :disabled="!tooltipText">
<template #activator="{ on: tooltipActivator, tooltipAttrs }">
<v-btn
v-bind="{ ...dialogAttrs, ...tooltipAttrs, ...$attrs }"
v-on="{ ...dialogActivator, ...tooltipActivator }"
#click="$emit('handle-dialog-open-click')"
>
<slot name="activator"> Open </slot>
</v-btn>
</template>
<span>{{ tooltipText }}</span>
</v-tooltip>
</template>
<v-card :height="cardHeight">
<v-card-title
v-if="hasTitleSlot"
class="d-flex justify-space-between"
>
<slot name="title" />
</v-card-title>
<slot v-if="dialog" />
<v-card-actions v-if="hasActionsSlot" class="d-flex justify-end">
<slot name="actions" :item="obj" />
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
export default {
inheritAttrs: false,
props: {
width: {
type: String,
default: '500',
},
tooltipText: {
type: String,
default: '',
},
persistent: {
type: Boolean,
default: false,
},
cardHeight: {
type: String,
default: '',
},
},
data() {
return {
dialog: false,
obj: {
val1: 1,
val2: 2,
},
};
},
computed: {
hasTitleSlot() {
return !!this.$slots.title;
},
hasActionsSlot() {
return !!this.$slots.actions;
},
},
created() {
this.$root.$on('close-dialog', this.closeModal);
},
beforeDestroy() {
this.$root.$off('close-dialog', this.closeModal);
},
methods: {
closeModal() {
this.dialog = false;
},
},
};
</script>
It feels like its a simple typo somewhere...
EDIT:
Confirming that it works fine if I do not try to extract the prop like this:
<template #actions>
Some awesome action goes here
</template>
Problem is is in v-if="hasActionsSlot"
hasActionsSlot() {
return !!this.$slots.actions;
},
This method always returns false as your <slot name="actions" :item="obj" /> is not a regular slot, it is a scoped slot! And because this is not Vue 3 (where all slots, scoped or not are part of $slots), you need to use:
hasActionsSlot() {
return !!this.$scopedSlots.actions;
},
See $slots VS $scopedSlots
since 2.6.0+: All $slots are now also exposed on $scopedSlots as functions. If you work with render functions, it is now recommended to always access slots via $scopedSlots, whether they currently use a scope or not. This will not only make future refactors to add a scope simpler, but also ease your eventual migration to Vue 3, where all slots will be functions.
So in Vue 2.6+ it is always safer to work with $scopedSlots (as it contains all the slots)

How to pass old value to setter if nothing is changed in computed property vuejs?

I'm learning to use Vuejs so don't mind me!
I have set getter and setter inside computed property and use it inside form with v-model. My problem is, if I change something inside the v-text-field then I can make patch request but if I don't change anything and leave it the value I got from the state then I can't make the patch request as it said the field may not be null.
How can I leave default value (value I get from state) to the v-text-field and be able to make patch request if i don't want to change anything inside the field.
my vue component.vue
<template>
<div id="update-post">
<v-dialog v-model="updatePostModal" max-width="600">
<v-card class="px-5 py-5">
<v-form ref="form" v-on:submit.prevent="update">
<v-text-field
type="text"
v-model="title" <---
label="Title"
required
></v-text-field>
</v-form>
<v-card-actions>
<v-btn color="green darken-1 white--text" #click="update">
Update
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</template>
<script>
import { getAPI } from "../axios-base";
export default {
name: "UpdatePostModal",
data() {
return {
updateTitle: null,
};
},
computed: {
title: {
get() {
var title = this.$store.state.APIData.postTitle;
return title;
},
set(value) {
this.updateTitle = value;
},
},
},
methods: {
update: function() {
var id = this.$store.state.APIData.postID;
getAPI
.patch(
`/api/posts/${id}/`,
{
title: this.updateTitle,
},
{
headers: {
Authorization: `Bearer ${this.$store.state.accessToken}`,
},
}
)
},
};
</script>
It seems like you have a few mistakes. First one, in here:
.patch(
`/api/posts/${id}/`,
{
title: this.updateTitle,
},
You are using the variable defined in data in your request. You should instead use the computed property so, it should be:
.patch(
`/api/posts/${id}/`,
{
title: this.title,
},
Next, the way you are using the state is also not right. If you are reading the computed property from the state you should always set it via the state as well. Otherwise, you'd end up with some unexpected behavior as your app grows. In order to do that you can do something like this:
get() {
// read from the state
},
set(value) {
// create an action to update the post title in the state
store.dispatch(
'updatePostTitle',
value
);
}
If you still don't want to do this, as a workaround you can address your problem like this (keeping in mind to fix the first issue mentioned above):
computed: {
title: {
get() {
return this.updateTitle || this.$store.state.APIData.postTitle;
},
set(value) {
this.updateTitle = value;
},
},
},

creating abstract components that can manage external data

Currently I use Vuetify for base components and would like to create reusable extensions. For example a list containing checkboxes, a datatable column with some functionality etc.
For this question I will take the list containing checkboxes example. I created the following component called CheckboxGroup.vue
<template>
<v-container>
<v-checkbox
v-for="(item, index) in items"
:key="index"
v-model="item.state"
:label="item.title"
></v-checkbox>
</v-container>
</template>
<script>
export default {
props: {
items: Array,
required: true
}
};
</script>
This component takes an array of objects as a property and creates a checkbox for each entry.
Important parts are v-model="item.state" and :label="item.title". Most of the time the state attribute will have a different name, same for the title attribute.
For testing purposes I created a view file called Home.vue holding an array of documents.
<template>
<v-container>
<CheckboxGroup :items="documents"/>
<v-btn #click="saveSettings">Save</v-btn>
</v-container>
</template>
<script>
import CheckboxGroup from "../components/CheckboxGroup";
export default {
components: {
CheckboxGroup
},
data: function() {
return {
documents: [
{
id: 1,
name: "Doc 1",
deleted: false
},
{
id: 2,
name: "Doc 2",
deleted: false
},
{
id: 3,
name: "Doc 3",
deleted: true
}
]
};
},
methods: {
saveSettings: function() {
console.log(this.documents);
}
}
};
</script>
This time title is called name and state is called deleted. Obviously CheckboxGroup is not able to manage the documents because the attribute names are wrong.
How would you solve this problem? Would you create a computed property and rename these attributes? Would be a bad idea I think...
And by the way, is using v-model a good idea? A different solution would be to listen to the changed event of a checkbox and emit an event with the item index. Then you would have to listen for the change in the parent component.
I don't think there is a way to create something like
<CheckboxGroup :items="documents" titleAttribute="name" stateAttribute="deleted"/>
because it would be bad design anyway. I hope that this is a very trivial problem and every Vue developer has been confronted with it, since the primary goal should always be to develop abstract components that can be reused multiple times.
Please keep in mind that this checkbox problem is just an example. A solution for this problem would also solve same or similar problems :)
If I understood what you wanted, it`s not so trivial. Using props is a good idea. You dont need to manage the documents attribute names, just set the attribute name to your component.
Note
Renaming the attributes or using proxies is more resource-intensive like this solution, because you need to run loop to rename the attribute names or apply aliases to data array objects.
Example
CheckboxGroup.vue
<template>
<v-container fluid>
<v-checkbox
v-for="(item, index) in items"
:key="index"
v-model="item[itemModel]"
:label="item[itemValue]"
></v-checkbox>
<hr>
{{items}}
</v-container>
</template>
<script>
export default {
name: "CheckboxGroup",
props: {
items: {
type: Array,
required:true
},
itemValue:{
type:String,
default: 'title',
// validate props if you need
//validator: function (value) {
// return ['title', 'name'].indexOf(value) !== -1
// }
// or make required
},
itemModel:{
type:String,
default: 'state',
// validate props if you need
//validator: function (value) {
// validate props if you need
// return ['state', 'deleted'].indexOf(value) !== -1
// }
// or make required
}
}
};
</script>
Home.vue
<template>
<div id="app">
<checkbox-group :items="documents"
item-value="name"
item-model="deleted"
>
</checkbox-group>
</div>
</template>
<script>
import CheckboxGroup from "./CheckboxGroup.vue";
export default {
name: "App",
components: {
// HelloWorld,
CheckboxGroup
},
data: function() {
return {
documents: [
{
id: 1,
name: "Doc 1",
deleted: false
},
{
id: 2,
name: "Doc 2",
deleted: false
},
{
id: 3,
name: "Doc 3",
deleted: true
}
]
}
}
};
</script>
Based on your example I`v tried to show how to create component to managing object attributes in child component. If you need more information, please let me know.
Some good answers here that definitely solve your issue - you are essentially wanting to pass data down to a child (which isn't bad design - you were on the right track!)..
I am kind of shocked that slots or scoped-slots haven't been mentioned yet... so I figured I would chime in..
Scoped-slots allow you to take advantage of data you are passing to a child - but within the parent. The child essentially "reflects" data back to the parent, which allows you to style the child component/slot however you wish, from the parent.
This is different than just passing data via a prop attribute, because you would have to rely on styling within the child - you couldn't change the styles on a 'per-use' basis. The styles you set in the child would be "hard coded"..
In this example I am riding on top of the already provided label slot that Vuetify provides - just passing my own custom scoped-slot to it.. How to find documentation on v-checkbox slots
I made some minor changes to help spice some things up, and to show how you have greater control over styles this way (and you can use any object prop for the label you want .name, .whatever, .label, etc..)
Lastly, it is important to note that Vuetify already provides a "grouped checkbox" component - v-radio-group - I know it's called "radio-group" but it supports checkboxes...
Edit: fixed the state "issue"...
Scoped Slots With Render Function - Original Answer Moved To Bottom
Thanks to #Estradiaz for collaborating on this with me!
Vue.component('checkboxgroup', {
props: {
items: { type: Array, required: true }
},
render (h) {
return h('v-container', this.items.map((item) => {
return this.$scopedSlots.checkbox({ item });
}));
},
})
new Vue({
el: "#app",
data: {
documents: [{
id: 1,
name: "Doc 1 - delete",
deleted: false,
icon: "anchor",
},
{
id: 12,
title: "Doc 1 - state",
state: false,
icon: "anchor",
},
{
id: 2,
name: "Doc 2 - delete",
deleted: false,
icon: "mouse"
},
{
id: 3,
name: "Doc 3 - delete",
deleted: true,
icon: "watch"
}
]
},
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.min.js"></script>
<script src="https://unpkg.com/vuetify/dist/vuetify.min.js"></script>
<link href='https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons' rel="stylesheet" type="text/css">
<link href="https://unpkg.com/vuetify/dist/vuetify.min.css" rel="stylesheet" type="text/css"></link>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://use.fontawesome.com/releases/v5.0.8/css/all.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/MaterialDesign-Webfont/2.1.99/css/materialdesignicons.min.css" rel="stylesheet" />
<div id="app">
<v-app>
<v-container>
<CheckboxGroup :items="documents">
<template #checkbox={item}>
<v-checkbox
v-model="item[(item.name && 'deleted') || (item.title && 'state') ]" color="red">
<template #label>
<v-icon>mdi-{{item.icon}}</v-icon>
{{ item.name || item.title }}
{{ item }}
</template>
</v-checkbox>
</template>
</CheckboxGroup>
</v-container>
</v-app>
</div>
My attempt of a json to component parser
usefull names are welcome
so basically you can target element tagnames as slot #[slotname] or put slot names and target entries to overwrite the default component.
omiting tag property in the component will append children to the parent vnode
Consider:
[
{
ElementTag: 'Liste',
id: 1,
tag: 'p',
items: [
{
ElementTag: 'input',
id: 11,
type: 'checkbox',
title: "Sub Doc 1 - state",
state: true,
slotName: "slotvariant"
},
{
ElementTag: 'input',
id: 12,
type: 'date',
title: "Sub Doc 2 - Date",
date: "",
}
]
},
{
ElementTag: 'input',
id: 2,
type: 'checkbox',
title: "Doc 2 - deleted",
deleted: true,
slotName: 'deleted'
}
]
Example :
Vue.component('Liste', {
props:["tag", "items"],
render(h){
console.log(this.items)
let tag = this.tag || (this.$parent.$vnode && this.$parent.$vnode.tag)
if(tag === undefined) throw Error(`tag property ${tag} is invalid. Scope within valid vnode tag or pass valid component/ html tag as property`)
return h(tag, this.items.map(item => {
const {ElementTag, slotName, ...attrs} = item;
return (
this.$scopedSlots[slotName || ElementTag]
&& this.$scopedSlots[slotName || ElementTag]({item})
)
|| h(ElementTag, {
attrs: attrs,
scopedSlots: this.$scopedSlots
})
}))
}
})
new Vue({
data(){
return {
items: [
{
ElementTag: 'Liste',
id: 1,
tag: 'p',
items: [
{
ElementTag: 'input',
id: 11,
type: 'checkbox',
text: "Sub Doc 1 - state",
state: true,
slotName: "slotvariant"
},
{
ElementTag: 'input',
id: 12,
type: 'date',
title: "Sub Doc 2 - Date",
date: "",
}
]
},
{
ElementTag: 'input',
id: 2,
type: 'checkbox',
title: "Doc 2 - deleted",
deleted: true,
slotName: 'deleted'
}
]}
}
}).$mount('#app')
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.min.js"></script>
<div id="app">
<Liste tag="p" :items="items">
<template #input="{item}">
<label :for="item.id">
{{ item.title }}
</label>
<input :type="item.type" :id="item.id" v-model="item.date"/>
</template>
<template #slotvariant="{item}">
slotvariant - {{item.text}}<br>
</template>
<template #deleted="{item}">
<label :for="item.id">
{{ item.title }}
</label>
<input :type="item.type" :id="item.id" v-model="item.deleted"/>
</template>
</Liste>
</div>
Typescript:
import {Vue, Component, Prop} from 'vue-property-decorator'
export type AbstractElement = {
[key: string]: any // passed as $attrs | useable for assigned $props
ElementTag: string
slotName?: string
}
#Component<List>({
render(h){
let tag = this.tag
|| (this.$parent.$vnode && this.$parent.$vnode.tag)
|| (this.$parent.$el && this.$parent.$el.tagName)
if(tag === undefined) throw Error(`tag prperty: ${tag} is invalid. Scope within valid vnode tag or pass valid component/ html tag as property`)
return h(tag, this.items.map(item => {
const {ElementTag, slotName, ...attrs} = item;
console.log("slotName", slotName)
return (this.$scopedSlots[slotName || ElementTag]
&& this.$scopedSlots[slotName || ElementTag]({item}))
|| h(ElementTag, {
attrs: attrs,
slot: slotName || ElementTag,
scopedSlots: this.$scopedSlots
})
}))
}
})
export default class List extends Vue{
#Prop(String) readonly tag?: string
#Prop(Array) readonly items!: Array<AbstractElement>
}
will raise this here
You can use a Proxy to map the document property names during access.
Note
In my original answer, I used Proxy handlers for get and set, which is sufficient for plain javascript objects, but fails when used with Vue data properties because of the observer wrappers that Vue applies.
By also trapping has in the Proxy, this can be overcome. I left the original answer below for anyone interested in this problem.
Here is a demo of how to use Proxy to 'alias' Vue reactive properties to different names
without affecting the original data structure
without having to copy the data
console.clear()
Vue.config.productionTip = false
Vue.config.devtools = false
Vue.component('checkboxgroup', {
template: '#checkboxGroup',
props: { items: Array, required: true },
});
const aliasProps = (obj, aliasMap) => {
const handler = {
has(target, key) {
if (key in aliasMap) {
return true; // prevent Vue adding aliased props
}
return key in target;
},
get(target, prop, receiver) {
const propToGet = aliasMap[prop] || prop;
return Reflect.get(target, propToGet);
},
set(target, prop, value, receiver) {
const propToSet = aliasMap[prop] || prop;
return Reflect.set(target, propToSet, value)
}
};
return new Proxy(obj, handler);
}
new Vue({
el: '#app',
data: {
documents: [
{ id: 1, name: "Doc 1", deleted: false },
{ id: 2, name: "Doc 2", deleted: false },
{ id: 3, name: "Doc 3", deleted: true },
]
},
computed: {
checkBoxItems() {
const aliases = {
title: 'name',
state: 'deleted'
}
return this.documents.map(doc => aliasProps(doc, aliases));
}
},
methods: {
saveSettings: function() {
console.log(this.documents);
}
},
});
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="https://unpkg.com/vuetify/dist/vuetify.min.js"></script>
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons" rel="stylesheet"/>
<link href="https://unpkg.com/vuetify/dist/vuetify.min.css" rel="stylesheet"/>
<div id="app">
<v-app id="theapp">
<v-container>
<checkboxgroup :items="checkBoxItems"></checkboxgroup>
<v-btn color="info"
#click="saveSettings">Save</v-btn>
</v-container>
</v-app>
</div>
<template id="checkboxGroup">
<v-container style="display: flex">
<v-checkbox
v-for="(item, index) in items"
:key="index"
v-model="item.state"
:label="item.title"
></v-checkbox>
</v-container>
</template>
Original answer
You can use a Proxy to map the document property names during access.
<template>
...
<CheckboxGroup :items="checkBoxItems"/>
...
</template>
<script>
export default {
...
computed: {
checkBoxItems() {
const handler = {
get: function(target, prop) {
return prop === 'title' ? target.name :
prop === 'state' ? target.deleted :
target[prop];
},
set(obj, prop, value) {
const propToSet =
prop === 'title' ? 'name' :
prop === 'state' ? 'deleted' :
prop;
obj[propToSet] = value;
}
};
return documents.map(doc => new Proxy(doc, handler))
},
},
...
}
</script>
Demo
const documents = [
{ id: 1, name: "Doc 1", deleted: false },
{ id: 2, name: "Doc 2", deleted: false },
{ id: 3, name: "Doc 3", deleted: true },
]
const handler = {
get: function(target, prop) {
return prop === 'title' ? target.name :
prop === 'state' ? target.deleted :
target[prop];
},
set(obj, prop, value) {
const propToSet =
prop === 'title' ? 'name' :
prop === 'state' ? 'deleted' :
prop;
obj[propToSet] = value;
}
};
const checkItems = documents.map(doc => new Proxy(doc, handler))
console.log('Accessing new property names via checkItems')
checkItems.forEach(ci => console.log(ci.id, ci.title, ci.state))
console.log('After update, values of documents')
checkItems.forEach(ci => ci.state = !ci.state )
documents.forEach(doc => console.log(doc.id, doc.name, doc.deleted))

Categories

Resources