创建可以管理外部数据的抽象组件

时间:2019-06-06 14:04:47

标签: javascript vue.js vuetify.js

当前,我将Vuetify用于基本组件,并希望创建可重用的扩展。例如,包含复选框的列表,具有某些功能的数据表列等。

对于这个问题,我将以包含复选框的列表为例。我创建了以下名为 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>

该组件将对象数组作为属性,并为每个条目创建一个复选框。

重要的部分是v-model="item.state":label="item.title"。大多数情况下,state属性的名称与title属性的名称不同。

出于测试目的,我创建了一个名为 Home.vue 的视图文件,其中包含一系列文档。

<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>

这次,title被称为name,而state被称为deleted。显然CheckboxGroup无法管理文档,因为属性名称错误。

您将如何解决此问题?您将创建一个计算属性并重命名这些属性吗?我认为这是个坏主意...

顺便说一句,使用v-model是个好主意吗?另一种解决方案是侦听复选框的更改事件并发出带有项目索引的事件。然后,您将不得不侦听父组件中的更改。

我认为没有办法创建类似的东西

<CheckboxGroup :items="documents" titleAttribute="name" stateAttribute="deleted"/> 

因为无论如何这都是不好的设计。我希望这是一个非常琐碎的问题,每个Vue开发人员都已经遇到了这个问题,因为主要目标应该始终是开发可以多次重用的抽象组件。

请记住,此复选框问题仅是示例。解决此问题的方法还可以解决相同或相似的问题:)

4 个答案:

答案 0 :(得分:7)

如果我了解您的要求,那不是那么简单。使用道具是个好主意。您无需管理文档属性名称,只需将属性名称设置为您的组件即可。

注意

像这种解决方案一样,重命名属性或使用代理会占用大量资源,因为您需要运行循环来重命名属性名称或将别名应用于数据数组对象。

示例

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>

基于您的示例,我尝试演示如何创建组件来管理子组件中的对象属性。如果您需要更多信息,请告诉我。

答案 1 :(得分:5)

您可以在访问期间使用Proxy映射文档属性名称。

注意
在我的原始答案中,我对getset使用了 Proxy 处理程序,足以处理普通的javascript对象,但是在与Vue data属性一起使用时失败了,因为Vue应用的观察者包装的数量。

通过将has捕获在代理中,可以解决此问题。我将下面的原始答案留给了对此问题感兴趣的人。

这里是演示如何使用 Proxy 将Vue反应性属性“别名”为不同名称

  • 不影响原始数据结构
  • 无需复制数据

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>


原始答案

您可以在访问期间使用Proxy映射文档属性名称。

<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>

演示

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))

答案 2 :(得分:2)

这里有一些肯定可以解决您问题的好答案-您本质上是想将数据传递给孩子(这不是坏设计-您走对了!)。

对于slotsscoped-slots尚未被提及我感到很震惊...所以我想我会插话。

作用域槽可让您利用传递给孩子的数据-但在父级中。子级实质上将数据“反射”回父级,这使您可以从父级中随意设置子组件/插槽的样式。

这与仅通过prop属性传递数据不同,因为您将不得不依赖子级中的样式-您无法基于“每次使用”来更改样式。您在子级中设置的样式将为“硬编码”。

在此示例中,我骑在已经提供的label slot that Vuetify provides之上-只是将自己的自定义scoped-slot传递给它。.How to find documentation on v-checkbox slots

我做了一些小的更改,以帮助增加一些趣味性,并展示如何通过这种方式更好地控制样式(并且您可以将任何对象属性用于所需的标签.name,{{1} },.whatever等。)

最后,请注意.label已经提供了“分组复选框”组件-Vuetify-我知道它被称为“无线电组”,但它支持复选框...

编辑:修复了v-radio-group“问题” ...

Edit Vue with Vuetify - Eagles

具有渲染功能的作用域插槽-原始答案移到底部

感谢@Estradiaz与我合作!

state
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"
      }
    ]
  },
})

答案 3 :(得分:1)

我尝试使用json进行组件解析器

欢迎使用全名


因此,基本上,您可以将元素标记名作为插槽#[slotname]定位,或放置插槽名称和目标条目以覆盖默认组件。

省略组件中的tag属性会将子级追加到父级vnode


考虑:

      [
        {
            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'
        }
    ]

示例:

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>


  

打字稿:

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