Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

like image 827
hrp8sfH4xQ4 Avatar asked Jun 06 '19 14:06

hrp8sfH4xQ4


2 Answers

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.

like image 88
Sabee Avatar answered Nov 02 '22 22:11

Sabee


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

Edit Vue with Vuetify - Eagles

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>
like image 41
Matt Oestreich Avatar answered Nov 03 '22 00:11

Matt Oestreich