How to use render function to encapsulate highly extensible components

Time:2021-9-26

How to use render function to encapsulate highly extensible components

As mentioned in the previous article, the example of render function given on Vue’s official website can only reflect the elegant aspect of render function, but can’t see its extensibility. Today we will encapsulate a component that reflects its extensibility.

demand

In the background management, there are often data display requirements with the following layout:

How to use render function to encapsulate highly extensible components

A table is not a table, and a form is not a form. In fact, it looks like a table. The data presented is an object, which is the same as the bound value of the form. I call it a form table.

The column with deep style is the title, and the column with shallow style is the value corresponding to the title. The data is often returned by the server, and the title is often fixed width. The values may be various. For example, to display a picture with a value of 01, you need to display yes or no. sometimes you need to add a Modify button to allow users to modify some values. You also need to set a column to span several columns.

Let’s take a look at an implementation based on element UI

Bad implementation

When you see an implementation in the project you take over, first look at the usage

<FormTable :data="lessonPackageArr" :fleldsInfo="lessonPackageInfo" :maxColumn="3" label-width="120px">
  <template #presentedHours="{ data }">
    <div class="flex-box between">
      <span>
        {{ data.presentedHours }}
      </span>
      < span class = "column BTN" @ Click = "editpresentedhours (data)" > Modify</span>
    </div>
  </template>
  <template #gifts="{ data }">
    <div class="flex-box between">
      <span>
        {{ data.gifts }}
      </span>
      < span class = "column BTN" @ Click = "editpresentedhours (data)" > Modify</span>
    </div>
  </template>
</FormTable>

lessonPackageInfoThe object structure is as follows:

//An object used to configure the title column and the fields corresponding to the title column
//Type specifies the type of value. Now, the internal settings of the component may display which types of values
//For the number of services that need to display whether to return 1 or 0, provide a map_ Data to map
//The column property is set to span columns
//You need to customize the display content to provide a slot
lessonPackageInfo: {
    Ordertype: {type: 'option', desc: 'class package category', map_data: {1: 'first order', 2: 'Renewal', 5: 'free course'}},
    Combo: {type: 'text', desc: 'package name'},
    Presentedhours: {type: 'text', desc: 'complimentary class', slot: true},
    Price: {type: 'text', desc: 'standard price'},
    Gifts: {type: 'text', desc: 'gift', column: 3, slot: true},
  }
  1. Props is not intuitive enough and there are many configuration items
  2. Not fully data driven

Why are there many bad configuration items for components?

The requirement is fixed, and the input of the component ispropsIt should be minimized and component functions should be maximized. Try to provide default values for props, so as to improve the development efficiency of the team.

Why not completely data-driven?

This component is not completely data-driven. You need to customize the display column and write a template.

If you need to customize a lot of columns, you need to write a lot of template code. If you want to extract it again, you can only encapsulate the components again. If you don’t extract it, the template code may expand. You may often see 500 lines of template? The inflated template code makes component maintenance difficult and requires switching back and forth between template and JS code. Moreover, adding a column of user-defined data requires at least two modifications.

Why do you need to be completely data-driven?

Although there are slots to extend components, we should use less when writing business components, and try to use data-driven templates. Because the data is JS code, when the component code expands, it is easy to extract the JS code into a separate file. If you want to extract the slot code, you can only encapsulate the component.

The design concepts of the three front-end frames areData driven templateThis is an important feature that distinguishes them from jQuery, and it is also the principle that we first follow when encapsulating business components.

After looking at the use of components, look at the code of components:

<template>
  <div v-if="tableData.length" class="form-table">
    <div v-for="(data, _) in tableData" :key="_" class="table-border">
      <el-row v-for="(row, index) in rows" :key="index">
        <el-col v-for="(field, key) in row" :key="key" :span="getSpan(field.column)">
          <div v-if="(field.disabled && data[key]) || !field.disabled" class="column-content flex-box between">
            <div class="label" :style="'width:' + labelWidth">
              <span v-if="field.required" class="required">*</span>
              {{ field.desc }}
            </div>
            <div class="text flex-item" :title="data[key]">
              <template v-if="key === 'minAge'">
                <span>{{ data[key] }}</span>
                -
                <span>{{ data['maxAge'] }}</span>
              </template>
              <template v-else-if="key === 'status'">
                <template v-if="field.statusList">
                  <span v-if="data[key] == 0" :class="field.statusList[2]">{{ field.map_data[data[key]] }}</span>
                  <span v-else-if="data[key] == 10 || data[key] == 34" :class="field.statusList[1]">
                    {{ field.map_data[data[key]] }}
                  </span>
                  <span v-else :class="field.statusList[0]">{{ field.map_data[data[key]] }}</span>
                </template>
                <span v-else>{{ field.map_data[data[key]] }}</span>
              </template>

              <slot v-else :name="key" v-bind:data="data">
                <TableColContent
                  :dataType="field.type"
                  :metaData="data[key]"
                  :mapData="field.map_data"
                  :text="field.text"
                />
              </slot>
            </div>
          </div>
        </el-col>
      </el-row>
    </div>
  </div>
  < div else class = "form table empty" > no data yet < / div >
</template>

<script>
  import TableColContent from '@/components/TableColContent'
  export default {
    name: 'FormTable',
    components: {
      TableColContent,
    },
    props: {
      //Data
      data: {
        required: true,
        type: [Object, Array, null],
      },
      //Field information
      fleldsInfo: {
        required: true,
        type: Object,
        //Classname: {type: "text", desc: "class name", column: 3},
      },
      //Maximum number of columns to display
      maxColumn: {
        required: false,
        type: Number,
        default: 2,
      },
      labelWidth: {
        required: false,
        type: String,
        default: '90px',
      },
    },
    data() {
      return {}
    },
    computed: {
      tableData() {
        if (!this.data) {
          return []
        }
        if (this.data instanceof Array) {
          return this.data
        } else {
          return [this.data]
        }
      },
      rows() {
        const returnArray = []
        let total = 0
        let item = {}
        for (const key in this.fleldsInfo) {
          const nextTotal = total + this.fleldsInfo[key].column || 1
          if (nextTotal > this.maxColumn) {
            returnArray.push(item)
            item = {}
            total = 0
          }
          total += this.fleldsInfo[key].column || 1
          item[key] = this.fleldsInfo[key]
          if (total === this.maxColumn) {
            returnArray.push(item)
            item = {}
            total = 0
          }
        }
        if (total) {
          returnArray.push(item)
        }
        return returnArray
      },
    },
    methods: {
      getSpan(column) {
        if (!column) {
          column = 1
        }
        return column * (24 / this.maxColumn)
      },
    },
  }
</script>

What are the problems?

  1. The template has too many conditional judgments and is not elegant
  2. Customize the display column. You also need to importTableColContent, increasing component complexity

TableColContentInternal or configuration itemtypeMake conditional judgment

Partial code

<span v-else-if="dataType === 'image' || dataType === 'cropper'" :class="className">
  <el-popover placement="right" trigger="hover">
    <img :src="metaData" style="max-width: 600px;" />
    <img slot="reference" :src="metaData" :alt="metaData" width="44" class="column-pic" />
  </el-popover>
</span>

After analyzing the above implementation problems, see a good implementation

Good implementation

Let’s look at the usage:

<template>
  <ZmFormTable :titleList="titleList" :data="data" />
</template>
<script>
  export default {
    name: 'Test',
    data() {
      return {
        Data: {}, // get from the server
        titleList: [
          {Title: 'name', prop: 'name', span: 3},
          {
            Title: 'class work',
            prop: (h, data) => {
              const img =
                (data.workPic && (
                  <ElImage
                    style='width: 100px; height: 100px;'
                    src={data.workPic}
                    preview-src-list={[data.workPic]}
                  ></ElImage>
                )) ||
                ''
              return img
            },
            span: 3,
          },
          {Title: 'work review', prop: 'workcomment', span: 3},
        ],
      }
    },
  }
</script>

Component description:
titleListIs the column configuration of the component. It is an array. The element title attribute is the title. Prop specifies the field taken from data, and span specifies the number of rows spanned by this column value.

Prop supports string and function, which is the way to realize custom display. When this function is large, it can be extracted into an independent JS file, or the whole titlelist can be extracted into a separate JS file.

How are parameters h and data passed in? Or where is this function called?

H yescreateElementFunction, data is the data from within the component, and the data passed in by the parent component is the same value.

When the first parameter of a normal function is h yes, it is a render function.

This method is much simpler to use.

Look at the internal implementation

<template>
  <div class="form-table">
    <ul v-if="titleList.length">
      <!--  Titleinfo is the transformed titlelist -- >
      <li
        v-for="(item, index) in titleInfo"
        :key="index"
        :style="{ width: ((item.span || 1) / titleNumPreRow) * 100 + '%' }"
      >
        <div class="form-table-title" :style="`width: ${titleWidth}px;`">
          <Container v-if="typeof item.title === 'function'" :renderContainer="item.title" :data="data" />
          <span v-else>
            {{ item.title }}
          </span>
        </div>
        <div class="form-table-key" :style="`width:calc(100% - ${titleWidth}px);`">
          <Container v-if="typeof item.prop === 'function'" :renderContainer="item.prop" :data="data" />
          <span v-else>
            {{ ![null, void 0].includes(data[item.prop] && data[item.prop]) || '' }}
          </span>
        </div>
      </li>
    </ul>
    < div else class = "form table no data" > no data yet < / div >
  </div>
</template>

<script>
  import Container from './container.js'
  export default {
    name: 'FormTable',
    components: {
      Container,
    },
    props: {
      titleWidth: {
        type: Number,
        default: 120,
      },
      titleNumPreRow: {
        type: Number,
        default: 3,
        validator: value => {
          const validate = [1, 2, 3, 4, 5, 6].includes(value)
          if (!validate) {
            Console. Error ('titlenumprerow indicates that a row has a header field pair, which can only be an even number of 1-6, and the default is 3 ')
          }
          return validate
        },
      },
      titleList: {
        type: Array,
        default: () => {
          return []
        },
        validator: value => {
          const validate = value.every(item => {
            const { title, prop } = item
            return title && prop
          })
          if (!validate) {
            Console. Log ('the element of the passed in titlelist attribute must contain title and prop attributes')
          }
          return validate
        },
      },
      data: {
        type: Object,
        default: () => {
          return {}
        },
      },
    },
  }
</script>
<!--  Style is not the key, omit -- >

Instead of using dynamic slots, a function component is used to realize custom displayContainer, the component receives a render function as prop.

export default {
  name: 'Container',
  functional: true,
  render(h, { props }) {
    return props.renderContainer(h, props.data)
  },
}

Call the function passed in by titlelist inside the container.

Install NPM experience

After packaging, there is 2.8m, which is very large. It is estimated that no one uses it, so it will not be optimized.

summary

  1. Data driven is preferred when packaging components
  2. The first parameter of a normal function is h, which is the rendering function
  3. Some people may not be used to writing JSX, which can be written in two ways

Recommended Today

Seven Python code review tools recommended

althoughPythonLanguage is one of the most flexible development languages at present, but developers often abuse its flexibility and even violate relevant standards. So PythoncodeThe following common quality problems often occur: Some unused modules have been imported Function is missing arguments in various calls The appropriate format indentation is missing Missing appropriate spaces before and after […]