создание абстрактных компонентов, которые могут управлять внешними данными

В настоящее время я использую 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 столкнулся с ней, поскольку основная цель всегда должна заключаться в разработке абстрактных компонентов, которые можно многократно использовать повторно.

Пожалуйста, имейте в виду, что эта проблема с флажками является лишь примером. Решение этой проблемы также решит те же или похожие проблемы :)

Ответ 1

Если я понял, что вы хотели, это не так тривиально. Использование реквизита это хорошая идея. Вам не нужно управлять именами атрибутов документов, просто установите имя атрибута для вашего компонента.

Заметка

Переименование атрибутов или использование прокси более ресурсоемки, как это решение, потому что вам нужно запустить цикл, чтобы переименовать имена атрибутов или применить псевдонимы к объектам массива данных.

пример

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>

На основе вашего примера я попытался показать, как создать компонент для управления атрибутами объекта в дочернем компоненте. Если вам нужна дополнительная информация, пожалуйста, дайте мне знать.

Ответ 2

Вы можете использовать прокси для сопоставления имен свойств документа во время доступа.

Заметка
В своем первоначальном ответе я использовал обработчики Proxy для get и set, что достаточно для простых объектов javascript, но не удается при использовании со свойствами data Vue из-за оболочек-наблюдателей, применяемых Vue.

Также путем захвата has в прокси - сервер, это можно преодолеть. Я оставил оригинальный ответ ниже для всех, кто интересуется этой проблемой.

Вот демонстрация того, как использовать прокси для псевдонима 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>

Ответ 3

Здесь есть несколько хороших ответов, которые определенно решают вашу проблему - вы, по сути, хотите передать данные ребенку (что не так уж плохо - вы были на правильном пути!)..

Я шокирован тем, что slots или slots scoped-slots еще не упоминались... поэтому я решил, что я буду вмешиваться...

Слоты Scoped позволяют вам использовать преимущества данных, которые вы передаете ребенку, но внутри родительского элемента. Дочерний процесс по существу "отражает" данные обратно к родителю, что позволяет вам стилизовать дочерний компонент/слот по своему усмотрению от родителя.

Это отличается от простой передачи данных через атрибут prop, потому что вам придется полагаться на стили в дочернем элементе - вы не можете изменять стили в зависимости от использования. Стили, которые вы установили в дочернем элементе, были бы "жестко запрограммированы"..

В этом примере я верхом на вершине, уже представленной label слота, scoped-slot который Vuetify обеспечивает - просто передавая свой собственный scoped-slot к нему.. Как найти документацию по v-CheckBox слотов

Я сделал некоторые незначительные изменения, чтобы помочь приправе некоторых вещей, и чтобы показать, как вы иметь больший контроль над стилями этим путем (и вы можете использовать любой объект пропеллер для метки вы хотите .name, .whatever, .label, и т.д..)

Наконец, важно отметить, что Vuetify уже предоставляет компонент "сгруппированные флажки" - v-radio-group - я знаю, что он называется "radio-group", но он поддерживает флажки...

Редактировать: исправлено state "проблема"...

Edit Vue with Vuetify - Eagles

Слоты с областью видимости с функцией рендеринга - оригинальный ответ перемещен в нижнюю часть

Спасибо @Estradiaz за сотрудничество со мной!

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>

Ответ 4

Нет другого шанса, кроме как сказать вашему компоненту, что есть что-то другое, чем обычно.




Я могу думать о трех способах, с третьим, как комбинация первых двух.

TL; DR: v-bind="{...desiredProps}"


Одним из подходов является использование специальных реквизитов:

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

Другим способом будет передача свойства элемента

documents: [
    {
      id: 1,
      name: "Doc 1",
      deleted: false,
      model: {
        value: 'name',
        state: 'deleted'
      }
    }, 
...]

И просто брось это:

<template>
  <v-container>
    <v-checkbox
      v-for="(item, index) in items"
      :key="index"
      v-model="item[(item.model && item.model.value) || 'state' /*assuming state is default*/]"
      :label="item[(item.model && item.model.value) || 'title' /*assuming title is default*/]"
    ></v-checkbox>
  </v-container>
</template>

Или вы можете просто обмануть и использовать оба подхода вместе.

Сначала очистите ваши данные в форме {[passedPropName]: "passedPropValue"}:

  data: {

    itemTitle: "name",
    itemModel: "deleted",
    items: [
      {
        id: 1,
        name: "Doc 1",
        deleted: false
      },
      {
        id: 2,
        name: "Doc 2",
        deleted: false
      },
      {
        id: 3,
        name: "Doc 3",
        deleted: true
      }
    ]
  }

а затем использовать магию v-bind:

<template>
  <v-container>
    <CheckboxGroup v-bind="data"/>
  </v-container>
</template>

<script>
import CheckboxGroup from "../components/CheckboxGroup";

export default {
  components: {
    CheckboxGroup
  },
  data: function() {
    return {
      data: {
        itemTitle: "name",
        itemModel: "deleted",
        items: [
          {
            id: 1,
            name: "Doc 1",
            deleted: false
          },
          {
            id: 2,
            name: "Doc 2",
            deleted: false
          },
          {
            id: 3,
            name: "Doc 3",
            deleted: true
          }
        ]
      }
    };
  }
};
</script>

Ответ 5

Моя попытка json для парсера компонентов

полезные имена приветствуются


поэтому в основном вы можете #[slotname] элемента как slot #[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>