preface

This background management system project selection with Vue as the main technology stack;

I used React to write projects (antD was used), and it felt great.

So this time we eliminated the Element UI in favor of Ant Design Vue;

After analyzing the whole project prototype, it was found that the React table search component similar to the previous one could be removed

rendering

  • Fixed part of initializationpropsAnd linkage, newslotThe transfer

  • I added another layout display, inline mode, along with fixing some known issues with the component renamed asAdvancedSearch.vue

  • 2019-04-23: Added slider configuration

  • If the length of the incoming data is less than the maximum format, the default display is inline mode, otherwise, the card mode

  • Callbacks support passing custom functions (to return their own combined data formats)

For other features, see the mind map below.

The packaging of specific business is much more complex, but also combined with some custom packaging components, showing that the code length is too long.

Implementation approach

  • What is used to communicate between components

When I wrote the first version yesterday, I used props and a custom event ($on,$emit) to implement it.

The amount of code implemented is excessive, because each layer of component refinement increases in complexity. Various callback to each other to achieve.

Take a close look at the Ant Design Vue documentation and post a similar implementation of React

  • How to do that

To implement an integrated business reusable thing, we first have to sort out the function points we want to implement.

The props tried not to break the exposed features of the documentation controls, but to implement and extend them in a compromise.

Let’s do a mind map to sort out the function points

Problems encountered

  • jsxTo implement the problem

When I first tried to implement it with JSX, I found it too naive. Various errors are reported, especially the Vue command support is a mess

As well as functional components are also a lot of holes, there is no way, obediently return to template writing

Vue officially provides JSX support, which is improving day by day; Github:vue/jsx

  • Control crowding into a lump problem

This may be the style of ANTD Vue version is not properly handled, I have carefully checked. There’s no way to unfold it without a copy of him.

Placeholder won’t open automatically, and digital controls are small

Revised:

The revised

  • Complete the original writingreactVersion some thoughtless things (such as on the returned query object)

usage

For general introductions, the exposed props and changes are as follows

A subitem overrides a feature of the same name brought by the global server and has a higher priority

options type explain
responsive object The layout object of the fence
size string Control size (most have itdefault,small,large)
gutter digital Spacing of controls
datetimeTotimeStamp Boolean type If it is fortrueAll time controls are returned as timestamps
searchDataSource The array object Is the need to render control of the data source, see the source codeprops
@change function This is the callback to the query
@callbackFormat An optional function Pass changes the callback data, not pass or ignore it
// SearchDataSource is the source of data<table-search :SearchDataSource="SearchDataSource" @change="tableSearchChange" />


<table-search :SearchDataSource="SearchDataSource" @change="tableSearchChange" @callbackFormat="formatFunc">
  <a-button type="primary" @click="test">xxxx</a-button>
  <template v-slot:extra>
    <div>fasdfas</div>
  </template>
</table-search>// Methods are filtered directly from null, which is true by default. {tableSearchChange(searchParams) {if (searchParams) {// Perform a query} else {// Perform a reset, The general default is to rerequest the entire list without arguments} console.log(' callback accepted form data: ', searchParams); }}Copy the code

Code implementation

AdvancedSearch.vue

<template>
  <div class="advance-search-wrapper">
    <a-form :form="form" @submit="handleSubmit">
      <template v-if="layoutMode === 'inline'">
        <a-card :bordered="bordered">
          <a-row :gutter="gutter">
            <template v-for="(item, index) in renderDataSource">
              <field-render
                :SearchGlobalOptions="SearchGlobalOptions"
                :itemOptions="item"
                :key="item.fieldName"
                v-show="index < SearchGlobalOptions.maxItem || (index >= SearchGlobalOptions.maxItem && collapsed)"
              />
            </template>
            <a-col :style="{ width: collapsed ? '100%' : 'auto' }">
              <a-tooltip placement="bottom">
                <template slot="title">
                  <span>Execute the query</span>
                </template>
                <a-button type="primary" :size="SearchGlobalOptions.size" @click="handleSubmit" icon="search">The query</a-button>
              </a-tooltip>

              <a-tooltip placement="bottom">
                <template slot="title">
                  <span>Clears the values of all controls</span>
                </template>
                <a-button
                  :size="SearchGlobalOptions.size"
                  style="margin-left: 8px"
                  @click="resetSearchForm"
                  icon="border"
                >reset</a-button>
              </a-tooltip>
              <template v-if="showCollapsedText">
                <a @click="togglecollapsed" style="margin-left: 8px">
                  <a-tooltip placement="bottom">
                    <template slot="title">
                      <span>{{ collapsed ? 'Click to collapse some controls' :' Click to expand all controls'}}</span>
                    </template>{{ collapsed ? 'Fold' : 'expand'}}<a-icon :type="collapsed ? 'up' : 'down'" />
                  </a-tooltip>
                </a>
              </template>
              <slot name="extra" />
            </a-col>
          </a-row>
        </a-card>
      </template>
      <template v-else>
        <a-card :bordered="bordered">
          <template v-slot:title>
            <span style="text-align:left; margin:0;">
              {{ title }}
            </span>
          </template>

          <template v-slot:extra>
            <a-row type="flex" justify="start" align="middle">
              <slot>
                <a-tooltip placement="bottom">
                  <template slot="title">
                    <span>Execute the query</span>
                  </template>
                  <a-button type="primary" :size="SearchGlobalOptions.size" @click="handleSubmit" icon="search">The query</a-button>
                </a-tooltip>

                <a-tooltip placement="bottom">
                  <template slot="title">
                    <span>Clears the values of all controls</span>
                  </template>
                  <a-button
                    :size="SearchGlobalOptions.size"
                    style="margin-left: 8px"
                    @click="resetSearchForm"
                    icon="border"
                  >reset</a-button>
                </a-tooltip>
              </slot>
              <template v-if="showCollapsedText">
                <a @click="togglecollapsed" style="margin-left: 8px">
                  <a-tooltip placement="bottom">
                    <template slot="title">
                      <span>{{ collapsed ? 'Click to collapse some controls' :' Click to expand all controls'}}</span>
                    </template>{{ collapsed ? 'Fold' : 'expand'}}<a-icon :type="collapsed ? 'up' : 'down'" />
                  </a-tooltip>
                </a>
              </template>
              <slot name="extra" />
            </a-row>
          </template>

          <a-row :gutter="gutter">
            <template v-for="(item, index) in renderDataSource">
              <template v-if="item.type && item.fieldName">
                <field-render
                  :SearchGlobalOptions="SearchGlobalOptions"
                  :itemOptions="item"
                  :key="item.fieldName"
                  v-show="index < SearchGlobalOptions.maxItem || (index >= SearchGlobalOptions.maxItem && collapsed)"
                />
              </template>
            </template>
          </a-row>
        </a-card>
      </template>
    </a-form>
  </div>
</template>

<script>
import FieldRender from './FieldRender';
export default {
  name: 'AdvancedSearch'.components: {
    FieldRender
  },
  computed: {
    showCollapsedText() {
      // Display the search and shrink criteria
      return this.renderDataSource.length > this.maxItem;
    },
    SearchGlobalOptions() {
      // Global configuration
      return {
        maxItem: this.maxItem,
        size: this.size,
        immediate: this.immediate,
        responsive: this.responsive
      };
    },
    renderDataSource() {
      // Reorganize incoming data, merge global configuration, subitem configuration takes precedence over global configuration
      return this.dataSource.map(item= >({... this.SearchGlobalOptions, ... item })); }, layoutMode() {// Display mode optimization
      if (this.layout) return this.layout;
      if (this.maxItem > this.dataSource.length) {
        return 'inline';
      } else {
        return 'card'; }}},props: {
    layout: {
      // Layout of the search area
      type: String.default: ' '
    },
    bordered: {
      // Whether to display a border
      type: Boolean.default: false
    },
    datetimeTotimeStamp: {
      // Whether to convert all return values of the time control to timestamps
      type: Boolean.default: false
    },
    maxItem: {
      // More than how many folds
      type: Number.default: 4
    },
    gutter: {
      // Spacing of controls
      type: Number.default: 48
    },
    size: {
      // Size of the control
      type: String.default: 'default'
    },
    responsive: {
      type: Object.default: function() {
        return {
          xxl: 6.xl: 8.md: 12.sm: 24}; }},title: {
      type: String.default: 'Search criteria area'
    },
    dataSource: {
      / / the data source
      type: Array.default: function() {
        return[{type: 'text'.// Control type
            labelText: 'Control name'.// Control displays the text
            fieldName: 'formField1'.placeholder: 'Text entry area' // Null value text for the default control
          },
          {
            labelText: 'Number entry field'.type: 'number'.fieldName: 'formField2'.placeholder: 'This is just a text entry box for numbers'
          },
          {
            labelText: 'Checkbox'.type: 'radio'.fieldName: 'formField3'.defaultValue: '0'.options: [{label: 'option 1'.value: '0'
              },
              {
                label: 'option 2'.value: '1'}]}, {labelText: 'Date selection'.type: 'datetime'.fieldName: 'formField4'.placeholder: 'Select date'
          },
          {
            labelText: 'Date range'.type: 'datetimeRange'.fieldName: 'formField5'.placeholder: ['Start date'.'Select date'] {},labelText: 'Drop-down box'.type: 'select'.fieldName: 'formField7'.placeholder: 'Drop down and choose what you want'.options: [{label: 'text1'.value: '0'
              },
              {
                label: 'text2'.value: '1'}]}, {labelText: 'linkage'.type: 'cascader'.fieldName: 'formField6'.placeholder: 'Cascade selection'.options: [{value: 'zhejiang'.label: 'Zhejiang'.children: [{value: 'hangzhou'.label: 'Hangzhou'.children: [{value: 'xihu'.label: 'West Lake'
                      },
                      {
                        value: 'xiasha'.label: 'Xia Sha'.disabled: true}}]}, {value: 'jiangsu'.label: 'Jiangsu'.children: [{value: 'nanjing'.label: 'Nanjing'.children: [{value: 'zhonghuamen'.label: 'Zhong Hua men'}]}]}]; } } }, data() {return {
      // Advanced search expanded/closed
      collapsed: false
    };
  },
  beforeCreate() {
    this.form = this.$form.createForm(this);
  },

  methods: {
    togglecollapsed() {
      this.collapsed = !this.collapsed;
    },
    handleParams(obj) {
      // The judgment must be obj
      if(! (Object.prototype.toString.call(obj) === '[object Object]')) {
        return {};
      }
      let tempObj = {};
      for (let [key, value] of Object.entries(obj)) {
        if (Array.isArray(value) && value.length <= 0) continue;
        if (Object.prototype.toString.call(value) === '[object Function]') continue;

        if (this.datetimeTotimeStamp) {
          // If true, the timestamp is converted
          if (Object.prototype.toString.call(value) === '[object Object]' && value._isAMomentObject) {
            / / determine my moment
            value = value.valueOf();
          }
          if (Array.isArray(value) && value[0]._isAMomentObject && value[1]._isAMomentObject) {
            / / determine my moment
            value = value.map(item= >item.valueOf()); }}// If it is a string, clear the Spaces on both sides
        if (value && typeof value === 'string') {
          value = value.trim();
        }
        tempObj[key] = value;
      }

      return tempObj;
    },
    handleSubmit(e) {
      // Triggers the form submission, i.e. the search button
      e.preventDefault();
      this.form.validateFields((err, values) = > {
        if(! err) {if (this.$listeners.callBackFormat && typeof this.$listeners.callBackFormat === 'function') {
            let formatData = this.$listeners.callBackFormat(values);
            this.$emit('change', formatData);
          } else {
            const queryParams = this.handleParams(values);
            this.$emit('change', queryParams); }}}); }, resetSearchForm() {// Reset the entire query form
      this.form.resetFields();
      this.$emit('change'.null); }}};</script>

<style lang="scss">.advance-search-wrapper { .ant-form-item { display: flex; margin-bottom: 12px ! important; margin-right: 0; .ant-form-item-control-wrapper { flex: 1; display: inline-block; vertical-align: middle; } > .ant-form-item-label { line-height: 32px; padding-right: 8px; width: auto; } .ant-form-item-control { height: 32px; line-height: 32px; display: flex; justify-content: flex-start; align-items: center; .ant-form-item-children { min-width: 160px; } } } .table-page-search-submitButtons { display: block; margin-bottom: 24px; white-space: nowrap; }}</style>


Copy the code

Fieldrender. vue(render corresponding controls)

<template>
  <a-col v-bind="fieldOptions.responsive" v-if="fieldOptions.fieldName && fieldOptions.type === 'text'">
    <a-form-item :label-col="labelCol" :wrapper-col="wrapperCol" :label="fieldOptions.labelText">
      <a-input
        :size="fieldOptions.size ? fieldOptions.size : 'default'"
        v-decorator="[ fieldOptions.fieldName, { initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : '' } ]"
        :placeholder="fieldOptions.placeholder"
      />
    </a-form-item>
  </a-col>
  <a-col v-bind="fieldOptions.responsive" v-else-if="fieldOptions.fieldName && fieldOptions.type === 'select'">
    <a-form-item :label-col="labelCol" :wrapper-col="wrapperCol" :label="fieldOptions.labelText">
      <a-select
        style="width: 100%"
        showSearch
        :filterOption="selectFilterOption"
        :size="fieldOptions.size ? fieldOptions.size : 'default'"
        allowClear
        v-decorator="[ fieldOptions.fieldName, { initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : undefined } ]"
        :placeholder="fieldOptions.placeholder"
      >
        <template v-for="(item, index) in fieldOptions.options">
          <a-select-option :value="item.value" :key="index">
            {{ item.label }}
          </a-select-option>
        </template>
      </a-select>
    </a-form-item>
  </a-col>
  <a-col v-else-if="fieldOptions.fieldName && fieldOptions.type === 'number'" v-bind="fieldOptions.responsive">
    <a-form-item :label-col="labelCol" :wrapper-col="wrapperCol" :label="fieldOptions.labelText">
      <a-input-number
        :size="fieldOptions.size ? fieldOptions.size : 'default'"
        :min="fieldOptions.min ? fieldOptions.min : 1"
        style="width: 100%"
        v-decorator="[ fieldOptions.fieldName, { initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : '' } ]"
        :placeholder="fieldOptions.placeholder"
      />
    </a-form-item>
  </a-col>
  <a-col
    v-bind="fieldOptions.responsive"
    v-else-if="fieldOptions.fieldName && fieldOptions.type === 'radio' && Array.isArray(fieldOptions.options)"
  >
    <a-form-item :label-col="labelCol" :wrapper-col="wrapperCol" :label="fieldOptions.labelText">
      <a-radio-group
        :size="fieldOptions.size ? fieldOptions.size : 'default'"
        buttonStyle="solid"
        v-decorator="[ fieldOptions.fieldName, { initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : '' } ]"
      >
        <template v-for="(item, index) in fieldOptions.options">
          <a-radio-button :key="index" :value="item.value">{{ item.label }} </a-radio-button>
        </template>
      </a-radio-group>
    </a-form-item>
  </a-col>
  <a-col v-bind="fieldOptions.responsive" v-else-if="fieldOptions.fieldName && fieldOptions.type === 'datetime'">
    <a-form-item :label-col="labelCol" :wrapper-col="wrapperCol" :label="fieldOptions.labelText">
      <a-date-picker
        :size="fieldOptions.size ? fieldOptions.size : 'default'"
        :placeholder="fieldOptions.placeholder"
        v-decorator="[ fieldOptions.fieldName, { initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : null } ]"
      />
    </a-form-item>
  </a-col>
  <a-col v-bind="fieldOptions.responsive" v-else-if="fieldOptions.fieldName && fieldOptions.type === 'datetimeRange'">
    <a-form-item :label-col="labelCol" :wrapper-col="wrapperCol" :label="fieldOptions.labelText">
      <a-range-picker
        :size="fieldOptions.size ? fieldOptions.size : 'default'"
        v-decorator="[ fieldOptions.fieldName, { initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : null } ]"
        :placeholder="fieldOptions.placeholder"
      />
    </a-form-item>
  </a-col>
  <a-col v-bind="fieldOptions.responsive" v-else-if="fieldOptions.fieldName && fieldOptions.type === 'cascader'">
    <a-form-item :label-col="labelCol" :wrapper-col="wrapperCol" :label="fieldOptions.labelText">
      <a-cascader
        :size="fieldOptions.size ? fieldOptions.size : 'default'"
        :options="fieldOptions.options"
        :showSearch="{ cascaderFilter }"
        v-decorator="[ fieldOptions.fieldName, { initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : [] } ]"
        :placeholder="fieldOptions.placeholder"
      />
    </a-form-item>
  </a-col>
  <a-col v-bind="fieldOptions.responsive" v-else-if="fieldOptions.fieldName && fieldOptions.type === 'slider'">
    <a-form-item :label-col="labelCol" :wrapper-col="wrapperCol" :label="fieldOptions.labelText">
      <a-slider
        :min="1"
        range
        :marks="fieldOptions.marks"
        :tipFormatter="e => e * (fieldOptions.baseMultiple ? fieldOptions.baseMultiple : 500)"
        v-decorator="[ fieldOptions.fieldName, { initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : [0, 0] } ]"
      />
    </a-form-item>
  </a-col>
</template>

<script>
export default {
  computed: {
    fieldOptions() {
      if (this.itemOptions.baseMultiple) {
        return {
          marks: {
            0: 0.1: this.itemOptions.baseMultiple,
            100: this.itemOptions.baseMultiple * 100
          },
          ...this.itemOptions
        };
      }
      return this.itemOptions; }},props: {
    itemOptions: {
      // The basic parameters of the control
      type: Object.default: function() {
        return {
          type: 'text'.// Control type
          defaultValue: ' './ / the default value
          label: 'Control name'.// Control displays the text
          value: ' '.// The value of the control
          responsive: {
            md: 8.sm: 24
          },
          size: ' '.// Control size
          placeholder: ' ' // Null value text for the default control
        };
      }
    }
  },
  data() {
    return {
      labelCol: { span: 6 },
      wrapperCol: { span: 18}}; },methods: {
    selectFilterOption(input, option) {
      // Dropdown box filter function
      return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0;
    },
    cascaderFilter(inputValue, path) {
      // Cascade filter function
      return path.some(option= > option.label.toLowerCase().indexOf(inputValue.toLowerCase()) > - 1); }}};</script>


Copy the code

conclusion

To this kind of a regular query component is achieved, what is wrong please leave a message, will be timely correction.

There are also some features that have not been extended, such as callbacks triggered by arbitrary controls and richer component support, such as export capabilities.

Specific business specific analysis, interested in their own development, thank you for reading.