JavaScript

Реализация шаблона Builder во Vue.js, часть 2: Формы

Spread the love

Перевод: Markus Oberlehner — Implementing the Builder Pattern in Vue.js Part 2: Forms

В первой части статьи о реализации шаблона Builder во Vue.js мы рассмотрели, как можно использовать эту технику для быстрого создания множества разных вариантов одного и того же компонента.

Еще один пример когда очень часто приходится создавать похожие компоненты — это формы в приложении CRUD с множеством различных типов содержимого. В этой части мы рассмотрим, как можно использовать шаблон Builder, чтобы упростить создание множества различных компонентов формы для каждого типа содержимого типичного приложения CRUD.

Как и в примере описанным в первой части, это часть также во многом вдохновлена выступлением Якоба Шаца. В своем выступлении он также показывает возможное решение для реализации форм с помощью шаблона Builder. Я настоятельно рекомендую вам посмотреть его видео, если вы еще этого не сделали.

FormFactory

Чтобы оставаться верными промышленной схеме именования, мы начинаем с создания нового компонента FormFactory. Этот компонент отвечает за создание формы из массива объектов полей.

<template>
  <form
    class="form-factory"
    @submit.prevent="submit"
  >
    <div
      v-if="success"
      class="form-factory-success"
    >
      Success!
    </div>
    <template v-else>
      <FormGroup
        v-for="field in fieldsWithDefaults"
        :key="field.name"
      >
        <FormLabel :for="`${_uid}-${field.name}`">
          {{ field.label }}
          <template v-if="field.validation.required">*</template>
        </FormLabel>
        <Component
          v-model="data[field.name]"
          :is="field.component"
          v-bind="{
            ...field.options.props,
            ...field.options.attrs,
          }"
          :id="`${_uid}-${field.name}`"
          @input="$v.data[field.name].$touch()"
        />
        <FormInlineMessage
          v-if="$v.data[field.name].$error"
        >
          Please fill in this field correctly.
        </FormInlineMessage>
      </FormGroup>

      <button>Submit</button>
    </template>
  </form>
</template>

<script>
// src/components/FormFactory.vue
import { validationMixin } from 'vuelidate';

import FormGroup from './FormGroup.vue';
import FormInlineMessage from './FormInlineMessage.vue';
import FormLabel from './FormLabel.vue';

const defaultField = {
  component: null,
  label: '',
  name: '',
  options: {},
  validation: {},
};

export default {
  name: 'FormFactory',
  // We use the vuelidate validation
  // Mixin for basic form validation.
  mixins: [validationMixin],
  // Injecting dependencies makes it
  // possible or reuse this component
  // for all kinds of content types.
  inject: ['fetch', 'post'],
  components: {
    FormGroup,
    FormInlineMessage,
    FormLabel
  },
  props: {
    fields: {
      default: () => [],
      type: Array,
    },
    id: {
      default: null,
      type: [Number, String],
    },
  },
  data() {
    return {
      data: {},
      success: false,
    };
  },
  computed: {
    // Apply default field configuration
    // to make sure all properties we rely
    // on in the template do exist.
    fieldsWithDefaults() {
      return this.fields.map(x => ({ ...defaultField, ...x }));
    },
  },
  async created() {
    // If there is an ID we initially
    // load the data and switch into
    // edit mode.
    if (this.id) {
      this.data = await this.fetch(this.id);
    }
  },
  methods: {
    async submit() {
      this.$v.$touch();
      if (this.$v.$error) return;

      const { success } = await this.post(this.data);
      this.success = success;
    },
  },
  // The vuelidate validation configuration is
  // automatically generated for us.
  validations() {
    const data = this.fieldsWithDefaults
      .filter(x => x.validation)
      .reduce((prev, field) => ({
        ...prev,
        [field.name]: field.validation,
      }), {});
    return { data };
  },
};
</script>

<style>
.form-factory > :not(:first-child) {
  margin-top: 1em;
}

.form-factory-success {
  color: green;
}
</style>

В приведенном выше фрагменте кода вы можете видеть, что этот компонент инкапсулирует множество разных параметров. Эго реализация может показаться не идеальна, но в будущем она значительно упростит создание новых компонентов формы, которые будут полностью функциональными с самого начала, без необходимости беспокоиться о макете и логике проверки или отправки формы.

Если вы хотите поближе взглянуть на код примера, который вы видите выше, вы можете увидеть полную демонстрацию в этом CodeSandbox.

Использование FormFactory

В следующем фрагменте кода вы можете увидеть, как мы можем использовать нашу только что созданную FormFactory для создания нового компонента UserForm, который наши пользователи могут использовать для изменения своих настроек.

<template>
  <UserProvider>
    <FormFactory :fields="fields" :id="id"/>
  </UserProvider>
</template>

<script>
// src/components/UserForm.vue
import { required } from 'vuelidate/lib/validators';

import FormFactory from './FormFactory.vue';
import FormInput from './FormInput.vue';
import FormTextarea from './FormTextarea.vue';
import UserProvider from './UserProvider.vue';

export default {
  name: 'UserForm',
  components: {
    FormFactory,
    FormInput,
    FormTextarea,
    UserProvider,
  },
  props: {
    // Passing an ID as a property makes
    // the form load an existing user and
    // switches the form into editing mode.
    id: {
      default: null,
      type: [Number, String],
    },
  },
  created() {
    this.fields = [
      {
        component: FormInput,
        label: 'Name',
        name: 'name',
        options: {
          attrs: {
            placeholder: 'Your name',
          },
        },
        validation: {
          required,
        },
      },
      {
        component: FormTextarea,
        label: 'Description',
        name: 'description',
        options: {
          attrs: {
            placeholder: 'About you',
          },
        },
      },
    ];
  },
};
</script>

Хотя это уже кажется довольно простым, мы можем упростить инициализацию новых форм с помощью шаблона Builder.

FormBuilder

В дополнение к тому, что вам не нужно создавать новый компонент или повторять один и тот же подробный код шаблона для каждой новой формы, шаблон Builder также позволяет очень простое динамическое создание новых компонентов, например на основе пользовательского ввода. Давайте посмотрим на возможную реализацию этого шаблона.

// src/builders/FormBuilder.js
import FormFactory from '../components/FormFactory.vue';

export default class FormBuilder {
  constructor() {
    this.props = {
      fields: []
    };
  }

  withProvider(provider) {
    this.provider = provider;
    return this;
  }

  addField(field) {
    this.props.fields.push(field);
    return this;
  }

  build() {
    const Provider = this.provider;
    const props = this.props;

    return {
      props: {
        id: {
          default: null,
          type: [Number, String],
        },
      },
      render(h) {
        return h(Provider, [
          h(FormFactory, { props: { id: this.id, ...props } }),
        ]);
      },
    };
  }
}

В следующем блоке кода вы можете увидеть, как мы можем использовать FormBuilder внутри нашего корневого компонента App.vue для создания новой UserForm  «на лету».

<template>
  <div id="app">
    <h2>Create User Form</h2>
    <UserForm/>

    <h2>Edit User Form</h2>
    <UserForm :id="1"/>
  </div>
</template>

<script>
// src/App.vue
import { required } from 'vuelidate/lib/validators';

import FormBuilder from './builders/FormBuilder';

import UserProvider from './components/UserProvider.vue';
import FormInput from './components/FormInput.vue';
import FormTextarea from './components/FormTextarea.vue';

export default {
  name: 'App',
  components: {
    UserForm: new FormBuilder()
      .withProvider(UserProvider)
      .addField({
        component: FormInput,
        label: 'Name',
        name: 'name',
        options: {
          attrs: {
            placeholder: 'Your name',
          },
        },
        validation: {
          required,
        },
      })
      .addField({
        component: FormTextarea,
        label: 'Description',
        name: 'description',
        options: {
          attrs: {
            placeholder: 'About you',
          },
        },
      })
      .build(),
  },
};
</script>

FormDirector

Хотя инициализация новых компонентов формы очень проста с классом FormBuilder, она также может стать очень утомительной, если мы хотим повторно использовать определенный компонент формы в нескольких местах. Что бы упростить этот момент используем шаблон Director.

import { required } from 'vuelidate/lib/validators';

import UserProvider from '../components/UserProvider.vue';

import FormInput from '../components/FormInput.vue';
import FormTextarea from '../components/FormTextarea.vue';

export default class FormDirector {
  constructor(builder) {
    this.builder = builder;
  }

  makeUserForm() {
    return this.builder
      .withProvider(UserProvider)
      .addField({
        component: FormInput,
        label: 'Name',
        name: 'name',
        options: {
          attrs: {
            placeholder: 'Your name',
          },
        },
        validation: {
          required,
        },
      })
      .addField({
        component: FormTextarea,
        label: 'Description',
        name: 'description',
        options: {
          attrs: {
            placeholder: 'About you',
          },
        },
      })
      .build();
  }
}

В следующем примере вы можете увидеть, как мы можем использовать класс FormDirector сверху, чтобы быстро получить определенный компонент формы.

<template>
  <div id="app">
    <h2>Create User Form</h2>
    <UserForm/>

    <h2>Edit User Form</h2>
    <UserForm :id="1"/>
  </div>
</template>

<script>
// src/App.vue
import FormBuilder from "./builders/FormBuilder";
import FormDirector from "./builders/FormDirector";

export default {
  name: 'App',
  components: {
    UserForm: new FormDirector(
      new FormBuilder(),
    ).makeUserForm(),
  },
};
</script>

Благодаря шаблону Director нам не нужно повторяться, чтобы создать один и тот же компонент формы в нескольких местах нашего приложения.

https://codesandbox.io/embed/implementing-the-builder-pattern-in-vuejs-forms-25vpm?fontsize=14&module=%2Fsrc%2FApp.vue&view=editor

Заключение

Хотя примеры, приведенные в этой статье, очень хорошо демонстрируют преимущества, не все идеально при таком подходе. Шаблон Builder обычно работает очень хорошо, если у нас много очень похожих компонентов. Как только вам понадобится форма с немного другим макетом или поведением, использование шаблона будет сложным. Но с другой стороны, поскольку мы по-прежнему используем обычные компоненты в качестве базовых строительных блоков для наших форм, мы можем решить не использовать шаблон Builder в таких случаях, а построить обычный компонент из этих компонентов формы.

Но в целом, я определенно вижу, что для этого паттерна есть своя ниша. Хотя он и не панацея для каждой проблемы, но в некоторых случаях может быть очень элегантным решением.

Была ли вам полезна эта статья?
[2 / 5]

Spread the love
Editorial Team

Recent Posts

Vue 3.4 Новая механика v-model компонента

Краткий перевод: https://vuejs.org/guide/components/v-model.html Основное использование​ v-model используется для реализации двусторонней привязки в компоненте. Начиная с Vue…

11 месяцев ago

Анонс Vue 3.4

Сегодня мы рады объявить о выпуске Vue 3.4 «🏀 Slam Dunk»! Этот выпуск включает в…

11 месяцев ago

Как принудительно пере-отобразить (re-render) компонент Vue

Vue.js — это универсальный и адаптируемый фреймворк. Благодаря своей отличительной архитектуре и системе реактивности Vue…

2 года ago

Проблемы с установкой сертификата на nginix

Недавно, у меня истек сертификат и пришлось заказывать новый и затем устанавливать на хостинг с…

2 года ago

Введение в JavaScript Temporal API

Каким бы ни было ваше мнение о JavaScript, но всем известно, что работа с датами…

2 года ago

Когда и как выбирать между медиа запросами и контейнерными запросами

Все, кто следит за последними событиями в мире адаптивного дизайна, согласятся, что введение контейнерных запросов…

2 года ago