vue 管理表单编辑状态,样板代码

vue managing form editing state, boilerplate code

我的应用程序中有相当数量的表单,用户可以在其中选择编辑、还原或保存对对象的更改,这些更改最终会保存到后端。

与此非常相似:(在另一个问题中找到的代码)
https://jsfiddle.net/k5j6zj9t/22/

var app = new Vue({
      el: '#app',
      data: {
        isEditing: false,
        user: {
          firstName: 'John',
          lastName: 'Smith',
        }
      },
      mounted() {
        this.cachedUser = Object.assign({}, this.user);
      },
      methods: {
        save() {
          this.cachedUser = Object.assign({}, this.user);
          this.isEditing = false;
        },
        cancel() {
          this.user = Object.assign({}, this.cachedUser);
          this.isEditing = false;
        }
      }
    })

由于v-model 绑定会立即更改底层对象,因此我必须首先创建对象的克隆。另外我需要保存一个数据成员是否对象处于编辑状态。
将此代码乘以更多的表单和字段,我最终会得到太多的数据成员和大量的样板代码。

在像 django 这样的服务器框架中,模型在 'temporary state' 中,直到它被保存,所以我可以这样编辑

user.first_name = 'aa' # temporary object in memory
user.save() # saved to the db

我的问题是,vue 是否有一个模型 component/pattern 可以更好地处理这个任务?
保持模型状态的东西 - 即 isEditing、自动克隆对象以进行表单编辑、还原更改等。
这样我就不用为那么多对象写这样的代码了?

使用Scoped Slots可能会满足您的要求。

我的解决方案:

  1. 用一个插槽创建一个组件

  2. 那么这个slot会绑定valuesclonedValues(如果closeMode为false,clondedValues = values)

  3. 最后,在父组件中,使用作用域插槽的属性生成模板,然后将其传递给插槽。

像下面的演示:

Vue.component('child', {
  template: `
  <div>
    <div>
      <slot v-bind:values="clonedValues"></slot>
    </div>
    <p>
      <button @click="saveAction(clonedValues)">Save</button>
      <button @click="resetAction()">Reset</button>
    </p>
  </div>`,
  props: {
    'cloneMode': {
      type: Boolean,
      default: true
    },
    'values': {
      type: Object,
      default: () => { return new Object() }
    }, 
    'saveAction': {
      type: Function,
      default: function (newValues) {
        this.$emit('save', newValues)
      }
    }, 
    'resetAction': {
      type: Function,
      default: function () {
        this.syncValues(this.values)
      }
    }
  },
  data() {
    return {
      clonedValues: {}
    }
  },
  created: function () {
    this.syncValues(this.values)
  },
  watch: {
    values: {
      handler: function (newVal) {
        this.syncValues(newVal)
      },
      deep: true
    },
    cloneMode: function () {
      this.syncValues(this.values)
    }
  },
  methods: {
    syncValues: function (newVal) {
      this.clonedValues = this.cloneMode ? Object.assign({}, newVal) : newVal // if you'd like to support nested object, you have to deep clone
    }
  }
})

Vue.config.productionTip = false

app = new Vue({
  el: "#app",
  data: {
    mode: true,
    labels: ['id', 'name'],
    childForm: {
      'id': 1,
      'name': 'test'
    }
  },
  methods: {
    saveForm: function (ev) {
      Object.keys(this.childForm).forEach((item) => {
        this.childForm[item] = ev[item]
      })
      // call backend to update the data
    },
    changeCurrentValue: function () {
      this.childForm.id += '#'
      this.childForm.name += '@'
    }
  }
})
<script src="https://unpkg.com/vue@2.5.16/dist/vue.js"></script>
<div id="app">
  <p><button @click="mode=!mode">Mode: {{mode}}</button></p>
  <p>Current: {{childForm}}  --<button @click="changeCurrentValue()">Change Current</button></p>
  <child :values="childForm" @save="saveForm($event)" :clone-mode="mode">
    <template slot-scope="slotProps">
      <p>ID: <input v-model="slotProps.values['id']"/></p>
      <p>Name: <input v-model="slotProps.values['name']"/></p>
    </template>
  </child>
</div>

请求编辑 OP:

  1. 将默认插槽更改为命名插槽=编辑,然后创建一个插槽=查看

  2. 添加数据属性=编辑,如果为true,显示'Edit'槽,如果为false,显示'View'插槽.

  3. 在父组件中,为slot=view.

  4. 设计模板

像下面的演示:

Vue.component('child', {
  template: `
  <div>
    <div v-show="editing">
      <slot name="edit" v-bind:values="clonedValues"></slot>
      <button @click="saveForm(clonedValues)">Save</button>
      <button @click="resetAction()">Reset</button>
    </div>
    <div v-show="!editing">
      <slot name="view"></slot>
      <button @click="editing = true">Edit</button>
    </div>
  </div>`,
  props: {
    'values': {
      type: Object,
      default: () => { return new Object() }
    }, 
    'saveAction': {
      type: Function,
      default: function (newValues) {
        this.$emit('save', newValues)
      }
    }, 
    'resetAction': {
      type: Function,
      default: function () {
        this.syncValues(this.values)
      }
    }
  },
  data() {
    return {
      editing: false,
      clonedValues: {}
    }
  },
  created: function () {
    this.syncValues(this.values)
  },
  watch: {
    editing: function (newVal) {
      if(newVal) this.syncValues(this.values)
    },
    values: {
      handler: function (newVal) {
        if(this.editing) this.syncValues(newVal) //comment out this if don't want to sync latest props=values
      },
      deep:true
    }
  },
  methods: {
    syncValues: function (newVal) {
      this.clonedValues = Object.assign({}, newVal) // if you'd like to support nested object, you have to deep clone
    },
    saveForm: function (values) {
      this.saveAction(values)
      this.editing = false
    }
  }
})

Vue.config.productionTip = false

app = new Vue({
  el: "#app",
  data: {
    childForm: {
      'id': 1,
      'name': 'test'
    }
  },
  methods: {
    saveForm: function (ev) {
      Object.keys(this.childForm).forEach((item) => {
        this.childForm[item] = ev[item]
      })
      // call backend to update the data
    },
    changeCurrentValue: function () {
      this.childForm.id += '#'
      this.childForm.name += '@'
    }
  }
})
<script src="https://unpkg.com/vue@2.5.16/dist/vue.js"></script>
<div id="app">
  <p>Current: {{childForm}}  --<button @click="changeCurrentValue()">Change Current</button></p>
  <child :values="childForm" @save="saveForm($event)">
    <template slot-scope="slotProps" slot="edit">
      <h3>---Edit---</h3>
      <p>ID: <input v-model="slotProps.values['id']"/></p>
      <p>Name: <input v-model="slotProps.values['name']"/></p>
    </template>
    <template slot="view">
      <h3>---View---</h3>
      <p>ID: <span>{{childForm['id']}}</span></p>
      <p>Name: <span>{{childForm['name']}}</span></p>
    </template>
  </child>
</div>