3

I'm trying to write a custom form control in Vue.js to learn how to package together multiple form inputs into a single custom component.

My project setup looks like this (standard webpack-simple vue cli setup):

$ tree -L 1
.
├── README.md
├── index.html
├── node_modules
├── package.json
├── src
└── webpack.config.js

Here's my top level Vue instance .vue file:

// App.vue
<template>
    <div class="container">
        <form v-if="!submitted" >
            <div class="row">
                <div class="col-xs-12 col-sm-8 col-sm-offset-2 col-md-6 col-md-offset-3">
                    <form>
                        <fullname v-model="user.fullName"></fullname>

                        <div class="form-group">
                            <label for="email">Email:</label>
                            <input id="email" type="email" class="form-control" v-model="user.email">
                            <label for="password">Password:</label>
                            <input id="password" type="password" class="form-control" v-model="user.password">
                        </div>

                        <fieldset class="form-group">
                            <legend>Store data?</legend>
                            <div class="form-check">
                              <label class="form-check-label">
                                <input type="radio" class="form-check-input" name="storeDataRadios" id="storeDataRadios1" value="true" checked v-model="user.storeData">
                                Store Data
                              </label>
                            </div>
                            <div class="form-check">
                            <label class="form-check-label">
                                <input type="radio" class="form-check-input" name="storeDataRadios" id="storeDataRadios2" value="false" v-model="user.storeData">
                                No, do not make my data easily accessible
                              </label>
                            </div>
                        </fieldset>
                    </form>

                    <button class="btn btn-primary" @click.prevent="submitForm()">Submit</button>

                </div>
            </div>
        </form>
        <hr>
        <div v-if="submitted" class="row">
            <div class="col-xs-12 col-sm-8 col-sm-offset-2 col-md-6 col-md-offset-3">
                <div class="panel panel-default">
                    <div class="panel-heading">
                        <h4>Your Data</h4>
                    </div>
                    <div class="panel-body">
                        <p>Full Name: {{ user.fullName }}</p>
                        <p>Mail: {{ user.email }}</p>
                        <p>Password: {{ user.password }} </p>
                        <p>Store in Database?: {{ user.storeData }}</p>
                    </div>
                </div>
            </div>
        </div>
    </div>
</template>

<script>
    import FullName from './FullName.vue';

    export default {
        data() {
            return {
                user: {
                    fullName: 'John Smith',
                    email: '', 
                    password: '',
                    storeData: true,
                }, 
                submitted: false
            }
        }, 
        methods: {
            submitForm() {
                this.submitted = true;
            },
        },
        components: {
            'fullname' : FullName,
        }
    }
</script>

<style>
</style>

And here's the custom component file:

<template>
    <div class="form-group">
        <label for="firstName">First name:</label>
        <input id="firstName" type="text" class="form-control" :value="first" @input="emitChange(true, $event)">

        <label for="lastName">Last name:</label>
        <input id="lastName" type="text" class="form-control" :value="last" @input="emitChange(false, $event)">
    </div>
</template>

<script>
    export default {
        props: ['value'],
        methods: {
            emitChange(isFirst, evt) {
                let name = '';
                let evtValue = evt.target.value == undefined ? "" : evt.target.value;

                if (isFirst) {
                    name = evtValue +" "+ this.second;
                } else {
                    name = this.first +" "+ evtValue;
                }

                this.value = name;
                this.$emit('input', this.value);
            }
        },
        computed: {
            first() {
                if (this.value != "")
                    return this.value.split(" ")[0];
                else return "";
            }, 
            last() {
                if (this.value != "")
                    return this.value.split(" ")[1];
                else return "";
            }
        }
    }
</script>

Which I also realize I'm messing up because I'm directly editing a prop value with:

this.value = name;

(not an error, but Vue.JS gives a warning).

However, even before that, typing in the first input box causes the second input box to update its value to undefined (...wat!?).

Would be grateful for advice on how to properly set up custom form control components! (and why this example isn't working).

1 Answer 1

1

I think, the problem here is you never know where the first name ends and where the last name starts. Take Barack Hussein Obama for instance and imagine he's Belgian, his name would be Barack Hussein van Obama. You can't safely assume which part ist first and which part is lastname.

However, if you could say the firstname is exactly one word and the rest is lastname, here's an example implementation (stripped down). To illustrate the problem, try to put in Obamas second name.

Otherwise, the component behaves like a two way bound component. You can alter the separate values on the fullname component, or edit the fullname on the root component and everything stays up to date. The watcher listens for changes from above, the update method emits changes back up.

Vue.component('fullname', {
  template: '#fullname',
  data() {
    // Keep the separate names on the component
    return {
      firstname: this.value.split(' ')[0],
      lastname: this.value.split(' ')[1],
    }
  },
  props: ['value'],
  methods: {
    update() {
      // Notify the parent of a change, chain together the name
      // and emit
      this.$emit('input', `${this.firstname} ${this.lastname}`);
    },
  },
  mounted() {
    // Parse the prop input and take the first word as firstname,
    // the rest as lastname.
    // The watcher ensures that the component stays up to date
    // if the parent changes.
    this.$watch('value', function (value){
      var splitted = value.split(' ');
      this.firstname = splitted[0];
      splitted.shift();
      this.lastname = splitted.join(' ');
    });
  }
});

new Vue({
  el: '#app',
  data: {
    fullname: 'Barack Obama',
  }
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.1.10/vue.js"></script>

<div id="app">
  <fullname v-model="fullname"></fullname>
  <p>Thanks, {{fullname}}</p>
  <input v-model="fullname" />
</div>

<template id="fullname">
  <div>
    <input v-model="firstname" @input="update" type="text" id="firstname" />
    <input v-model="lastname" @input="update" type="text" id="lastname" />
  </div>
</template>

Sign up to request clarification or add additional context in comments.

1 Comment

awesome, all makes sense now!!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.