K
K
karpo5182019-10-13 01:30:16
Vue.js
karpo518, 2019-10-13 01:30:16

How to properly organize the code of a large calculator form in vue.js?

I'm creating my first form in vue.js and apparently I'm doing something wrong. The shape is big enough. When the number of lines of code exceeded 200, I decided that I needed to break the form into components block by block. As a result, it was necessary to take into account many nuances, and as a result, the code did not become simpler, but more complicated.
Apparently I'm doing something wrong, but there is no one to consult with. I hid my sheet of text and code so as not to unnerve those passing by. If someone has the time and desire to get acquainted and help with advice, I will be grateful.

Details
Чтобы в data() была понятная структура полей, я выделил каждый блок в отдельный объект. Итак, в каждый компонент я пробрасываю этот объект в виде prop и с его помощью инициализирую поля формы. Сразу возникают 2 проблемы:
1. Оказывается, редактировать props не рекомендуется. Но ведь я только за этим и передал props, чтобы пользователь своим вводом их редактировал.
2. Vuelidate быстро настроивается, если есть v-model, а без неё потребуются сеттеры.
Решено. Нужно инициализировать data из props. Как это сделать? Одним свойством-объектом? Тогда потребуется в полях формы прописывать v-model как свойство объекта. Но я же хотел лаконичный код. Тогда придется в data заводить несколько свойств и присваивать каждому свойство из props. Наверно можно повесить на хук created инициализацию data (вызвать в цикле по пропсу set()). Но тогда я не увижу в компоненте структуру data(). Это плохо.
Допустим, я проинициализировал data статично свойствами из пропса. Теперь могу привязать data к полям формы и изменять. Но! Новые значения нужно пробрасывать обратно к родителю. Там несколько свойств и на каждое придется повесить emit.
Но это ещё не всё. В каждом дочернем блоке будет валидация полей, результат которой тоже нужно будет передать в родителя. Тут вообще непонятно как поступить. Наверно нужно через emit отправлять объект $v или его свойства. В родительском компоненте из этих объектов будет собираться общий объект $v с доступом ко всем полям валидации. Не уверен, что такое вообзе возможно.
В итоге никакого упрощения структуры кода не получается. Валидацию вообще непонятно как организовать с разделением по компонентам. Также непонятно с ценами. Они обновляются только в родительском компоненте (забираются ajax запросом с сервера). Но при моей организации кода они не только не будут актуализироваться в дочерних компонентах, но и перезатрутся при первом обновлении полей формы. В итоге их придется вынести в отдельный объект, который будет дублировать структуру блоков.

Main form component: App.vue
<template>
  <div id="app">
    <h1>Калькулятор клининговых услуг</h1>

    <form class="calculatorForm">
        

        <Cleaning  v-bind:item="cleaning" @input="updateCleaning(item)" ></Cleaning>

        <button @click.prevent="getPriceInfo" >Рассчитать</button>
    </form>


  </div>
</template>

<script>

import Vue from 'vue'

import axios from 'axios'

import Vuelidate from 'vuelidate'
Vue.use(Vuelidate)

import { required, minLength, between, numeric } from 'vuelidate/lib/validators'
import { helpers } from 'vuelidate/lib/validators'

const mustBeCool = (value) => !helpers.req(value) || value.indexOf('cool') >= 0



import Carpet from './components/Carpet.vue'
import Cleaning from './components/Cleaning.vue'

axios.defaults.baseURL = 'https://site.ru/calculator/php'

export default {
    name: 'app',
    data () {
      return {

        services: ["cleaning","windows", "carpets","furniture"],
        cleaning: {type: 2, area: null, showAbout: false, price: null, isActive: false},
        windows: {isClassic:true, oneFoldCount: 0, twoFoldCount: 0, threeFoldCount: 0, area: null, isActive: false}, 
        carpets: {items:[{type: 1, area: null}], isActive: false},

        endpoint: '/index.php'
      }
    },

    validations: {
        cleaning: {    
            area:{
                required
            }
        },

        windows: {
            oneFoldCount:{ 
                numeric 
            },
            twoFoldCount:{ 
                numeric 
            },
            threeFoldCount:{ 
                numeric }
        }
    },

    components: {Cleaning,Carpet},

    methods: {

        getPriceInfo: function(event) {
            
            // Здесь будет код отправки данных для расчёта на сервер и получения стоимости услуг
        },

        updateCleaning: function(item) {
            
            this.cleaning = item;
        },

        updateCarpet: function(item, index) {
            
            this.carpets.items[index] = item;
        },

        addCarpet: function() {
            
            this.carpets.items.push({type: 1, area: null});
        }
    }
}
</script>


Component of one of the form blocks: Cleaning.vue

<template>

        <div class="serviceBlock" v-bind:class="{ active: isActive }" >
            <div class="serviceSwitcher">
                <label class="serviceName" for="serviceCleaning">Уборка
                    <input type="checkbox" id="serviceCleaning" value="cleaning" @change="isActive = !isActive" ><span class="checkmark"></span>
                </label>
            </div>
            <div class="serviceContent" v-if="isActive">
                <div class="serviceFields" >
                    
                    <div class="fieldItem">
                        <label for="cleaningType">Тип уборки</label>
                        <select id="cleaningType" v-model="type">
                          <option disabled value="">Выберите один из вариантов</option>
                          <option value="1" >Быстрая</option>
                          <option value="2" >Генеральная</option>
                          <option value="3">После ремонта</option>
                        </select>
                    </div>

                    <div class="fieldItem">
                        <label for="cleaningArea">Площадь помещения (кв. м.)</label>
                        <input type="number" id="cleaningArea" min="1" v-model="area" v-on:input="filterArea">
                    </div>

                </div>

                <div class="servicePrice">
                        <div class="title">Стоимость уборки:</div>
                        <div class="value" v-if="price !== null" ><span class="price"></span><span class="currency"></span></div>
                        <div class="serviceErrors">
                            <div class="error" v-if="!$v.area.required">Введите площадь помещения, чтобы узнать стоимость уборки</div>
                        </div>
                </div>

                <div class="serviceDescription">
                    <a class="aboutLink" @click="showAbout = !showAbout" >Что входит в стоимость?</a>
                    <div class="aboutContent" v-if="showAbout" >Описание услуги</div>
                </div>
            </div>
        </div>

</template>

<script>


import Vue from 'vue'

import Vuelidate from 'vuelidate'
Vue.use(Vuelidate)

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



export default {

    name: 'Cleaning',

    props:['item'],

    data () {
            return {
                type: this.item.type, 
                area: this.item.area, 
                showAbout: this.item.showAbout, 
                price: this.item.price, 
                isActive: this.item.isActive
            }
    },

    updated() 
    {
        $this.$emit('updated', this.data());
    },

    methods: {
        
        validateArea: function()
        {
            // Натуральное число
            const templateArea = /^(?:[1-9]\d{0,4})$/;

            if(templateArea.test(this.area))
            {
                return true;
            }

            return false;
            
        },

        filterArea: function(event) {
            //Целое число или пустое значение
            const templateArea = /^(?:[1-9]\d{0,4}|)$/;
            if(templateArea.test(this.area))
            {
                this.previousArea = this.area;
            }
            else
            {
                this.area = this.previousArea;            
            }
        },
    }
}
</script>

Answer the question

In order to leave comments, you need to log in

4 answer(s)
L
Lavich, 2019-10-13
@Lavich

Divide your Cleaning.vue component into Type.vue, Area.vue, Window.vue, etc., in which you implement the mechanics of each form element and its styling; for them you implement work through v-model:

<template>
                    <div class="fieldItem">
                        <label for="cleaningType">Тип уборки</label>
                        <select id="cleaningType" :value="value" @input="$emit('input', $event.value)">
                          <option disabled value="">Выберите один из вариантов</option>
                          <option value="1" >Быстрая</option>
                          <option value="2" >Генеральная</option>
                          <option value="3">После ремонта</option>
                        </select>
                    </div>
</template>

export default {
  props: {
    value: Number
}
}

in App.vue leave the validation and sending to the server:
<form class="calculatorForm">
        <Type v-model="type" />
        <Area v-model="area" />
        <Window v-model="windows" />
        ....
        <button @click.prevent="getPriceInfo" >Рассчитать</button>
    </form>

V
vitaly_74, 2019-10-13
@vitaly_74

not familiar with vue. but the code is understandable in principle, try to do this: all calculations in one file, all output in another.
in fact you will have 3 files. general (collecting)
so to speak "view" which inserts all the necessary data.
and a separate logic file. how it all unfolds for you.
it makes no sense to create a file for each button, just as it makes no sense to draw each button in a separate file

S
Savva Shishak, 2019-10-14
@Savva_Tobolsk

Connect the mobx library and take out all the logic from the templates, let the templates only transfer data to one of the mobx classes, and let the classes themselves decide what and where to display.

E
Evgeny Danilov, 2019-10-14
@jmelkor

The form is the most common component of web pages. Now there are a lot of frameworks and libraries for solving standard tasks. For example, the lightweight Buefy framework for Vue (if you want, you can disable css styles, leaving only the js stuffing). Your code will be significantly reduced by using ready-made components. Reuse of FOREIGN code should become a habit for you.
Second, connect eslint and follow its instructions. You have a lot of indentation and places where the code can be made more compact. For example, I write like this:

if (var === true) {
  do_something()
} else {
  print_something()
}

Well, here you have an extra comma after the word "do")))

Didn't find what you were looking for?

Ask your question

Ask a Question

731 491 924 answers to any question