Writing custom form controls to use v-model in Vue.js - javascript

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).

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>

Related

Laravel: Vue.js page not displaying in production environment

I'm working on a project in Laravel. I'm using vue.js components inside Laravel blade files. So far, I have a component called "Index.vue" which is loaded from index.blade.php. Another component called "create.vue" which is loaded from "create.blade.php". The problem is Create.vue component is not displaying when I click the button "create a new service" from index.blade.php. All the other Vue.js components works fine, but this issue is only for that particular component. I have tried to run "npm run dev" and changed webpack.mix.js file but it didn't solve my problem. I don't have any issues on my local machine, only in the production environment.
index.js
<x-admin-master>
#section('content')
<div class="container">
<div id="app">
<Index></Index>
Create a new service
</div>
</div>
#endsection
</x-admin-master>
Webpack.js
const mix = require('laravel-mix');
/*
|--------------------------------------------------------------------------
| Mix Asset Management
|--------------------------------------------------------------------------
|
| Mix provides a clean, fluent API for defining some Webpack build steps
| for your Laravel application. By default, we are compiling the Sass
| file for the application as well as bundling up all the JS files.
|
*/
mix.js('resources/js/app.js', 'public/js')
.vue()
.sass('resources/sass/app.scss', 'public/css')
.copy(
'node_modules/#fortawesome/fontawesome-free/webfonts',
'public/webfonts'
)
.sourceMaps()
.version(['public/js/app.js']);
Create.vue
<template>
<div>
<p v-if="errors.length">
<b>Please correct the following error(s):</b>
<ul>
<li v-for="error in errors">{{ error }}</li>
</ul>
</p>
<form #submit.prevent="addService" action="/api/services" method="POST">
<div class="form-group">
<label for="name">Name: </label>
<input type="text" name="name" class="form-control" id="name" placeholder="Enter service name" v-model="services.name">
</div>
<div class="form-group">
<label for="vendor">Vendor: </label>
<select class="form-control" name="vendor" placeholder="select vendor" v-model="services.vendor" value="Select vendor">
<option name="vendor" :value="service.vendor.id" v-for="service in services">{{service.vendor.name}}</option>
</select>
</div>
<div class="form-group">
<label for="service">Description: </label>
<textarea class="form-control" id="desc" name="desc" rows="3" v-model="services.desc"></textarea>
</div>
<div class="d-flex flex-row hour-price" v-if="hourAndPrice">
<div class="form-group">
<input type="number" name="hours" class="form-control" #click="fixedPrice = false" id="hours" placeholder="Hours" v-model="services.hours">
</div>
<div class="form-group">
<input type="number" name="price" class="form-control" #click="fixedPrice = false" id="price" placeholder="Price" v-model="services.price">
</div>
</div>
<div class="d-flex flex-row fixed-price" v-if="fixedPrice">
<div class="form-group">
<input type="number" #click="hourAndPrice = false" name="fixed_price" class="form-control" id="price" placeholder="Enter fixed price" v-model="services.fixed_price">
</div>
</div>
<input type="submit" value="Add Service" class="btn btn-primary">
<div class="btn-toolbar d-flex justify-content-end">
Services
</div>
</form>
</div>
</template>
<script>
export default {
data(){
return {
//vendors: this.vendors,
//projects: this.projects,
hourAndPrice: true,
fixedPrice: true,
services: [],
errors: [],
services: {
name: null,
vendor: null,
desc: null,
hours: null,
price: null,
fixed_price: null
}
}
},
methods: {
addService(){
let ok = ''
if(this.services.name && this.services.vendor){
ok = true
}
if(!this.services.name){
this.errors.push('Service name is required')
}
if(!this.services.vendor){
this.errors.push('Vendor ID is required')
}
if(ok) {
axios.get('/sanctum/csrf-cookie').then(response => {
axios.post('/api/services', {
name: this.services.name,
vendor: this.services.vendor,
desc: this.services.desc,
hours: this.services.hours,
price: this.services.price,
fixed_price: this.services.fixed_price,
})
.then(function (response) {
console.log(response)
window.location.href = '/admin/services';
})
.catch(function (error) {
console.log(error);
});
});
}
},
loadServices: function(){
axios.get('/api/services/getservicedata')
.then(response => {
this.services = response.data;
})
.catch(function(error){
console.log(error);
});
}
},
mounted(){
console.log('Successfully mounted!');
this.loadServices()
}
}
</script>

Vue.js: A value in a v-for loop is not staying with the correct array items

I am trying to create a simple application to request a car key for a service department. Obviously the code could be written better, but this is my third day with Vue.js. The time function that is called in the first p tag in the code updates every minutes to keep count of an elapsed time. The problem I am having is when I request a new key the time function doesn't follow the array items as intended. For example, if there are no other requests the first request I submit works perfectly. However, when I submit a new request the elapsed time from my first request goes to my second request. I am sure it could have something to do with the glued together code, but I have tried everything I can think of. Any help would be appreciated.
<template>
<div class="row">
<div class="card col-md-6" v-for="(key, index) in keys" :key="index">
<div class="card-body">
<h5 class="card-title">Service Tag: {{ key.service_tag }}</h5>
<p class="card-text"> {{time}} {{key.reqTimestamp}}min</p>
<p class="invisible">{{ start(key.reqTimestamp) }}</p>
<p class="card-text">Associates Name: {{key.requestor_name}}</p>
<p class="card-text">Model: {{key.model}}</p>
<p class="card-text">Color: {{key.color}}</p>
<p class="card-text">Year: {{key.year}}</p>
<p class="card-text">Comments: {{key.comments}}</p>
<p class="card-text">Valet: {{key.valet}}</p>
<input class="form-control" v-model="key.valet" placeholder="Name of the person getting the car...">
<button
#click="claimedKey(key.id, key.valet)"
type="submit"
class="btn btn-primary"
>Claim</button>
<button v-if="key.valet !== 'Unclaimed'"
#click="unclaimedKey(key.id, key.valet)"
type="submit"
class="btn btn-primary"
>Unclaim</button>
<button class="btn btn-success" #click="complete(key.id)">Complete</button>
</div>
</div>
<!-- END OF CARD -->
<!-- START OF FORM -->
<div class="row justify-content-md-center request">
<div class="col-md-auto">
<h1 class="display-4">Operation Tiger Teeth</h1>
<form class="form-inline" #submit="newKey(service_tag, requestor_name, comments, model, year, color, valet, reqTimestamp)">
<div class="form-group col-md-6">
<label for="service_tag">Service Tag: </label>
<input class="form-control form-control-lg" v-model="service_tag" placeholder="ex: TB1234">
</div>
<div class="form-group col-md-6">
<label for="service_tag">Associates Name: </label>
<!-- <input class="form-control form-control-lg" v-model="requestor_name" placeholder="Your name goes here..."> -->
<div class="form-group">
<label for="exampleFormControlSelect1">Example select</label>
<select v-model="requestor_name" class="form-control" id="requestor_name">
<option>James Shiflett</option>
<option>Austin Hughes</option>
</select>
</div>
</div>
<div class="form-group col-md-6">
<label for="service_tag">Model: </label>
<input class="form-control form-control-lg" v-model="model" placeholder="What is the model of the vehicle?">
</div>
<div class="form-group col-md-6">
<label for="service_tag">Color: </label>
<input class="form-control form-control-lg" v-model="color" placeholder="What is the color of the vehicle?">
</div>
<div class="form-group col-md-6">
<label for="service_tag">Year: </label>
<input class="form-control form-control-lg" v-model="year" placeholder="What year is the car?">
</div>
<div class="form-group col-md-6">
<label for="service_tag">Comments: </label>
<input class="form-control form-control-lg" v-model="comments" placeholder="Place any additional comments here...">
</div>
<div class="form-group col-md-6 invisible">
<label for="service_tag">Valet: </label>
<input v-model="valet">
</div>
<div class="form-group col-md-6 invisible">
<label for="service_tag">Timestamp: </label>
<input v-model="reqTimestamp">
</div>
<div class="col-md-12">
<button class="btn btn-outline-primary" type="submit">Request A Key</button>
</div>
</form>
</div>
</div>
</div>
</template>
<script>
import { db } from "../main";
import { setInterval } from 'timers';
export default {
name: "HelloWorld",
data() {
return {
keys: [],
reqTimestamp: this.newDate(),
service_tag: "",
requestor_name: "",
comments: "",
color: "",
model: "",
year: "",
inputValet: true,
valet: "Unclaimed",
state: "started",
startTime: '',
currentTime: Date.now(),
interval: null,
};
},
firestore() {
return {
keys: db.collection("keyRequests").where("completion", "==", "Incomplete")
};
},
methods: {
newKey(service_tag, requestor_name, comments, model, year, color, valet, reqTimestamp, completion) {
// <-- and here
db.collection("keyRequests").add({
service_tag,
requestor_name,
comments,
color,
model,
year,
valet,
reqTimestamp,
completion: "Incomplete",
});
this.service_tag = "";
this.requestor_name = "";
this.comments = "";
this.color = "";
this.model = "";
this.year = "";
this.reqTimestamp = this.newDate()
},
complete(id) {
db.collection("keyRequests").doc(id).update({
completion: "Complete"
})
},
// deleteKey(id) {
// db.collection("keyRequests")
// .doc(id)
// .delete();
claimedKey(id, valet) {
console.log(id);
this.inputValet = false
db.collection("keyRequests").doc(id).update({
valet: valet,
claimTimestamp: new Date()
})
},
moment: function () {
return moment();
},
newDate () {
var today = new Date()
return today
},
updateCurrentTime: function() {
if (this.$data.state == "started") {
this.currentTime = Date.now();
}
},
start(timestamp) {
return this.startTime = timestamp.seconds * 1000
}
},
mounted: function () {
this.interval = setInterval(this.updateCurrentTime, 1000);
},
destroyed: function() {
clearInterval(this.interval)
},
computed: {
time: function() {
return Math.floor((this.currentTime - this.startTime) /60000);
}
}
}
</script>
Ideally I am looking for the time lapse to follow each request.
So the problem lines in the template are:
<p class="card-text"> {{time}} {{key.reqTimestamp}}min</p>
<p class="invisible">{{ start(key.reqTimestamp) }}</p>
The call to start has side-effects, which is a major no-no for rendering a component. In this case it changes the value of startTime, which in turn causes time to change. I'm a little surprised this isn't triggering the infinite rendering recursion warning...
Instead we should just use the relevant data for the current iteration item, which you've called key. I'd introduce a method that calculates the elapsed time given a key:
methods: {
elapsedTime (key) {
const timestamp = key.reqTimestamp;
const startTime = timestamp.seconds * 1000;
return Math.floor((this.currentTime - startTime) / 60000);
}
}
You'll notice this combines aspects of the functions start and time. Importantly it doesn't modify anything on this.
Then you can call it from within your template:
<p class="card-text"> {{elapsedTime(key)}} {{key.reqTimestamp}}min</p>

Show bootstrap alert with condition (Vue.js): how can I access this variable in order to change its value?

First of all I would like to apologize if the answer to my question is obvious, however since I'm still pretty new to Vue.js, I'm getting really stuck here and I need help.
I got an authentication system and if the user wants to register without putting in an username, I would like to show an bootstrap alert. The code looks like this right now:
<template>
<div class="container">
<div class="row">
<div class="col-md-6 mt-5 mx-auto">
<form v-on:submit.prevent="register">
<h1 class="h3 mb-3 font-weight-normal">Register</h1>
<div class="form-group">
<label for="username">Username</label>
<input
type="text"
v-model="username"
class="form-control"
name="username"
placeholder="Please choose your username"
>
</div>
<div class="form-group">
<label for="email">Email Address</label>
<input
type="email"
v-model="email"
class="form-control"
name="email"
placeholder="Please enter your email address"
>
</div>
<div class="form-group">
<label for="password">Password</label>
<input
type="password"
v-model="password"
class="form-control"
name="password"
placeholder="Please choose your password"
>
</div>
<button class="btn btn-lg btn-primary btn-block" type="submit">Register</button>
</form>
<div>
<b-alert variant="success" show>Example alert</b-alert>
</div>
<div>
<b-alert variant="danger" :show="showAlert">Example Alert!</b-alert>
</div>
</div>
</div>
</div>
</template>
<script>
import axios from "axios";
import router from "../router";
export default {
data() {
return {
username: "",
email: "",
password: "",
showAlert: false };
},
methods: {
register() {
axios
.post("/user/register", {
username: this.username,
email: this.email,
password: this.password
})
.then(res => {
if (!res.data.username) {
// show an alert, I would like to do something similar to "showAlert = true;"
} else {
// redirect to login
}
})
.catch(err => {
console.log(err);
});
}
}
};
</script>
<style scoped>
#import "../assets/css/reglog.css";
#import "../assets/css/modal.css";
</style>
However I'm not sure how to access the showAlert variable neither how to change its value in the if-statement. The only thing that I know here is that if I change the showAlert manually in the code (line 9 counting from the script tag) from false to true, the page does react and shows the alert when wanted.
I'm sorry if you need more information or if something is unclear, I'm a bit tired and stuck with this for some hours, not gonna lie.
You can access showAlert variable following: this.showAlert = true
.then(res => {
if (!res.data.username) {
this.showAlert = true; // update showAlert
} else {
// redirect to login
}
})

Input not binding correctly with Vue.js

I'm still relatively new to Vue.js and am having an issue binding one of my inputs to my viewmodel.
Here is my JavaScript:
var viewModel = new Vue({
el: "#InventoryContainer",
data: {
upcCode: "",
component: {
Name: ""
}
},
methods: {
upcEntered: function (e) {
if (this.upcCode.length > 0){
$.ajax({
url: "/Component/GetByUpc",
type: "GET",
data: {
upc: this.upcCode
}
}).done(function (response) {
if (response.exists) {
$("#ComponentInformation").toggleClass("hidden");
this.component = response.component;
} else {
alert("No component found.");
}
});
}
}
}
});
Here is my HTML:
<div class="form-horizontal row">
<div class="col-sm-12">
<div class="form-group">
<label class="control-label col-md-4">UPC Code</label>
<div class="col-md-8">
<input id="ComponentUPC" class="form-control" placeholder="Scan or enter UPC Code" v-on:blur="upcEntered" v-model="upcCode" />
</div>
</div>
<div id="ComponentInformation" class="hidden">
<input type="text" class="form-control" readonly v-model="component.Name" />
</div>
</div>
</div>
Now the issue is that even when I enter a valid UPC code and I assign the component to my ViewModel, the input that is bound to component.Name does not update with the component name. And when I enter into the console viewModel.component.Name I can see that it returns "".
But if I put an alert in my ajax.done function after I've assigned the component and it looks like this alert(this.component.Name) it alerts the name of the component.
Any ideas of where I'm going wrong here?
You cannot use that line
this.component = response.component;
because of the this-variable.
You should put the line
var self = this
before your ajax call and use self.component instead of this.component
in order for vue to work you need to define the parent container with id InventoryContainer
<div id="InventoryContainer" class="form-horizontal row">
<div class="col-sm-12">
<div class="form-group">
....
here is the updated code: https://jsfiddle.net/hdqdmscv/
here is the updated fiddle based on your comment
https://jsfiddle.net/hdqdmscv/2/
(replace this with name of vue variable in ajax)

AngularJS reset form completely

I have a pretty big form that's being validated on the client side by Angular. I am trying to figure out how to reset the state of the form and its inputs just clicking on a Reset button.
I have tried $setPristine() on the form but it didn't really work, meaning that it doesn't clear the ng-* classes to reset the form to its original state with no validation performed.
Here's a short version of my form:
<form id="create" name="create" ng-submit="submitCreateForm()" class="form-horizontal" novalidate>
<div class="form-group">
<label for="name" class="col-md-3 control-label">Name</label>
<div class="col-md-9">
<input required type="text" ng-model="project.name" name="name" class="form-control">
<div ng-show="create.$submitted || create.name.$touched">
<span class="help-block" ng-show="create.name.$error.required">Name is required</span>
</div>
</div>
</div>
<div class="form-group">
<label for="lastName" class="col-md-3 control-label">Last name</label>
<div class="col-md-9">
<input required type="text" ng-model="project.lastName" name="lastName" class="form-control">
<div ng-show="create.$submitted || create.lastName.$touched">
<span class="help-block" ng-show="create.lastName.$error.required">Last name is required</span>
</div>
</div>
</div>
<button type="button" class="btn btn-default" ng-click="resetProject()">Reset</button>
</form>
And my reset function:
$scope.resetProject = function() {
$scope.project = {
state: "finished",
topic: "Home automation"
};
$("#create input[type='email']").val('');
$("#create input[type='date']").val('');
$scope.selectedState = $scope.project.state;
// $scope.create.$setPristine(); // doesn't work
}
Also if you could help me clear the input values of the email and date fields without using jQuery would be great. Because setting the $scope.project to what's defined above doesn't reset the fields for some reason.
Try to clear via ng-model
$scope.resetProject = function() {
$scope.project = {
state: "finished",
topic: "Home automation"
};
$("#create input[type='email']").val('');
$("#create input[type='date']").val('');
$scope.selectedState = $scope.project.state;
$scope.project = {
name: "",
lastName: ""
};
}
As mentioned in the comments, you can use $setUntouched();
https://docs.angularjs.org/api/ng/type/form.FormController#$setUntouched
This should set the form back to it's new state.
So in this case $scope.create.$setUntouched(); should do the trick
Ref all that jquery. You should never interact with the DOM via controllers. That's what the directives are for
If you want to reset a given property then do something like:
$scope.resetProject = function() {
$scope.project = {
state: "finished",
topic: "Home automation"
};
$scope.project.lastName = '';
$scope.project.date= '';
}

Categories