Vue.component('json-input', { template: '\ <div>\ <input\ ref="input"\ v-bind:class="{ dirty: isDirty }"\ v-bind:value="format(value)"\ v-on:input="parse($event.target.value)"\ >\ <hr>\ isDirty: {{ isDirty }}\ <br>\ isObject: {{ isObject }}\ <br>\ isBoolean: {{ isBoolean }}\ <br>\ isNumber: {{ isNumber }}\ <br>\ isString: {{ isString }}\ </div>\ ', props: { // The form label/key label: { type: String, required: true }, // The form value value: { required: true } }, data() { return { // dirty is true if the type of the field doesn't match the original // value passed. dirty: false, // typeChecked is true when the type of the original value has been // checked. This allows us to validate user-input against the original // (expected) type. typeChecked: false, isObject: false, isBoolean: false, isNumber: false, isString: false } }, computed: { isDirty: function() { return this.dirty } }, methods: { // init determines the JS type of the field (once) during initialization. init: function() { this.typeChecked = false this.isObject = false this.isBoolean = false this.isNumber = false this.isString = false if (_.isPlainObject(this.value) || _.isArrayLikeObject(this.value)) { this.isObject = true } else if (_.isNumber(this.value)) { this.isNumber = true } else if (_.isBoolean(this.value)) { this.isBoolean = true } else if (_.isString(this.value)) { this.isString = true } this.typeChecked = true }, // format returns a formatted value based on its type; Objects are // JSON.stringify'ed, and Boolean & Number values are noted to prevent // reading them back as strings. format: function() { // Check the types of our fields on the initial format. if (!this.typeChecked) { this.init() } var res if (this.isObject) { res = JSON.stringify(this.value) } else if (this.isNumber) { res = this.value } else if (this.isBoolean) { res = this.value } else if (this.isString) { res = this.value } else { res = JSON.stringify(this.value) } return res }, // Based on custom component events from // https://vuejs.org/v2/guide/components.html#Form-Input-Components-using-Custom-Events parse: _.debounce(function(value) { this.dirty = false if (this.isObject) { var res try { res = JSON.parse(value) this.$emit("input", this.format(res)) } catch (e) { // Mark the field as dirty. this.dirty = true res = value } this.$emit("input", res) return } // Check the original type of the value; if the user-input does not conform // flag the field as dirty. if (this.isBoolean) { if (value === "true" || value === "false") { this.dirty = false // Convert back to a Boolean. this.$emit("input", (value === "true")) return } this.dirty = true this.$emit("input", value) return } else if (this.isNumber) { // Convert numbers back to numbers. let num = _.toNumber(value) if (_.isNumber(num) && _.isFinite(num)) { this.$emit("input", num) return } this.dirty = true this.$emit("input", value) return } else { // Write other types as-is. this.$emit("input", value) return } }, 1000) } }) new Vue({ el: '#app', data: { obj: { "count": 555 } } }) ()
<script src="https://unpkg.com/vue/dist/vue.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.3/lodash.min.js"></script> <div id="app"> <json-input label="thing" v-model="obj.count"></json-input> <hr>Data: {{ obj }} </div>
html, div, input { font-size: 14px; font-family: "Fira Mono"; line-height: 24px; width: 90%; } input.dirty { border: 2px solid crimson; }