Updating a computed property (Form) in Vue on error? - javascript

Okay so I followed along with the entire Vue Laracasts videos, which ends up having a Form and an Errors class. Here's a github link to the repo from the video:
https://github.com/laracasts/Vue-Forms/blob/master/public/js/app.js
Now what I'm trying to do is integrate Vuex to keep track of my currently logged in user, prefill a form with the current values, and show errors if any of the Laravel validation fails.
The problem I seem to be running into is that I have to set my authUser as a computed property, to pull it from the Vuex store. Then since I can't use these in anything set in data I had to move my form to a computed property. Now that it's a computed property it never gets updated so my form never has errors even when errors are being thrown.
Here's my route file Account.vue
<template>
<div>
<form #submit.prevent="onSubmit" #keydown="form.errors.clear($event.target.name)">
<div class="row">
<div class="form-group col-md-6 col-lg-3">
<label>Name</label>
<input type="text" class="form-control" name="user" autocomplete="false" v-model="form.name">
<span class="help is-danger" v-if="form.errors.has('name')" v-text="form.errors.get('name')"></span>
</div>
<div class="form-group col-md-6 col-lg-3">
<label>Email address</label>
<input type="email" class="form-control" placeholder="Enter email" autocomplete="false" name="email" v-model="form.email">
<span class="help is-danger" v-if="form.errors.has('email')" v-text="form.errors.get('email')"></span>
</div>
<div class="form-group col-lg-3">
<label>Password</label>
<input type="password" class="form-control" placeholder="Password" autocomplete="false" name="password" v-model="form.password">
<span class="help is-danger" v-if="form.errors.has('password')" v-text="form.errors.get('password')"></span>
</div>
</div>
<div class="col-12">
<button type="submit" class="btn btn-primary">Submit</button>
</div>
</form>
</div>
</template>
<script>
export default {
computed: {
authUserName: {
get: function() {
return this.$store.getters.getAuthUser.name;
},
set: function(value) {
this.$store.commit('setAuthUserValue', { key: 'name', value })
}
},
authUserEmail: {
get: function() {
return this.$store.getters.getAuthUser.email;
},
set: function(value) {
this.$store.commit('setAuthUserValue', { key: 'email', value })
}
},
form() {
return new Form({
name: this.authUserName,
email: this.authUserEmail,
password: ''
})
}
},
data() {
return {
}
},
mounted() {
},
methods: {
onSubmit() {
this.form
.patch('/current-user')
.then(status => {
this.$emit('success', status);
});
}
}
}
</script>
index.js
import AuthUser from "../models/AuthUser";
export default new Vuex.Store({
state: { // = data
authUser: {
name: null,
email: null,
}
},
getters: { // = computed properties
getAuthUser(state, getters) {
return state.authUser;
}
},
actions: { // = methods
fetchAuthUser(context) {
AuthUser.load(user => {
context.commit('setAuthUser', user);
});
}
},
mutations: {
setAuthUser(state, user) {
Vue.set(state, 'authUser', user);
},
setAuthUserValue(state, payload) {
state.authUser = { ...state.authUser, payload }
}
}
});
app.js
import router from './routes';
import Hamburger from './components/Hamburger';
import NavHeader from './components/NavHeader';
import store from "./store/index";
new Vue({
el: '#app',
components: {
Hamburger,
NavHeader
},
data() {
return {
}
},
created() {
store.dispatch('fetchAuthUser');
},
router,
store
});
I've tried messing around with watchers, but everything threw errors. I'm trying really hard to get the form to be created with the existing users values preset, and was recommended to try vuex, but am now running into these issues where form now needs to be computed instead of set in data, so form never has errors now. Sorry, pulling my hair out learning all these new things at once lol.
EDIT
I've now also tried setting form to an empty form object in data, then setting the properties in mounted(), but no matter how I try to access authUser in mounted I'm getting undefined.
mounted() {
console.log(this.authUser);
console.log(this.authUser.name);
console.log(this.$store.getters.getUser.name);
},
outputs...
{ob: Observer}
undefined
undefined
EDIT 2
Latest code updated, still not getting the errors to show up below the input, although they do show up under the dev tools in..
Root > Account > computed > form > errors > errors
In there it lists name and email errors if I left them blank. Could it be cause it's double nested? I know this is a Laravel issue with how it nests the json response.
Perhaps something in that Form or Errors class in app.js?

AuthUser.load is an asynchronous function, as you're executing it in the created hook, it's not guaranteed that authUsers values will be set by the time that the Account.vue's created hook is fired.
Now the issue that you're having is due to the fact that you have a computed property with no setter. The Form class attempts to mutate the value of the store directly, which will result in the console exploding.
If you provide a setter for your value, when you attempt to write to it, you can react to the value being passed to the setter and you can dispatch (or in your case, commit) that value to the store:
computed: {
authUser: {
get(): {
return this.$store.getters.getUser;
},
set(value): {
this.$store.commit('setUser', value)
}
}
}
But wait, this still won't work! Why? Because the value you will receive will just be the text input value, how will you know which property within the AuthUser to update? Probably the easiest way to resolve that is to create a computed property for each property on the authUser
computed: {
authUserName: {
get(): {
return store.getters.getUser.name;
},
set(value): {
this.$store.commit('setUser', { key: 'name', value })
}
},
authUserEmail: {
get(): {
return store.getters.getUser.email;
},
set(value): {
this.$store.commit('setUser', { key: 'email', value })
}
},
authUserPassword: {
get(): {
return store.getters.getUser.password;
},
set(value): {
this.$store.commit('setUser', { key: 'password', value })
}
}
}
Now you can instantiate the form with each of the getters:
new Form({
name: this.authUserName,
email: this.authUserEmail,
password: this.authUserPassword
})
But, yet again, this still won't work! We need a new mutation that will only write properties of our authUser
setUserValue(state, payload) {
state.authUser = { ...state.authUser, payload }
}
By using a simple destructure we can overwrite the properties of our authUser and merge in the new value of the computed property.
Now you need to update your computed properties and use this.$store.commit('setUserValue', { key: 'name/email/password': value}) in your setter instead.
Finally, your authUser should be an object with preset properties:
authUser: {
name: null,
email: null,
password: null
}
This is important as properties of an object are reactive. However, if properties are added to the object, you cannot simply overwrite the object and have new reactive properties. You would need to use Vue.set() for this, such as:
setUser(state, user) {
Vue.set(state, 'authUser', user)
}
This would correctly allow for the observed properties to become reactive. This is an important concept in the reactivness of the VueX store. This is also the reason why your values are undefined.

Related

Vue: Radio does not update its value when data changes asynchronously

I do not understand why my new code does not work. I was able to extract a minimum reproducible case. When the created() sets a data synchronously, it works well and an article radio is displayed. When I surround it with timeout, then the blog stays selected. Vue 2.6.12
The bug in this code has been fixed, but this was not the cause for my troubles because my real code is different. My problem is that the radio button is not checked when it should be after the reactive data is changed.
<Radio
v-model="type"
identifier="article"
class="pl-3"
label="article"
name="type"
/>
<Radio
v-model="type"
identifier="blog"
class="pl-3"
label="blog"
name="type"
/>
<div>Selected {{ type }}</div>
data() {
return {
type: "blog",
};
},
created() {
setTimeout(function () {
this.type = "article";
console.log(this.type);
}, 800);
},
This makes my head explode because a similar code in different component works well.
UPDATE:
my original code, that does not work, is
computed: {
blog() {
return this.$store.getters.BLOG;
},
},
watch: {
blog() {
this.type = (this.blog.info.editorial) ? 'article' : 'blog';
},
created() {
this.$store.dispatch('FETCH_BLOG', { slug: this.slug });
},
Relevant source code:
https://github.com/literakl/mezinamiridici/blob/234_editorial_team/spa/src/views/item/WriteBlog.vue
https://github.com/literakl/mezinamiridici/blob/234_editorial_team/spa/src/components/atoms/Radio.vue
https://github.com/literakl/mezinamiridici/blob/234_editorial_team/spa/src/modules/vuex/items.js
All you need is to change your function to an arrow function because it isn't point your data like this
setTimeout(() => {
this.type = "article";
console.log(this.type);
}, 800);
The problem is the selected property in Radio.vue is only set equal to value in the created() hook. When the setTimeout() occurs in the parent component, Radio.vue's v-model property is changed, which updates its value property, but its selected property is not automatically updated to match.
The solution is to replace the created() hook change with a watcher on value that updates selected:
// Radio.vue
export default {
created() {
// ⛔️ Remove this
//if (this.value) {
// this.selected = this.value
//}
},
watch: {
value: {
handler(value) {
this.selected = value
},
immediate: true,
},
},
}
demo
I assume your original code does not set the type in vue's data function, so it will not reactive when you assign this.type to a new value.
Manage state in a form is complicated, check out this library: https://github.com/vue-formily/formily and maybe it helps you easier to work with form, it will let you separate the form definition from vue component that makes it reusable, and it will manage the state for you...
Here is a small demo for your problem: https://codepen.io/hqnan/pen/YzQbxxo

How to prevent user from submitting text that already exists in a remote api?

I am creating a VueJS app that contains a list of names called divisions. The user can submit a new name for a division and can also update a division name. The names of the divisions are received from a remote api and any edits made are then also sent to the api via a PUT request. This works well.
However, the problem is how can I prevent a user from submitting a division name that already exists?
I have a parent component (named Divisions.vue) that contains a GET request like so:
methods: {
async getAllDivisions() {
try {
this.divisions = (await ApiService.getAllDivisions()).data
} catch (error) {
console.error(error)
}
}
},
Here is how I have my code set up in a file called DivisionEdit.vue
Template HTML:
<form #submit.prevent="onSubmitUpdate">
Division Name:
<input type="text" v-model="division.division" />
<button type="submit">
Update Division
</button>
</form>
Script section:
data() {
return {
division: {
division: '',
division_id: null
},
methods: {
onSubmitUpdate() {
ApiService.updateDivision(this.division)
}
}
And I have the api service code like so in apiService.js:
updateDivision(division) {
return this.getApiClient().put('/Divisions', division)
}
You already have all the divisions in the parent component, you can pass that as a props to child component
And in the child component before onSubmitUpdate, you can have two approaches here
1) you can disable the update button by default, and have validation
for the input division by adding #input event -> check division if
already exist, if not enable the button
<form #submit.prevent="onSubmitUpdate">
Division Name:
<input type="text" v-model="division.division" #input="divisionExists" />
<button type="submit" :disabled="btnDisable">
Update Division
</button>
</form>
In Script:
props: {
divisions: Object,
},
data() {
return {
division: {
division: '',
division_id: null
},
btnDisable: true,
}
}
methods: {
divisionExists() {
if (this.divisions.map(x => x.division).includes(this.division.division)){
this.btnDisable = true
} else {
this.btnDisable = false;
}
},
onSubmitUpdate() {
ApiService.updateDivision(this.division)
}
}
2) You can dirrectly add a condition in the onSubmitUpdate method to
check if the edit division is already exist it will not trigger update
api
onSubmitUpdate() {
if (!this.divisions.map(x => x.division).includes(this.division.division)){
ApiService.updateDivision(this.division)
}
}

Laravel 5: show comments using Vue.js

I am creating blog commenting system, I want to show comments for a post using vue.js.
In console, it says
Property or method "comment" is not defined on the instance but
referenced during render.
Also, when I try to catch user name, I got this error
Error in render: "TypeError: Cannot read property 'user' of undefined"
I want to show comments and users who commented to a particular post
in show.blade.php.
web.php
Route::get('results/{post}', 'ResultsController#show')->name('posts.show');
ResultsController
public function show(Post $post)
{
$recommended_posts = Post::latest()
->whereDate('date','>',date('Y-m-d'))
->where('category_id','=',$post->category_id)
->where('id','!=',$post->id)
->limit(7)
->get();
$posts['particular_post'] = $post;
$posts['recommended_posts'] = $recommended_posts;
//return $post->comments()->paginate(5); it returns objects
return view('posts.show',compact('posts'));
}
Comments.vue
<div class="reply-comment" :v-for="comment in comments">
<div class="user-comment" >
<div class="user">
<!--<img src="" alt="" >-->
<avatar :username="comment.user.name" :size="30" ></avatar>
</div>
<div class="user-name">
<span class="comment-name">{{ comment.user.name }}</span>
<p> {{ comment.body }} </p>
</div>
</div>
<div class="reply">
<div class="seemorecomments">
see more
</div>
<button class="reply-button">
<i class="fas fa-reply"></i>
</button>
</div>
</div>
<script>
import Avatar from 'vue-avatar'
export default {
props: ['post'],
components: {
Avatar
},
mounted() {
this.fetchComments()
},
data: () => ({
comments: {
data: []
}
}),
methods: {
fetchComments() {
axios.get(`/results/${this.post.id}`).then(({ data }) => {
this.comments = data
})
}
}
}
show.blade.php
<comments-component :post="{{ $posts['particular_post']->comments }}"></comments-component>
migration table
Schema::create('comments', function (Blueprint $table) {
$table->bigIncrements('id');
$table->integer('user_id');
$table->integer('post_id');
$table->text('body');
$table->integer('comment_id')->nullable();
$table->timestamps();
});
comment.php, I have this.
protected $with = ['user'];
You have a couple of minor issues with your Vue file that can be addressed pretty quickly.
First, you should define comments as an empty array — a collection will be returned as an array of objects to the Vue. By adding an unnecessary data property in the beginning, you are allowing the v-for loop to run in your template before the data has been retrieved.
EDIT: I'm not sure about the way you wrote this data function, so I have re-written it a way in which I'm familiar.
data() {
return {
comments: []
}
},
Second, you want to get the correct data from the response. Axios data is stored another level deep (response.data). Of course, if you are paginating the results, they are one more level deep (response.data.data).
fetchComments() {
axios.get(`/results/${this.post.id}`).then(response => {
this.comments = response.data
// or for paginated results
// this.comments = response.data.data
})
}
EDIT: Thank you for providing the Gist! I think I'm seeing things more clearly now.
Update your controller like so:
You want to load the comments into the post here.
public function show(Post $post)
{
$recommended_posts = Post::latest()
->whereDate('date','>',date('Y-m-d'))
->where('category_id','=',$post->category_id)
->where('id','!=',$post->id)
->limit(7)
->get();
// load the post comments here
$post->load('comments');
$posts['particular_post'] = $post;
$posts['recommended_posts'] = $recommended_posts;
return view('posts.show',compact('posts'));
}
And you blade like so:
Your module wants a single post, not an array of comments.
<comments-component :post="{{ $posts['particular_post'] }}"></comments-component>
And you Vue like so:
You don't actually need to use Axios at all since we've already loaded the comments.
<script>
import Avatar from 'vue-avatar'
export default {
props: ['post'],
components: {
Avatar
},
data() {
return {
comments: this.post.comments
}
},
}
</script>

Passing Parameter in Vue.js

I am trying to pass a parameter in my Vue.js project, but having no luck. The goal is when I select a user from a list, it should route to that specific user's profile as localhost:61601/Profile/"lastName"
Here is my method when I click next to the users name:
editItem(lastName) {
this.$router.push({ name: 'GetInquiry', params: { lastName: lastName } })
this.$http.get('http://localhost:61601/api/' + 'GetInquiry/' + { lastName : lastName })
},
async GetAllInquiries() {
this.loading = true
try {
this.records = await api.GetAllInquiries()
} finally {
this.loading = false
}
},
As of right now, I am just passing the lastName, but eventually, it will pass a unique id.
Here is the test Profile Page, Here I am just trying to get that users information to display to test the page. So if everything works, the users firstName should display:
<template>
<div class="Profile">
<div class="container">
<div class="row">
<template slot="items" slot-scope="records">
<h1> Student Name: {{ records.items.firstName }}</h1>
</template>
</div>
</div>
<script>
import api from '../../store/api.js'
export default {
data() {
return {
records: {},
}
},
async created() {
this.GetInquiriesByUser()
},
methods: {
async GetInquiriesByUser() {
this.loading = true
try {
this.records = await api.GetInquiriesByUser()
} finally {
this.loading = false
}
},
}
}
</script>
Here is my routes.js
{
path: '/Profile/:lastName',
name: 'GetInquiry',
component: Profile
}
When i open dev tools on chrome, I get
localhost:61601/api/GetInquiry/[object%20Object]
Im using .netcore api for the backend, which gets results as expected, just cannot seem to get it up on my frontend.
If someone can help me and point me to the right direction that would be awesome. Please do let me know if anyone needs more details.
You are passing an object on the vue-resource instead of the value.
Just pass directly the lastname into the route and it should work as fine.
this.$http.get(`http://localhost:61601/api/GetInquiry/${lastName}`);

VueJS throws errors because some datas are not ready yet

I'm rather new to VueJS, and I would like to add a picture loaded from an API as a background image. However, the image loading eventually works.
Here is the code:
<template>
<div id="bio" :style="{ backgroundImage: 'url(' + this.settings.bio_bg.url +')' }">
<h1>Biography</h1>
<router-link to="/">Home</router-link><br />
<span>Biography</span><br />
<router-link to="/shop">Shop</router-link><br />
<router-link to="/contact">Contact</router-link><br />
</div>
</template>
<style scoped>
</style>
<script>
export default {
data () {
return {
settings: {},
bio: {}
}
},
created () {
.catch(error => this.setError(error))
this.$http.secured.get('/settings')
.then(response => {
this.settings = response.data
console.log(this.settings)
})
.catch(error => this.setError(error))
}
}
</script>
The image is loaded, but my console returns two errors:
Error in render: "TypeError: Cannot read property 'url' of undefined"
Cannot read property 'url' of undefined
I guess that since the Axios call is asynchronous, everything arrives after the page is done loading, but is still loaded after.
What would the proper way be to correctly wait for data to be available? I tried a few things that I know from React, but it doesn't load at all (even though the errors stop showing up)
Thank you in advance
Yo need to be sure that this.settings.bio_bg.url exist from the component birth, so compiler doesn't broke trying to render it. In order to do so, just setup a 'fake' url in the original data of the component:
export default {
data () {
return {
settings: {
bio_bg: {
url: '',
}
},
bio: {}
}
},
created () {
this.$http.secured.get('/settings')
.then(response => {
this.settings = response.data
console.log(this.settings)
})
.catch(error => this.setError(error))
}
}
This not only prevent errors, also provides better intellisense since now your code editor can now that settings has a bio_bg member which in turn has an url.
If instead of ' ', you provide a real image url with a placeholder img, probably the UI will be nicer:
data () {
return {
settings: {
bio_bg: {
url: 'http://via.placeholder.com/350x150',
}
},
bio: {}
}
}

Categories