[{"data":1,"prerenderedAt":942},["ShallowReactive",2],{"writing-building-a-controlled-vue-input":3},{"id":4,"title":5,"body":6,"category":927,"date":928,"description":929,"excerpt":930,"extension":931,"image":932,"keywords":933,"meta":934,"navigation":168,"path":935,"readingTime":936,"seo":937,"slug":938,"stem":939,"tags":940,"__hash__":941},"blog\u002Fblog\u002Fbuilding-a-controlled-vue-input.md","Building a controlled Vue input component",{"type":7,"value":8,"toc":924},"minimark",[9,30,37,59,256,259,368,374,387,640,643,800,825,828,833,846,911,914,917,920],[10,11,12,13,17,18,21,22,25,26,29],"p",{},"First, let's explain what a ",[14,15,16],"strong",{},"controlled component"," is. A controlled component is one that takes its ",[14,19,20],{},"current value"," from ",[14,23,24],{},"props"," and ",[14,27,28],{},"notifies the parent of changes to that value using an event."," Which means that usually, controlled components are form fields.",[10,31,32,33,36],{},"We all know that in every modern application there is a form that needs to be filled. Every form has fields, which is why we want to build ",[14,34,35],{},"reusable input components"," that we can use in all of our forms.",[10,38,39,40,44,45,44,48,25,51,54,55,58],{},"Imagine we have a big Vue component that is used for user registration. We would at least need 4 input fields there, ",[41,42,43],"em",{},"Name",", ",[41,46,47],{},"Email",[41,49,50],{},"password",[41,52,53],{},"password confirmation",". We can bind each of the inputs to some data in the Vue component using the ",[14,56,57],{},"v-model"," directive. Basically, the component would look something like this:",[60,61,66],"pre",{"className":62,"code":63,"language":64,"meta":65,"style":65},"language-vue shiki shiki-themes github-light github-dark","\u003Ctemplate>\n  \u002F* ... *\u002F\n  \u003Cinput type=\"text\" v-model=\"name\" ...>\n  \u003Cinput type=\"email\" v-model=\"email\" ...>\n  \u002F* ... *\u002F\n\u003C\u002Ftemplate>\n\n\u003Cscript>\n  export default {\n    data() {\n      return {\n        name: '',\n        email: '',\n        ...\n    };\n  }\n\u003C\u002Fscript>\n","vue","",[67,68,69,85,91,124,148,153,163,170,180,193,202,210,222,232,238,244,250],"code",{"__ignoreMap":65},[70,71,74,78,82],"span",{"class":72,"line":73},"line",1,[70,75,77],{"class":76},"sVt8B","\u003C",[70,79,81],{"class":80},"s9eBZ","template",[70,83,84],{"class":76},">\n",[70,86,88],{"class":72,"line":87},2,[70,89,90],{"class":76},"  \u002F* ... *\u002F\n",[70,92,94,97,100,104,107,111,114,116,119,122],{"class":72,"line":93},3,[70,95,96],{"class":76},"  \u003C",[70,98,99],{"class":80},"input",[70,101,103],{"class":102},"sScJk"," type",[70,105,106],{"class":76},"=",[70,108,110],{"class":109},"sZZnC","\"text\"",[70,112,113],{"class":102}," v-model",[70,115,106],{"class":76},[70,117,118],{"class":109},"\"name\"",[70,120,121],{"class":102}," ...",[70,123,84],{"class":76},[70,125,127,129,131,133,135,138,140,142,144,146],{"class":72,"line":126},4,[70,128,96],{"class":76},[70,130,99],{"class":80},[70,132,103],{"class":102},[70,134,106],{"class":76},[70,136,137],{"class":109},"\"email\"",[70,139,113],{"class":102},[70,141,106],{"class":76},[70,143,137],{"class":109},[70,145,121],{"class":102},[70,147,84],{"class":76},[70,149,151],{"class":72,"line":150},5,[70,152,90],{"class":76},[70,154,156,159,161],{"class":72,"line":155},6,[70,157,158],{"class":76},"\u003C\u002F",[70,160,81],{"class":80},[70,162,84],{"class":76},[70,164,166],{"class":72,"line":165},7,[70,167,169],{"emptyLinePlaceholder":168},true,"\n",[70,171,173,175,178],{"class":72,"line":172},8,[70,174,77],{"class":76},[70,176,177],{"class":80},"script",[70,179,84],{"class":76},[70,181,183,187,190],{"class":72,"line":182},9,[70,184,186],{"class":185},"szBVR","  export",[70,188,189],{"class":185}," default",[70,191,192],{"class":76}," {\n",[70,194,196,199],{"class":72,"line":195},10,[70,197,198],{"class":102},"    data",[70,200,201],{"class":76},"() {\n",[70,203,205,208],{"class":72,"line":204},11,[70,206,207],{"class":185},"      return",[70,209,192],{"class":76},[70,211,213,216,219],{"class":72,"line":212},12,[70,214,215],{"class":76},"        name: ",[70,217,218],{"class":109},"''",[70,220,221],{"class":76},",\n",[70,223,225,228,230],{"class":72,"line":224},13,[70,226,227],{"class":76},"        email: ",[70,229,218],{"class":109},[70,231,221],{"class":76},[70,233,235],{"class":72,"line":234},14,[70,236,237],{"class":185},"        ...\n",[70,239,241],{"class":72,"line":240},15,[70,242,243],{"class":76},"    };\n",[70,245,247],{"class":72,"line":246},16,[70,248,249],{"class":76},"  }\n",[70,251,253],{"class":72,"line":252},17,[70,254,255],{"class":76},"\u003C\u002Fscript>\n",[10,257,258],{},"But usually, our form fields don't look like this. They have some wrapper, they have labels, they have some classes etc etc. So, let's see what a typical form field looks like (in a real app, for example using Bootstrap):",[60,260,264],{"className":261,"code":262,"language":263,"meta":65,"style":65},"language-html shiki shiki-themes github-light github-dark","\u003Cdiv class=\"col-md-6\">\n  \u003Cdiv class=\"form-group\">\n    \u003Cinput type=\"text\" class=\"form-control\" ... >\n    \u003Clabel for=\"name\">Name \u003Csub*\u003C\u002Fsub>\u003C\u002Flabel>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\n","html",[67,265,266,283,298,323,351,360],{"__ignoreMap":65},[70,267,268,270,273,276,278,281],{"class":72,"line":73},[70,269,77],{"class":76},[70,271,272],{"class":80},"div",[70,274,275],{"class":102}," class",[70,277,106],{"class":76},[70,279,280],{"class":109},"\"col-md-6\"",[70,282,84],{"class":76},[70,284,285,287,289,291,293,296],{"class":72,"line":87},[70,286,96],{"class":76},[70,288,272],{"class":80},[70,290,275],{"class":102},[70,292,106],{"class":76},[70,294,295],{"class":109},"\"form-group\"",[70,297,84],{"class":76},[70,299,300,303,305,307,309,311,313,315,318,320],{"class":72,"line":93},[70,301,302],{"class":76},"    \u003C",[70,304,99],{"class":80},[70,306,103],{"class":102},[70,308,106],{"class":76},[70,310,110],{"class":109},[70,312,275],{"class":102},[70,314,106],{"class":76},[70,316,317],{"class":109},"\"form-control\"",[70,319,121],{"class":102},[70,321,322],{"class":76}," >\n",[70,324,325,327,330,333,335,337,340,344,347,349],{"class":72,"line":126},[70,326,302],{"class":76},[70,328,329],{"class":80},"label",[70,331,332],{"class":102}," for",[70,334,106],{"class":76},[70,336,118],{"class":109},[70,338,339],{"class":76},">Name \u003C",[70,341,343],{"class":342},"s7hpK","sub*\u003C\u002Fsub",[70,345,346],{"class":76},">\u003C\u002F",[70,348,329],{"class":80},[70,350,84],{"class":76},[70,352,353,356,358],{"class":72,"line":150},[70,354,355],{"class":76},"  \u003C\u002F",[70,357,272],{"class":80},[70,359,84],{"class":76},[70,361,362,364,366],{"class":72,"line":155},[70,363,158],{"class":76},[70,365,272],{"class":80},[70,367,84],{"class":76},[10,369,370,371],{},"Looking at this form field, one thing is for sure - we don't want to write this code every time we want to write a text input field, so we use the most important concept from Vue - ",[14,372,373],{},"building reusable components.",[10,375,376,377,379,380,383,384],{},"Now, if we want to wrap our heads around this issue, we can see that if we wrap this input field in a component, we want it to be a ",[14,378,16],{}," — receiving a value and emitting changes to the parent. So, let's do that. We create a new ",[14,381,382],{},"SFC (single file component)"," and start implementing our desired functionality. This is how it would look like. Please note that all of our state lives in the parent component. ",[41,385,386],{},"(Note: You cannot bind v-model to a prop, because Vue will let you know that you cannot change immutable objects. Instead, you should bind the value to the prop itself, and emit changes to the parent.)",[60,388,390],{"className":62,"code":389,"language":64,"meta":65,"style":65},"\u002F\u002F TextInput.vue\n\n\u003Ctemplate>\n  \u003Cdiv class=\"col-md-6\">\n    \u003Cdiv class=\"form-group\">\n      \u003Cinput :value=\"value\" @input=\"handleChange\" ... >\n      \u003Clabel for=\"name\">Name \u003Csub*\u003C\u002Fsub>\u003C\u002Flabel>\n    \u003C\u002Fdiv>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript>\n  export default {\n    props: {\n      value: {\n        required: true,\n        type: String,\n      }\n    },\n    methods: {\n      handleChange(e) {\n        this.$emit('handleChange', e.target.value);\n      }\n    }\n  }\n\u003C\u002Fscript>\n",[67,391,392,397,401,409,423,437,464,490,499,507,515,519,527,535,540,545,556,561,567,573,579,595,615,620,626,631],{"__ignoreMap":65},[70,393,394],{"class":72,"line":73},[70,395,396],{"class":76},"\u002F\u002F TextInput.vue\n",[70,398,399],{"class":72,"line":87},[70,400,169],{"emptyLinePlaceholder":168},[70,402,403,405,407],{"class":72,"line":93},[70,404,77],{"class":76},[70,406,81],{"class":80},[70,408,84],{"class":76},[70,410,411,413,415,417,419,421],{"class":72,"line":126},[70,412,96],{"class":76},[70,414,272],{"class":80},[70,416,275],{"class":102},[70,418,106],{"class":76},[70,420,280],{"class":109},[70,422,84],{"class":76},[70,424,425,427,429,431,433,435],{"class":72,"line":150},[70,426,302],{"class":76},[70,428,272],{"class":80},[70,430,275],{"class":102},[70,432,106],{"class":76},[70,434,295],{"class":109},[70,436,84],{"class":76},[70,438,439,442,444,447,449,452,455,457,460,462],{"class":72,"line":155},[70,440,441],{"class":76},"      \u003C",[70,443,99],{"class":80},[70,445,446],{"class":102}," :value",[70,448,106],{"class":76},[70,450,451],{"class":109},"\"value\"",[70,453,454],{"class":102}," @input",[70,456,106],{"class":76},[70,458,459],{"class":109},"\"handleChange\"",[70,461,121],{"class":102},[70,463,322],{"class":76},[70,465,466,468,470,472,474,476,478,481,484,486,488],{"class":72,"line":165},[70,467,441],{"class":76},[70,469,329],{"class":80},[70,471,332],{"class":102},[70,473,106],{"class":76},[70,475,118],{"class":109},[70,477,339],{"class":76},[70,479,480],{"class":80},"sub*",[70,482,483],{"class":342},"\u003C\u002Fsub",[70,485,346],{"class":76},[70,487,329],{"class":80},[70,489,84],{"class":76},[70,491,492,495,497],{"class":72,"line":172},[70,493,494],{"class":76},"    \u003C\u002F",[70,496,272],{"class":80},[70,498,84],{"class":76},[70,500,501,503,505],{"class":72,"line":182},[70,502,355],{"class":76},[70,504,272],{"class":80},[70,506,84],{"class":76},[70,508,509,511,513],{"class":72,"line":195},[70,510,158],{"class":76},[70,512,81],{"class":80},[70,514,84],{"class":76},[70,516,517],{"class":72,"line":204},[70,518,169],{"emptyLinePlaceholder":168},[70,520,521,523,525],{"class":72,"line":212},[70,522,77],{"class":76},[70,524,177],{"class":80},[70,526,84],{"class":76},[70,528,529,531,533],{"class":72,"line":224},[70,530,186],{"class":185},[70,532,189],{"class":185},[70,534,192],{"class":76},[70,536,537],{"class":72,"line":234},[70,538,539],{"class":76},"    props: {\n",[70,541,542],{"class":72,"line":240},[70,543,544],{"class":76},"      value: {\n",[70,546,547,550,554],{"class":72,"line":246},[70,548,549],{"class":76},"        required: ",[70,551,553],{"class":552},"sj4cs","true",[70,555,221],{"class":76},[70,557,558],{"class":72,"line":252},[70,559,560],{"class":76},"        type: String,\n",[70,562,564],{"class":72,"line":563},18,[70,565,566],{"class":76},"      }\n",[70,568,570],{"class":72,"line":569},19,[70,571,572],{"class":76},"    },\n",[70,574,576],{"class":72,"line":575},20,[70,577,578],{"class":76},"    methods: {\n",[70,580,582,585,588,592],{"class":72,"line":581},21,[70,583,584],{"class":102},"      handleChange",[70,586,587],{"class":76},"(",[70,589,591],{"class":590},"s4XuR","e",[70,593,594],{"class":76},") {\n",[70,596,598,601,604,607,609,612],{"class":72,"line":597},22,[70,599,600],{"class":552},"        this",[70,602,603],{"class":76},".",[70,605,606],{"class":102},"$emit",[70,608,587],{"class":76},[70,610,611],{"class":109},"'handleChange'",[70,613,614],{"class":76},", e.target.value);\n",[70,616,618],{"class":72,"line":617},23,[70,619,566],{"class":76},[70,621,623],{"class":72,"line":622},24,[70,624,625],{"class":76},"    }\n",[70,627,629],{"class":72,"line":628},25,[70,630,249],{"class":76},[70,632,634,636,638],{"class":72,"line":633},26,[70,635,158],{"class":76},[70,637,177],{"class":80},[70,639,84],{"class":76},[10,641,642],{},"What we did in this component is that we always emit the changed value to the parent, then the parent caches that changed event and updates its state, which then triggers the value on our controlled component to change since we pass it as a prop. This is tricky to understand, but let me show you how the parent component could look like, so you can better understand this concept.",[60,644,646],{"className":261,"code":645,"language":263,"meta":65,"style":65},"\u002F\u002F Form.vue\n\n\u003Ctemplate>\n  \u002F* ... *\u002F\n  \u003Ctext-input :value=\"value\" @handleChange=\"handleChange($event)\">\n  \u002F* ... *\u002F\n\u003C\u002Ftemplate>\n\n\u003Cscript>\n  export default {\n    data() {\n      return {\n        value: '',\n      }\n    },\n    methods: {\n      handleChange(payload) {\n        this.value = payload;\n      }\n    }\n  }\n\u003C\u002Fscript>\n",[67,647,648,653,657,665,669,692,696,704,708,716,724,730,736,745,749,753,757,768,780,784,788,792],{"__ignoreMap":65},[70,649,650],{"class":72,"line":73},[70,651,652],{"class":76},"\u002F\u002F Form.vue\n",[70,654,655],{"class":72,"line":87},[70,656,169],{"emptyLinePlaceholder":168},[70,658,659,661,663],{"class":72,"line":93},[70,660,77],{"class":76},[70,662,81],{"class":80},[70,664,84],{"class":76},[70,666,667],{"class":72,"line":126},[70,668,90],{"class":76},[70,670,671,673,676,678,680,682,685,687,690],{"class":72,"line":150},[70,672,96],{"class":76},[70,674,675],{"class":80},"text-input",[70,677,446],{"class":102},[70,679,106],{"class":76},[70,681,451],{"class":109},[70,683,684],{"class":102}," @handleChange",[70,686,106],{"class":76},[70,688,689],{"class":109},"\"handleChange($event)\"",[70,691,84],{"class":76},[70,693,694],{"class":72,"line":155},[70,695,90],{"class":76},[70,697,698,700,702],{"class":72,"line":165},[70,699,158],{"class":76},[70,701,81],{"class":80},[70,703,84],{"class":76},[70,705,706],{"class":72,"line":172},[70,707,169],{"emptyLinePlaceholder":168},[70,709,710,712,714],{"class":72,"line":182},[70,711,77],{"class":76},[70,713,177],{"class":80},[70,715,84],{"class":76},[70,717,718,720,722],{"class":72,"line":195},[70,719,186],{"class":185},[70,721,189],{"class":185},[70,723,192],{"class":76},[70,725,726,728],{"class":72,"line":204},[70,727,198],{"class":102},[70,729,201],{"class":76},[70,731,732,734],{"class":72,"line":212},[70,733,207],{"class":185},[70,735,192],{"class":76},[70,737,738,741,743],{"class":72,"line":224},[70,739,740],{"class":76},"        value: ",[70,742,218],{"class":109},[70,744,221],{"class":76},[70,746,747],{"class":72,"line":234},[70,748,566],{"class":76},[70,750,751],{"class":72,"line":240},[70,752,572],{"class":76},[70,754,755],{"class":72,"line":246},[70,756,578],{"class":76},[70,758,759,761,763,766],{"class":72,"line":252},[70,760,584],{"class":102},[70,762,587],{"class":76},[70,764,765],{"class":590},"payload",[70,767,594],{"class":76},[70,769,770,772,775,777],{"class":72,"line":563},[70,771,600],{"class":552},[70,773,774],{"class":76},".value ",[70,776,106],{"class":185},[70,778,779],{"class":76}," payload;\n",[70,781,782],{"class":72,"line":569},[70,783,566],{"class":76},[70,785,786],{"class":72,"line":575},[70,787,625],{"class":76},[70,789,790],{"class":72,"line":581},[70,791,249],{"class":76},[70,793,794,796,798],{"class":72,"line":597},[70,795,158],{"class":76},[70,797,177],{"class":80},[70,799,84],{"class":76},[10,801,802,803,806,807,810,811,25,814,817,818,821,822,603],{},"Let's break down what we do here. We send the changed value from the child component ",[67,804,805],{},"this.$emit('handleChange', e.target.value);"," and then in the parent we ",[14,808,809],{},"catch the fired event"," using ",[67,812,813],{},"@handleChange=\"handleChange($event)\"",[14,815,816],{},"assign the payload"," we send (Vue passes the payload we send from the child component in the ",[67,819,820],{},"$event"," variable) to the value we want to update in our parent component's state. ",[41,823,824],{},"(Note: payload can also be objects, arrays ..)",[10,826,827],{},"And that's it. Now we have a fully functional controlled component that we can reuse.",[829,830,832],"h3",{"id":831},"but-what-if-i-told-you-we-can-simplify-this-even-further","But what if I told you we can simplify this even further?",[10,834,835,836,838,839,25,842,845],{},"Vue's ",[14,837,57],{}," directive is basically a combination of ",[67,840,841],{},":value=\"value\"",[67,843,844],{},"@input=\"value = $event.target.value\"",". So, what we can do in our controlled component is implement those exact attributes\u002Fevents and we can then use v-model in our parent component. Let me show you what i mean.",[60,847,849],{"className":261,"code":848,"language":263,"meta":65,"style":65},"\u002F\u002F TextInput.vue\n\u003Cinput type=\"text\" :value=\"value\" @input=\"$emit('input', $event.target.value)\" ... \u002F>\n\n\u002F\u002F Form.vue\n\u003Ctext-input v-model=\"value\">\u003C\u002Ftext-input>\n",[67,850,851,855,885,889,893],{"__ignoreMap":65},[70,852,853],{"class":72,"line":73},[70,854,396],{"class":76},[70,856,857,859,861,863,865,867,869,871,873,875,877,880,882],{"class":72,"line":87},[70,858,77],{"class":76},[70,860,99],{"class":80},[70,862,103],{"class":102},[70,864,106],{"class":76},[70,866,110],{"class":109},[70,868,446],{"class":102},[70,870,106],{"class":76},[70,872,451],{"class":109},[70,874,454],{"class":102},[70,876,106],{"class":76},[70,878,879],{"class":109},"\"$emit('input', $event.target.value)\"",[70,881,121],{"class":102},[70,883,884],{"class":76}," \u002F>\n",[70,886,887],{"class":72,"line":93},[70,888,169],{"emptyLinePlaceholder":168},[70,890,891],{"class":72,"line":126},[70,892,652],{"class":76},[70,894,895,897,899,901,903,905,907,909],{"class":72,"line":150},[70,896,77],{"class":76},[70,898,675],{"class":80},[70,900,113],{"class":102},[70,902,106],{"class":76},[70,904,451],{"class":109},[70,906,346],{"class":76},[70,908,675],{"class":80},[70,910,84],{"class":76},[10,912,913],{},"Using this approach what we basically did is mimic the v-model's implementation for our custom component and simplified our controlled component even more. Seems pretty neat right? It's basically the same as a normal input field, but packaged as a reusable component with classes, labels, and whatever else you need.",[10,915,916],{},"This concept can be used also for custom check boxes and custom select form fields. Actually its best use is for custom form fields like these.",[10,918,919],{},"This concept can be extended to custom checkboxes, selects, and any other form field where you want reusability without sacrificing control.",[921,922,923],"style",{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s7hpK, html code.shiki .s7hpK{--shiki-default:#B31D28;--shiki-default-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":65,"searchDepth":87,"depth":87,"links":925},[926],{"id":831,"depth":93,"text":832},"Vue.js","2019\u002F08\u002F12","First, let's explain what a controlled component is. A controlled component is one that takes its current value from props and notifies the parent of changes to that value using an event. Which means that usually, controlled components are form fields.",null,"md","\u002Fimages\u002Fvue-controller-component.jpg","vue, vue.js, controlled component, vue component, advanced vue component design, advanced vue, vue model, vue v-model",{},"\u002Fblog\u002Fbuilding-a-controlled-vue-input","☕️ 5 min read",{"title":5,"description":929},"building-a-controlled-vue-input","blog\u002Fbuilding-a-controlled-vue-input","vue components, advanced vue, vue","ZfLj7Zul6NgBOhSoS016P2pJ_9aOeO-gJtmiMMRtE34",1774945272381]