七、创建模式驱动的表单

形式有不同的形状、大小和复杂程度。快速构建带有几个字段的登录表单或联系人表单相对简单,但当您必须将其提升到下一个级别并创建由 API 或模式驱动的完全动态表单时会发生什么?

到目前为止,我们使用的是一个相对简单的表单,它只要求用户提供一些基本数据,但所有内容都是作为静态表单硬编码的。如果我们的模拟网站想要从表单中添加或删除一些字段,我们必须手动进行更改,将它们部署到服务器,甚至可能调整后端以处理不同的字段。但是如果我们想让整个过程自动化呢?

在本章中,我们将构建一个完全由 API 端点支持的示例动态表单。模式驱动的表单非常强大,因为它们可以由应用程序的 API 直接控制和修改。这意味着,当后端发生变化时,表单不仅会在前端自动进行调整,而且会自动了解如何将动态数据发送回 API。

本章将涵盖以下主题:

  • 探索初学者工具包
  • 准备模式
  • 加载模式并创建Renderer组件
  • 动态绑定用户数据
  • 创建模拟 API
  • 将新 API 加载到应用程序中
  • 将 API 转换为工作模式

技术要求

我假设您已经阅读或理解了前面章节中查看的概念,例如使用 Axios 进行 HTTP 调用和组件创建,并且已经作为模拟 API 提供程序安装在您的系统上。您可以参考此链接了解更多信息:https://mockoon.com/

为了加快应用程序的搭建,我为我们设置了一个 starter Vue CLI-3 存储库,其中包含两个自定义组件和一个示例静态表单。您可以从以下链接克隆或下载它:

https://github.com/PacktPublishing/Building-Forms-with-Vue.js/tree/master/Chapter07

请查看以下视频以查看代码的运行情况:

T0http://bit.ly/2VMe3eU T1

探索初学者工具包

克隆或下载 starter 存储库后,您将发现自己有一个 Vue CLI 3 项目。首先要做的是看看我们将要做什么!存储库包含一个非常简单的表单,其中包含一些输入字段和一个选择框。您可以在App.vue中找到表单的结构。如您所见,我们使用两种不同的定制组件,BaseInputBaseSelect。这两个都可以在src/components文件夹中找到。它们都分别包装了一个inputselect标签,并公开了一些属性,我们可以使用这些属性向它们中的每一个注入必要的数据,例如labelsoptions

我已经冒昧地将 Axios 添加到项目依赖项中;你可以查看package.json来证实。Bootstrap 的一些基类的 CSS 文件已经导入到main.js中。

现在我们已经对项目结构有了一个很好的概述,让我们继续安装依赖项并在浏览器上运行它们。遵循以下步骤:

  1. 进入终端并运行以下命令:
    > npm install
    > npm run serve
  1. 完成此操作后,请在浏览器上签出表单并使用字段进行操作。除了在App.vue中将字段v-model绑定到本地状态之外,没有什么奇怪的事情发生。

如果您想了解如何向服务器发送表单数据的更新,提交按钮只会在控制台上记录一条消息,请查看本书的第 2 章最简单的表单

现在您已经了解了应用程序的起点,我们将在下一节准备演示模式。

准备模式

目前,我们的表单(如前所述)是硬编码的。开始使其成为动态表单所需的第一步是,每次需要添加新字段时,都不需要将BaseInputBaseSelect直接添加到App.vue文件中。这意味着我们将需要某种有组织的结构,或模式,来表示我们试图为我们的形式完成什么。因为我们使用的是 JavaScript,所以最合乎逻辑的方法是使用 JSON 对象格式。这将使以后我们更容易进一步,让我们的模拟 API 直接将信息提供给表单。

现在,我们将使用静态模式。让我们在src中创建一个数据文件夹,并在其中创建一个新的schema.json文件。我们将用一些虚拟数据填充 JSON 文件。为了举个例子,我选择将 top 元素作为一个对象,其中的每个属性将表示表单中的一个字段。每个元素将至少包含一个component属性和一个label属性。然而,在 T4 的情况下,我们也将填充菜单。

要创建演示模式,请将以下数据添加到schema.json

      {   "firstName": {   
          "component": "BaseInput",
             "label": "First name"
          },
          "lastName": {
              "component": "BaseInput",
              "label": "Last name"
          },
          "favoriteAnimal": {
              "component": "BaseSelect",
              "label": "What's your favorite animal?",
              "options": [
                  { "label": "Cat", "value": "cat" },
                  { "label": "Dog", "value": "dog" },
                  { "label": "Sea Otter", "value": "onlyvalidanswer" }
              ]
          }
      }

现在我们有了一个结构化模式作为我们希望动态表单理解的内容的演示,我们可以继续下一节,在Renderer组件的帮助下,我们将把这个模式加载到我们的应用程序中。

加载模式并创建渲染器组件

现在我们已经设置了一个基本的模式来使用,让我们继续加载到应用程序中,以便使用它。在本章的后面,我们将创建一个虚拟 API,它将以稍微不同的方式向我们提供数据,我们将对其进行转换,以满足我们应用程序的要求。

现在,让我们转到App.vue并导入 JSON。我们将首先在其他导入语句附近的顶部添加以下import语句:

import schema from '@/data/schema.json';

现在我们的应用程序可以使用我们的数据,我们需要一些组件来将这些信息解析为BaseInputBaseSelect组件。让我们继续在components文件夹中创建一个新文件,并将其命名为Renderer.vue。这个组件只有一个目的:理解我们的模式并在屏幕上呈现正确的组件。它目前只有一个属性element,它代表我们模式中的每个元素。为此,请在Renderer.vue中添加以下代码:

      <template>
        <component 
          :is="component" 
          v-bind="props"
        />
      </template>
      <script>
      export default {
        props: {
          element: {
            type: Object,
            required: true
          }
        },
        computed: {
          component() {
            const componentName = this.element.component;
            return () => import(`./${componentName}`);
          },
          props() {
            return this.element;
          }
        }
      }
      </script>

在这个组件中有两件重要的事情需要注意。详情如下:

  • element道具是一个物体,需要使用。如果没有它,这个组件将根本无法工作。我们有两个计算性质。第一个组件负责动态加载我们需要的任何元素。首先,我们创建一个componentName常量,并将其分配给element.component的值,这是我们组件的字符串名称存储在模式中的位置。

  • 值得一提的是,我们不只是为了清楚起见才添加这个const。计算属性在缓存方面的工作方式要求在这里存在const,因为我们返回的是一个函数,不会检查其依赖性。

  • :是属性的<component>标记调用此计算属性时,它将加载组件并传递它。请注意,这仅在组件已全局注册的情况下有效;在任何其他情况下,都需要一个需要正确组件的计算属性。有关动态组件的更多信息,请查阅官方文档:https://vuejs.org/v2/guide/components-dynamic-async.html

第二个计算属性props将简单地将整个元素及其属性作为props传递给我们使用v-on绑定加载的任何组件。例如,在BaseSelect组件上,它将把模式中的options属性传递给该组件,以便它能够呈现正确的选项。如果您想知道为什么我们使用 computed 属性而不是直接将元素传递给[T5]指令,那么您的思路是正确的。现在,它肯定是不需要的,但是以这种方式设置它可以让我们以后添加另一个特定组件可能需要的逻辑或解析级别。

让我们回到App.vue

我们需要导入我们的Renderer组件并将其添加到template中。我们还需要清理一下;我们不再需要手动导入BaseInputBaseSelect,我们的表单本地状态很快将是动态的,因此不需要静态声明,如下面的代码段所示:

      <template>
        <div id="app" class="container py-4">
          <div class="row">
            <div class="col-12">
              <form>
                <Renderer 
                 v-for="(element, name) in schema" 
                  :key="name" 
                  :element="element" 
                />
               <div class="form-group">
                  <button 
                    @click.prevent="onSubmit" 
                    type="submit" 
                   class="btn btn-primary"
                  >Submit</button>
                </div>
              </form>
           </div>
          </div>
        </div>
      </template>

      <script>
      import schema from '@/data/schema.json';
      import Renderer from '@/components/Renderer';
      export default {
        name: 'app',
        components: { Renderer },
        data() {
          return {
            schema: schema,
            form: {}
          }
        },
        methods: {
          onSubmit() {
            console.log('Submit clicked');
          }
        }
      }
      </script>

继续并在浏览器中运行它,您应该会看到模式在schema.json中声明的三个输入,<select>应该包含我们的三个选项。此时您将遇到几个控制台错误,因为我们还没有处理组件的双向值绑定,它们是根据需要设置的。不过别担心,我们很快就会回来的!

动态绑定用户的数据

如果我们不能使用用户输入的数据,任何形式都有什么好处?尽管我们可以完全基于模式动态生成这些表单很酷,但我们仍然需要能够以某种方式存储这些值,以便以后可以根据需要处理它们。表单能够创建双向绑定的第一步是告诉Renderer.vue如何处理来自动态组件的输入事件。

遵循以下步骤:

  1. 让我们进入Renderer.vue并在<component>中添加:value绑定和@input侦听器:
      <component 
        :is="component" 
        v-bind="props"
        :value="value"
        @input="handleComponentInput"
      />

记住,为了v-model或双向绑定到自定义组件中,我们通常需要传入一个值并侦听输入事件。在本例中,我们将显式地侦听输入事件,因为我们的自定义组件都为双向绑定触发这种类型的事件

  1. 继续,将新的value道具添加到我们的Renderer组件中:
      props: {
        element: {
          type: Object,
          required: true
      },
        value: {
          required: true
        }
      }

最后,我们需要实现handleComponentInput方法。请记住,我选择将其作为一种方法,而不是将[T1]直接发射到[T2]中,原因有二。第一个是我发现以后编写单元测试更容易是一个很好的实践,第二个是,如果我们需要为具有特定需求的特定组件编写一个if语句或条件语句,它允许更大的灵活性。

  1. 新增handleComponentInput方法:
      methods: {
        handleComponentInput (value) {
          this.$emit('input', value);
        }
      }

现在渲染器已经准备好与v-model进行双向绑定,让我们回到App.vue,在这里我们实现它并添加实际绑定。我们将把v-model属性添加到<Renderer>,这里的技巧是将其绑定到form[name]。请记住,我们的模式有一个结构,其中属性的名称是该字段的唯一标识符。打开schema.json查看。

例如,在第一个字段中,firstName是保存模式对象中第一个空格的属性的名称。这个属性是我们要用来绑定的,这样我们以后就可以知道它在数据中代表哪个字段。

  1. 让我们将我们的v-model添加到App.vue中的<Renderer>
      <Renderer 
        v-for="(element, name) in schema" 
        :key="name" 
        :element="element" 
        v-model="form[name]"
      />

打开浏览器并签出表单;如果您开始填写字段并查看您的Vue开发工具,您将看到绑定都正常工作。渲染器通过动态v-model所做的是将每个属性绑定到本地数据形式的属性。

  1. 如果您希望以一种更快捷的方式查看此操作,而不必使用dev工具,请在App.vue中的<Renderer>组件中添加以下代码:
      <pre>{{ form }}</pre>

我们只是将表单转储到屏幕中,并使用 HTML<pre>标记来获得一些简单的格式。尝试进入schema.json并添加一些新字段。您将立即在浏览器上看到结果,因为渲染器将接收模式的更改,页面将自行重新加载(热重新加载)。新模式就绪后,您将看到所有新字段都就绪。

我们很快就找到了位置!在下一节中,我们将再次准备演示 API。在实际的应用程序中,您不会从文件提供模式服务,而可能是从服务器提供模式服务。启动 Mockoon,我们开始吧!

创建模拟 API

演示的下一步是创建一个实际的模拟 API,然后将其转换为渲染器能够理解的结构。我们为什么要这样处理这个问题?在实际工作场景中,后端与前端的需求不完全匹配的情况并不少见。也许 API 是首先构建的,或者是在考虑到前端的早期版本的情况下以一种完全不同的方式工作的;有很多可能性,在这种情况下,我们将调整到一个不兼容的 API,以了解如何防范这种情况。

这种方法还可以确保我们有一个中间人来翻译并理解我们应用程序的 API。如果 API 因任何原因发生变化,我们可以安全地改变这个中间人,以适应变化,在大多数情况下,甚至不必接触应用程序的任何内部组件。

遵循以下步骤:

  1. 启动 Mockoon,我们在本书中一直在使用它来进行虚拟 API 调用。如果您尚未安装,可以在此处找到下载链接:https://mockoon.com/
  2. 通过单击绿色播放按钮,并单击第二列顶部的添加路由按钮,确保环境正在运行。在右侧屏幕上,我们将添加一些数据。让我们从路径开始。
  3. 在 Route settings(路由设置)下,将 GET 作为路由的默认动词,然后继续并将路径命名为/schema
  4. 继续,导航到第二个选项卡 Headers,并将单个标题设置为 Content Type:[T0]。从好的方面来说,这将在下一部分为我们提供一些不错的颜色编码。
  5. 返回到第一个选项卡,响应和正文。
  6. 在主体部分内,继续并复制以下结构。请注意,这不是我们之前在schema.json文件中的内容,而是我们稍后将要解释的类似结构。我们甚至会忽略其中的一些数据,因为它对我们当前的表单没有用处。API 有时会返回我们实际不使用的数据,这并不少见:
{
  "fieldCount": 4,
    "fields": [
        { 
            "type": "input",
            "id": "firstName",
            "label": "First Name"
        },
        {
            "type": "input",
            "id": "lastName",
            "label": "Last Name"
        },
        {
            "type": "input",
            "id": "email",
            "label": "Email"
        },
        {
            "type": "singleChoice",
            "label": "What's your favorite animal?",
            "opts": [
                { "label": "Cat", "value": "cat" },
                { "label": "Dog", "value": "dog" },
                { "label": "Sea Otter", "value": "onlyvalidanswer" 
                }
            ]
        }
    ]
}

请仔细查看本例中 API 返回的 JSON 的结构。您将开始看到后端如何试图描述它需要什么,以及前端渲染器希望得到什么,这两方面有一些相似之处。

如果你在日常生活中面临这种选择,你会意识到有两种方法可以做到:

  • 我们可以在组件级别更改前端实现,以适应新的 API,在某些情况下,这可能是我们想要的。
  • 或者,我们可以制作一个小型库或文件,用于解释后端的 API。我们之所以选择这一点,是因为我前面已经描述了一些原因。

现在我们有了虚拟 API,我们可以教应用程序如何这种新的 API 格式转换为它能够理解的格式。过程的这一部分非常重要,因为您不希望每次后端发生更改时都必须修改整个应用程序。

将新 API 加载到应用程序中

现在,如果您进入App.vue,您会注意到我们正在通过import语句加载静态模式,如下所示:

import schema from '@/data/schema.json';.

这在以前对我们非常有效,因为它是一个静态文件,但这次,我们需要调用 API 端点来获取表单的模式。让我们从删除 import 语句开始;我们不再需要它,可以安全地删除它。您也可以进入data()属性并将schema设置为默认值,默认值为空对象:

schema: {},

我认为加载表单模式的一个好地方是为[T0]文件创建的钩子。我们希望尽快完成这项工作,并且在加载 DOM 时实际上不需要操作任何 DOM,只需设置调用模式内部属性的结果。

遵循以下步骤:

  1. 让我们将 Axios 导入到App.vue文件的顶部,靠近Renderer导入,因为我们将很快使用它:
      import axios from 'axios';
  1. 继续向我们的App.vue组件添加一个新创建的钩子;在其中,我们将对我们的模拟 API 端点进行一个简单的 Axios 调用。记住检查 Mockoon 以查看它是否正在运行:
      created() {
        axios.get('http://localhost:3000/schema')
        .then(response => {
          this.schema = response.data;
        })
        .catch(error => {
          console.log('Network error', error);
        })
      }

我们正在使用 AxiosGET方法调用我们的新端点http://localhost:3000/schema。确保您检查 Mockoon 是否正在使用端口3000作为您的模拟 API,或者根据需要随时调整 URL。Axios 调用返回承诺如果失败,我们将记录错误。但是,如果调用成功,我们希望确保捕获整个响应,并将此响应的data属性传递给模式的内部数据。请务必记住,在这种特殊情况下,API 给我们的响应应该是直接 JSON 模式对象。如果 API 返回不同的结构,例如嵌套对象或数组,则必须进行相应的调整。

打开浏览器并根据需要重新加载页面。看起来,正如预期的那样,我们成功地完全破坏了应用程序。当我们将新的 API 响应分配给模式的data属性时,应用程序试图将每个项加载到呈现程序中,但它不准备理解后端为我们提供的新模式格式。

在下一节中,我们将了解如何构建一个非常精简的实用程序库,使我们能够将这个新结构解析为渲染器可以理解的内容。

将 API 转换为工作模式

现在我们已经运行了我们的模拟 API,下一步是为我们的应用程序创建一种方法,以便将这个 API 结构解析或转换为我们以前拥有的模式结构,并且它能够理解。如果您对此非常好奇,试图在此时运行该应用程序,您将遇到大量控制台错误,它们会对您大喊大叫:prop:type check 失败和[T0]绑定失败。在这一点上,这是意料之中的。

继续并在src内创建一个新文件夹;我们称之为libraries。这不是一个严格的命名约定,所以请随意命名对您或您的团队更有意义的名称。在这个文件夹中,我们将创建一个名为Api.js的新文件。这个文件的目标是将处理 API 模式解析的所有代码放在应用程序的模式中。通过这种方式,我们可以将我们需要的任何东西导入到我们的组件中,关于 API 到模式的转换,我们有一个单一的真实来源,如果这些目的中的任何一个出于任何原因发生了变化,我们只需在这里更新它。

遵循以下步骤:

  1. 让我们从添加入口点开始;它将是一个名为parse的函数,并将接受一个参数:来自 APIschema端点的响应:
      export const parse = schema => {
       return schema;
      }

现在,我们将按原样返回[T0],这样我们就可以从小步开始了。

  1. 继续并将此功能导入顶部的App.vue
      import { parse } from '@/libraries/Api';
  1. 然后,在创建的钩子中,更新then块以使用该函数,然后再将其分配给状态:
      .then(response => {
       this.schema = parse(response.data);
      })

现在我们可以回到Api.js,我们将对这个解析器进行一个基本的实现。实际上,这里代码的复杂性取决于应用程序的需求和 API 结构之间的差异有多大。谢天谢地,它只包含几行代码。

将以下代码添加到Api.js

      export const parse = schema => {
        const fields = schema['fields'];
        const parsedSchema = {};
        for (let i = 0; i < fields.length; i++) {
          const field = fields[i];
          parsedSchema[field.id] = {
            component: componentForField(field.type),
            label: field.label,
            options: field.opts || null
          }
        }
        return parsedSchema;
      }
      function componentForField(field) {
        switch(field) {
          case 'singleChoice': return 'BaseSelect';

          default: return 'BaseInput';
        }
      }

让我们把这里发生的事情分解成小块:

  1. 首先,我们创建一个常量字段,它将从我们的 API 数据中提取fields属性,因为它嵌套在那里,我们并不真正关心它发送的其他数据。
  2. 我们创建了一个新对象parsedSchema,在其中我们将为表单的每个字段添加一个属性。
  3. 我们循环遍历fields中的每个项,并为其创建一个属性。在我们的 API 模式中,id属性拥有字段的唯一名称,因此我们将使用该名称作为属性名称parsedSchema[field.id]
  4. 我们将一个内部对象分配给 component 属性,这是我们新的componentForField函数的结果,在该函数中,我们计算出每种情况下必须使用的组件。
  5. 对于options属性,我们检查opts属性是否存在于 API 模式中,或者将其设置为 null。重要的是要记住,例如,即使BaseInput组件不需要此属性,它也不会关心它是否存在并设置为 null 或 undefined。
  6. 最后,我们返回模式的解析版本,我们的应用程序可以使用它将表单呈现为工作状态。

继续在浏览器中运行它,看看您的模式驱动、API 驱动的动态表单!

总结

花点时间给自己一个大大的拍拍。你不仅做到了这一章的结尾,而且做到了这本书的结尾!在本章中,您已经掌握了理解模式驱动表单用例的知识和技能,以及创建Renderer组件以适应这些用例的能力。您知道如何创建一个库来将后端的输出解析为您自己的表单,以及如何在需要时将表单数据反馈给 API。

现在,出去做些有活力的运动,吃很多鳄梨!