Vue JS change submit button if errors - javascript

Using Vuex I have a form that when the button is clicked (#click="loader(true)") sends to the loader mutation to change loading to true, which then sets a is-loading class with Bulma CSS to true ('is-loading' : $store.state.index.loading ).
I then receive errors from the server if the form is empty with errors.title, this works fine with the inputs but how do I then set the is-loading class to false if there are errors?
(the code snippet will not work if you run it)
export const state = () => ({
loading: false
});
export const mutations = {
loader(state, value) {
state.loading = value;
}
}
<form #submit.prevent="postThis">
<div class="field">
<div class="control">
<input class="input" :class="{ 'is-danger': errors.title }" type="text" id="title" placeholder="I have this idea to..." autofocus="" v-model="newTitle">
</div>
<p class="is-size-6 help is-danger" v-if="errors.title">
{{ errors.title[0] }}
</p>
</div>
<div class="field">
<div class="control">
<button #click="loader(true)" type="submit" :class="{'is-loading' : $store.state.index.loading }">
Post
</button>
</div>
</div>
</form>
<script>
import {mapMutations,} from 'vuex';
methods: {
...mapMutations({
loader: 'index/loader'
})
}
</script>

The question is about using ...mapMutations, but in case someone want to add business logic, mapAction and mapState would be recommended. I will explain how to make it work with mapAction and mapState since calling API might involve using business logic within your application. Otherwise, I would say, why do you even bother using VueX except for notifying other part of your application that you are loading ;). That being said, here's my answer.
Using the ...mapState you have what you would be searching for, the computed reactivity of the state. This would happen especially during the invoke of the action. The action would then be changing, or what we call commit in VueX, the state (See doc: https://vuex.vuejs.org/guide/state.html)
Let's take your code and change it into a module with a namespace and then use the module in your vue (This is what I would do if the application is big, otherwise the same can be achieved using the mutation or no VueX at all):
const LOADING_STATE = 'LOADING_STATE'
export default {
namespaced: true, // Read the doc about that
state: {
loaded: false
},
mutations: {
[LOADING_STATE]: function (state, isLoading) {
state.loading = isLoading
}
},
actions: {
setLoading ({ commit }, isLoading) {
commit(LOADING_STATE, isLoading)
}
}
}
For your vue file where you have your template and your actions. It would look like this:
<script>
import { mapAction, mapState } from 'vuex'
exports default {
computed: {
...mapState({
// Here you could even have the full computation for the CSS class.
loading: state => state.loadingModule.loading,
// Like this... or you could use the getters that VueX does (search in the documentation since it's out of the scope of your question)
loadingCss: state => { return state.loadingModule.loading ? 'is-loading' : '' }
})
},
methods: {
// Usage of a namespace to avoid other modules in your VueX to have the same action.
...mapActions(['loadingModule/setLoading']),
}
}
</script>
And regarding your html template, you will be able to call the method this['loadingModule/setLoading'](true) or false and then the property that you can react to will be "loading".
While using promises, during your post or get or any other HTTP rest call, don't forget the context. If you're using Axios, after registering it in your VueJs context, I would do
this.$http.get('/my/api')
.then(response => {
// ... some code and also set state to ok ...
})
.catch(e => {
// set state to not loading anymore and open an alert
})
Let's complete your code now considering you're doing your HTTP(S) call somewhere.
<form #submit.prevent="postThis">
<div class="field">
<div class="control">
<!-- Here I would then use a computed property for that class (error). I would even put the a template or a v-if on a div in order to show or not all those html elements. That's you're choice and I doubt this is your final code ;) -->
<input class="input" :class="{ 'is-danger': errors.title }" type="text" id="title" placeholder="I have this idea to..." autofocus="" v-model="newTitle">
</div>
<p class="is-size-6 help is-danger" v-if="errors.title">
{{ errors.title[0] }}
</p>
</div>
<div class="field">
<div class="control">
<button #click="['loadingModule/setLoading'](true)" type="submit" :class="{'is-loading' : loading }">
Post
</button>
</div>
</div>
</form>

First, there is no need to have locally only needed state (loading) in global state (Vuex). So, typical usage looks like this:
<template>
<form>
<div class="field">
<div class="control">
<input
class="input" :class="{ 'is-danger': errors.title }"
type="text"
id="title"
placeholder="I have this idea to..."
autofocus=""
v-model="newTitle"
>
</div>
<p class="is-size-6 help is-danger" v-if="errors.title">
{{ errors.title[0] }}
</p>
</div>
<div class="field">
<div class="control">
<button
#click="postForm"
:class="{'is-loading': isLoading }"
>
Post
</button>
</div>
</div>
</form>
</template>
<script>
export default {
...
data () {
return {
...
newTitle: '',
isLoading: false,
response: null
}
},
methods: {
async postForm () {
try {
this.isLoading = true // first, change state to true
const { data } = await axios.post('someurl', { title: this.newTitle }) // then wait for some async call
this.response = data // save the response
} catch(err) {
// if error, process it here
} finally {
this.isLoading = false // but always change state back to false
}
}
}
}
</script>

if you using vuex like this. I guess you misunderstood vuex. Because you can use for local variable and you can check api result. if you want seperate api request, you have to mapAction in methods and mapGetters in Computed

Related

Vue2: Use form component with input type textarea to display AND edit data (without directly manipulating props)

I am building an MVP and this is the first time I do web development. I am using Vue2 and Firebase and so far, things go well.
However, I ran into a problem I cannot solve alone. I have an idea how it SHOULD work but cannot write it into code and hope you guys can help untangle my mind. By now I am incredibly confused and increasingly frustrated :D
So lets see what I got:
Child Component
I have built a child component which is a form with three text-areas. To keep it simple, only one is included it my code snippets.
<template>
<div class="wrap">
<form class="form">
<p class="label">Headline</p>
<textarea rows="2"
v-model="propHeadline"
:readonly="readonly">
</textarea>
// To switch between read and edit
<button
v-if="readonly"
#click.prevent="togglemode()">
edit
</button>
<button
v-else
type="submit"
#click.prevent="togglemode(), updatePost()"
>
save
</button>
</form>
</div>
</template>
<script>
export default {
name: 'PostComponent'
data() {
return {
readonly: true
}
},
props: {
propHeadline: {
type: String,
required: true
}
},
methods: {
togglemode() {
if (this.readonly) {
this.readonly = false
} else {
this.readonly = true
}
},
updatePost() {
// updates it to the API - that works
}
}
}
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
And my parent component:
<template>
<div class="wrap">
<PostComponent
v-for="post in posts"
:key="post.id"
:knugHeadline="post.headline"
/>
</div>
</template>
<script>
import PostComponent from '#/components/PostComponent.vue'
export default {
components: { PostComponent },
data() {
return {
posts: []
}
},
created() {
// Gets all posts from DB and pushes them in array "posts"
}
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
Current Status
So far, everything works. I can display all posts and when clicking on "edit" I can make changes and save them. Everything gets updated to Firebase - great!
Problem / Error Message
I get the following error message:
[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value.
As the error says I should use a computed property based on the props value. But how can I achieve that?
Solution Approach
I believe I have to use a computed getter to return the prop value - how to do that?
And then I have to use the setter to emit an event to the parent to update the value so the prop passes it back down - how to do that?
I have found bits and pieces online but by now all I see is happy families passing around small packages of data...
Would be really thankful for a suggestion on how to solve this one! :)
Thanks a lot!
This error shows because of your v-model on texterea which mutate the prop, but in vue it is illegal to mutate props :
<textarea rows="2"
v-model="propHeadline"
:readonly="readonly">
</textarea>
So, what you could do is to use this created() lifecycle hook and set the propHeadline prop as data :
<script>
export default {
name: 'PostComponent'
data() {
return {
readonly: true,
headline: ""
}
},
props: {
propHeadline: {
type: String,
required: true
}
},
created() {
this.headline = this.propHeadline
}
}
</script>
An then, update the new variable on your textarea :
<textarea rows="2"
v-model="headline"
:readonly="readonly">
</textarea>

Why does old value not passed to vue component?

why does old value not passed to vue component?
passed old value here
<autocomplete-region-component :query="old('regionName')"></autocomplete-region-component>
my code from vue component
<template>
<div>
<input
type="text"
autocomplete="off"
v-model="query"
v-on:keyup="autoComplete"
class="form-control js-region-name"
name="regionName"
value=""
>
<input
type="hidden"
class="form-control js-region-id"
name="regionId"
value="enteredRegion">
<div class="panel-footer" v-if="results.length">
<ul class="list-group select-region">
<li class="list-group-item list-region" v-for="result in results" v-on:click="selectRegion(result)">
{{ result.name }}
</li>
</ul>
</div>
</div>
</template>
<script>
export default {
data() {
return {
results: [],
query: '',
}
},
methods: {
autoComplete() {
this.results = [];
if(this.query.length > 2){
axios.get('/api/regions',{params: {_limit: 2, query: this.query}}).then(response => {
this.results = response.data;
});
}
},
selectRegion(result) {
let inputWithRegionName = document.querySelector('.js-region-name');
let inputWithRegionId = document.querySelector('.js-region-id');
let listRegions = document.querySelector('.panel-footer');
inputWithRegionName.value = result.name;
inputWithRegionId.value = result.id;
listRegions.hidden = true;
}
}
}
</script>
console not have mistakes
Please help with a detailed answer since I am a beginner and really need your help. thanks
UPDATE
When you create a component (like you have with <autocomplete-region-component>, if you want to pass values to it from it's parent, you have to define the prop at the component level. So in your script, add a props property like so:
props: [
'query',
],
Now in your autocomplete-region-component component, you can use the value of query as this.query as you would expect.
In your component tag, you don't use mustache tags to pass the value, you would just pass normal javascript. I'd also recommend not encoding the json at that point. You could always encode it within the component if you needed to.
<autocomplete-region-component :query="old('regionName')">
</autocomplete-region-component>
You have to define a prop and set the data to that prop
When you pass data in a component tag via the v-bind shorthand : these data are known as props and have to be defined as such
Vue disallows mutating props passed from the parent in the child, therefor you should make a local copy of that prop in the reactive data object
props: {
queryProp: {
required: false,
type: String
}
},
data() {
return {
results: [],
query: this.queryProp
};
},
Assuming a Blade view like so
<div id="app">
<form action="/" method="post">
#csrf
<input type="text" name="regionName"> <br>
<button type="submit">Submit</button>
</form>
<autocomplete-region-component :query-prop="{{ json_encode(old('regionName')) }}"></autocomplete-region-component>
</div>
<script src="/js/app.js"></script>
And routes like so
<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::view('/', 'welcome');
Route::post('/', fn (Request $request) => $request->validate(['regionName' => 'integer']));
This is what you would get if invalid data is posted
Mustache syntax is to interpolate text, not to pass props.
To pass props or set attributes to child components (including native HTML elements), you need to use v-bind directive (doc).
<a> element is good to remember this rule.
<a :href="myUrl">{{ linkText }}</a>
Then, your code should look like this:
<autocomplete-region-component :query="json_encode(old('regionName'))">
</autocomplete-region-component>

Svelte: Replace nested component by changing binded variable

I am writing Svelte project, where I have Message component which represents some js object.
There is ability to edit this object. For this purpose I desided to use two nested component MessageEditable and MessageReadable.
They should replace each other, depending on Message component state.
The problem is that when I am trying to save result of editing and
change MessageEditable to MessageReadable by setting isEditing property to false I get error:
image of error from console
Did I make a mistake or this is normal behavior? Is binding a good approach or there is more optimal for exchanging of values with parent components?
Message:
<div class="message">
{#if isEditing}
<MessageEditable bind:message bind:isEditing />
{:else}
<MessageReadable {message}/>
{/if}
<div class="message__controllers">
<button on:click="set({isEditing: true})">Edit</button>
</div>
</div>
<script>
import MessageEditable from './MessageEditable.html';
import MessageReadable from './MessageReadable.html';
export default {
components:{
MessageEditable,
MessageReadable,
},
data:() => ({
message:{
id: '0',
text: 'Some message text.'
},
isEditing: false,
}),
}
</script>
MessageEditable:
<form class="message-editable" on:submit>
<label><span >text</span><input type="text" bind:value=message.text required></label>
<label><span>id</span><input type="text" bind:value=message.id required></label>
<div><button type="submit">Save</button></div>
</form>
<script>
export default {
events:{
submit(node){
node.addEventListener('submit', (event) => {
event.preventDefault();
this.set({isEditing: false});
});
},
},
};
</script>
MessageReadable:
<div class="message-readable">
<p><span>text: </span>{message.text}</p>
<p><span>id: </span>{message.id}</p>
</div>
Its probably better to use a method than a custom event handler since you are performing actions on submit. I tested this code in the REPL and didn't experience the error you are getting. I changed your events to a methods property and removed the node functionalities.
<form class="message-editable" on:submit="save(event)">
<label><span >text</span><input type="text" bind:value=message.text required></label>
<label><span>id</span><input type="text" bind:value=message.id required></label>
<div><button type="submit">Save</button></div>
</form>
<script>
export default {
methods: {
save(event){
event.preventDefault();
this.set({isEditing: false});
},
},
};
</script>
https://svelte.technology/repl?version=2.10.0&gist=d4c5f8e3864856d27a3aa8cb5b2e8710

Using sync modifier between Parent and Grandchildren Vue 2

Problem
Let's say I have a vue component called:
Note: All vue components has been simplified to explain what I'm trying to do.
reusable-comp.vue
<template>
<div class="input-group input-group-sm">
<input type="text" :value.number="setValue" class="form-control" #input="$emit('update:setValue', $event.target.value)">
<span>
<button #click="incrementCounter()" :disabled="disabled" type="button" class="btn btn-outline-bordercolor btn-number" data-type="plus">
<i class="fa fa-plus gray7"></i>
</button>
</span>
</div>
</template>
<script>
import 'font-awesome/css/font-awesome.css';
export default {
props: {
setValue: {
type: Number,
required: false,
default: 0
}
},
data() {
return {
}
},
methods: {
incrementCounter: function () {
this.setValue += 1;
}
}
}
</script>
Then in a parent component I do something like this:
subform.vue
<div class="row mb-1">
<div class="col-md-6">
Increment Value of Num A
</div>
<div class="col-md-6">
<reuseable-comp :setValue.sync="numA"></reuseable-comp>
</div>
</div>
<script>
import reusableComp from '../reusable-comp'
export default {
components: {
reusableComp
},
props: {
numA: {
type: Number,
required: false,
default: 0
}
},
data() {
return {
}
}
</script>
then lastly
page_layout.vue
<template>
<div>
<subform :numA.sync="data1" />
</div>
</template>
<script>
import subform from '../subform.vue'
export default {
components: {
subform
},
data() {
return {
data1: 0
}
}
}
</script>
Question
So, how do I sync a value between reusable-comp.vue, subform.vue, and page_layout.vue
I'm using reuseable-comp.vue is many different places. I'm using subform.vue only a couple times in page_layout.vue
And I'm trying to use this pattern several times. But I can't seem to get this to work. The above gives me an error:
Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "numA"
Okay I found a solution that worked.
In subform.vue, we change:
data() {
return {
numA_data : this.numA
}
}
So we now have reactive data to work with. Then in the template, we refer to that reactive data instead of the prop:
<reuseable-comp :setValue.sync="numA_data"></reuseable-comp>
Then finally we add a watcher to check if the reactive data gets changed, and then emit to the parent:
watch: {
numA_data: function(val) {
this.$emit('update:numA', this.numA_data);
}
}
Now all values from grandchildren to parent are synced.
Update (4/13/2018)
I made new changes to the reusable-comp.vue:
I replaced where it says 'setValue' to 'value'
I replaced where it says 'update:value' to 'input'
Everything else says the same.
Then in subform.vue:
I replaced ':setValue.sync' to 'v-model'
v-model is two way binding, so I made use of that where it needed to be. The sync between the parent-child (not child to grandchild), is still using sync modifier, only because the parent has many props to pass. I could modify this where I could group up the props as a single object, and just pass that.

Setting focus of an input element in vue.js

I'm trying to set the focus of an input element in Vue.js. I found some help online but none of the explanation worked for me.
Here's my code :
<template>
<form method="post" action="" v-on:submit.prevent="search">
<input type="text" placeholder="Person name" required v-model="name" v-el="nameInput" />
<input type="text" placeholder="Company" required v-model="company" v-el="domainInput" />
<input type="submit" value="Search" class="btn show-m" />
</form>
</template>
<script>
export default {
data () {
return {
contacts: [],
name: null,
company: null
}
},
ready: {
// I tried the following :
this.$$.nameInput.focus();
this.$els.nameInput.focus();
// None of them worked !
}
methods: {
search: function (event) {
// ...
// I also would like to give the focus here, once the form has been submitted.
// And here also, this.$$ and this.$els doesn't work
},
}
}
</script>
I tried this.$$.nameInput.focus(); and this.$els.nameInput.focus(); for what I could find online to target the focus, but this.$$ is undefined, and this.$els is empty.
If that can help, I'm using vue.js v1.0.15
Thank you for your help.
In vue 2.x you can solve it with a directive.
Vue.directive('focus', {
inserted: function (el) {
el.focus()
}
})
Then you can use v-focus attribute on inputs and other elements:
<input v-focus>
Another solution using Vue 2.x and ref.
You can use the ref/$refs attribute to target your input and focus it.
In the example a simple method is used which can target the inputs using the ref attribute supplied to the inputs.
Then access the $refs property on your instance to get a reference to the DOM element.
<script>
export default {
// ...
mounted: function () {
this.focusInput('nameInput');
},
methods: {
// This is the method that focuses the element
focusInput: function ( inputRef ) {
// $refs is an object that holds the DOM references to your inputs
this.$refs[inputRef].focus();
},
search: function (event) {
this.focusInput('domainInput');
},
}
}
</script>
<template>
<form method="post" action="" v-on:submit.prevent="search">
<input type="text" placeholder="Person name" required v-model="name" ref="nameInput" />
<input type="text" placeholder="Company" required v-model="company" ref="domainInput" />
<input type="submit" value="Search" class="btn show-m" />
</form>
</template>
This solution is best for a one off situation or for a reusable component. For a more global approach the directive is the way to go.
Setting focus inside a child element
(for those of you that struggled for hours like me)
Parent:
<template>
<div #click="$refs.theComponent.$refs.theInput.focus()">
<custom-input ref="theComponent"/>
</div>
</template>
Child (CustomInput.vue):
<template>
<input ref="theInput"/>
</template>
There are a couple of issues.
First of all, v-els are defined like this:
<input v-el:input-element/>
That'll turn the variable to a camelCase in the code. You can read up more on this weird functionality here.
Other than that, you should be able to access the variable through this.$els.inputElement. Mind you, it will only appear in the component that you're defining that element (or the main app itself, if you defined it there).
Secondly, the automatic focusing does not seem to be working on Firefox (43.0.4), at least on my machine. Everything works great on Chrome, and focuses as expected.
Using ref I managed to focus an Input on mounted like this.
Template :
<b-form-input v-model="query" ref="searchInput" ></b-form-input>
Javascript :
mounted(){
this.$refs.searchInput.$el.focus()
}
Vue 3.x
Use a custom directive.
app.directive('focus', {
mounted(el) {
el.focus()
}
})
Here is how you use it:
Step 1:
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(router)
app.directive('focus', {
mounted(el) { // When the bound element is inserted into the DOM...
el.focus() // Focus the element
}
})
/* Optional:
Add a slight delay if the input does not focus.
app.directive('focus', {
mounted(el) { // When the bound element is inserted into the DOM...
setTimeout(() => {
el.focus() // Focus the element
}, 500)
}
}) */
await router.isReady()
app.mount('#app')
Then in your component:
Step 2:
// MyInput.vue
<input v-focus>
Vue docs
According to vue 2.x, you can also register "directive" locally in component to get autofocus.
just write directive in component:
export default {
directives: { focus: {
inserted: function (el) {
el.focus()
}
}
}
}
Then in a template, you can use the new v-focus attribute on any element, like this:
<input type="text" v-focus>

Categories

Resources