Computed prop in VueJS not updated until input lost focus - javascript

same as title says. A computed property from a object it´s not reactive until the input box lost the focus.
But if the object is in data, the value changes as soon as changed in the input.
I created a codepen with the example: https://codepen.io/Albvadi/pen/QWwMOQV
<div id="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">Match Component</div>
<div class="card-body">
<div class="container">
<div class="row align-items-center">
<div class="col-4 text-right">{{homeTeam.desc}}</div>
<div class="col-2">
<input class="form-control text-center" type="number" min="0" v-model.number="homeTeam.score" />
</div>
<div class="col-2">
<input class="form-control text-center" type="number" min="0" v-model.number="awayTeam.score" />
</div>
<div class="col-4">{{ awayTeam.desc }}</div>
<div class="row">
<div class="col">
DATA:
<b>{{ homeTeam.score }}</b>
</div>
<div class="row">
<div class="col">
COMPUTED:
<b>{{ awayTeam.score }}</b>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
new Vue({
el: "#container",
data: {
homeTeam: {
name: "SPA",
desc: "Spain",
score: 0
}
},
computed: {
awayTeam: function() {
return {
name: "GER",
desc: "Germany",
score: 0
};
},
},
mounted() {
console.log("Component mounted.");
}
});
If you change the first input, the data value is changed inmediate.
If you change the second input, the computed value if changed only when input lost focus.
How can I solve this?
== EDIT ==
Thanks for your answers!
I know that putting awayteam in the data section solves the problem, it was just an example to simplify my problem.
My problem was that along with the two results, I wanted to have a computed property to be able to issue to another component.
I edited the codepen with the final result: https://codepen.io/Albvadi/pen/jOELYwN and it´s working correctly.
But #Sami Kuhmonen comments that computed properties are only read properties but in my checkFinalResult I´m setting the computedMatch..
It´s the right approach??

Computed properties should be considered read only. Every time there's a change in the properties they reference their value is computed from other values and all changes are lost (or at least should be thought that way, there may be cases where some values remain). Even though sometimes changing their properties seems to work it's not guaranteed and they may change whenever.
In the codepen you already have the computed property referencing other properties from the object, so you'll have to add all the things you want to change similarly and then change that value. This will cause the computed value to be re-evaluated and the changes will be visible and persisted.
Note that building a computed object from a lot of separate things might not be the most performant way and it depends on the situation how it should be handled. If the values are only used within the component then it's easy to just use them directly but if you need an object of specific form for something and need it reactive then computed property might be the way to go.

In your case, why this computed property does not update DOM element instantly,because it has no data reference with Vue instance.Here, it return some static value only,which was not the computed property's purpose. computed property came to purpose,when you need to calculate or compute some decision on the basis of your data property of your vue instance.
new Vue({
el: "#container",
data: {
homeTeam: {
name: "SPA",
desc: "Spain",
score: 0
},
awayTeam: {
name: "GER",
desc: "Spain",
score: 0
},
},
mounted() {
console.log("Component mounted.");
}
});
<div id="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">Match Component</div>
<div class="card-body">
<div class="container">
<div class="row align-items-center">
<div class="col-4 text-right">{{homeTeam.desc}}</div>
<div class="col-2">
<input class="form-control text-center" type="number" min="0" v-model.number="homeTeam.score" />
</div>
<div class="col-2">
<input class="form-control text-center" type="number" min="0" v-model.number="awayTeam.score" />
</div>
<div class="col-4">{{ awayTeam.desc }}</div>
<div class="row">
<div class="col">
DATA:
<b>{{ homeTeam.score }}</b>
</div>
<div class="row">
<div class="col">
COMPUTED:
<b>{{ awayTeam.score }}</b>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>

Why do you use a computed property for this task? I would simply do it this way.
new Vue({
el: "#container",
data: {
homeTeam: {
name: "SPA",
desc: "Spain",
score: 0
},
awayTeam: {
name: "GER",
desc: "Germany",
score: 0
},
},
mounted() {
console.log("Component mounted.");
}
});

Related

VueJS - i can't hide readmore button before or after reach the selected limit data to show in vuejs

i'm using vuejs2. Try to hide the button before or after reach the amount of value, but i can't event get the data length to compare with limit. I try to re-assign it into an array, still not work cause the length is 1. Any idea how to do or different way to do that? Thanks
export default {
name: 'SlideEvents',
props: {
dataEvents: {
type: Array,
default () {
return []
}
}
},
data () {
return {
limit: 6
}
},
components: {
CardSlide
},
computed: {
dataFilter () {
if (this.dataEvents) {
return this.dataEvents.slice(0, this.limit)
} else {
return this.dataEvents
}
}
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div class="container-fluid more-top">
<div class="row">
<div class="col-md-12">
<div class="card box-simple border-0">
<h2>Agenda Terbaru</h2>
<div class="mt-5 mb-5">
<div class="row row-cols-1 row-cols-md-3 g-5 mt-2 px-4">
<CardSlide class="mb-4" v-for="each in dataFilter"
:key="each.id"
:content="each" />
</div>
<button #click="limit*=2">Show More</button>
</div>
</div>
</div>
</div>
</div>
To hide your button, use the v-if directive.
<button v-if="dataFilter && dataFilter.length < dataEvents.length" #click="limit*=2">Show More</button>
This will only show the button if there are events, and the filtered events are less than the total.
You can also use the v-show directive.

Vuejs - Radio input remain checked when a new question is rendered in mobile app

I have implemented a tricky solution to display some questions without using v-for loop. I need it to avoid that all the questions are rendered at the same time in a mobile android app, this will avoid scroll. The logics works fine but I've noticed that when the user select an answer and the next new question is rendered, the radio input to select the answer will be not resetted and the selected radio input will be equal to the previous choiced from the user. The problem will not occur if I use v-for to render the questions but as I wrote, this isn't what I want.
<div class="container-fluid bg-light vh-100" v-if="isLoaded">
<div class="row m-0">
<div class="col-12 card shadow p-0 mt-5">
<div class="card-header">
<h6 class="fw-bold">{{ questions[n].question }}</h6>
</div>
<div class="card-body">
<div class="form-check mb-3" v-for="(choice, index) in questions[n].choices" :key="index">
<input class="form-check-input" type="radio" :name="questions[n].questionIndex" :value="index" #change="checkAnswer(questions[n].questionIndex, index)" :disabled="answeredQuestions[n]">
<small class="form-check-label" for="">{{ index }}) {{ choice }}</small>
</div>
</div>
</div>
</div>
</div>
<div class="navbar bg-light fixed-bottom">
<div class="container-fluid">
<small :class="score">{{ currentScore }}</small>
</div>
</div>
export default {
name: 'Quiz',
data() {
return {
n: 0,
answeredQuestions: [],
currentScore: 0
}
},
mounted() {
front.on('questions', (data) => {
console.log(data)
this.$store.commit('quizQuestions', data);
this.$store.commit('contentLoaded', true);
});
front.on('checkResponse', (response) => {
console.log(response);
if( response.answerCheck ){
this.currentScore++;
}
this.showNext();
});
},
computed: {
isLoaded() {
return this.$store.getters.showLoader;
},
questions() {
return this.$store.getters.quiz;
},
score() {
return this.currentScore > 0 ? 'text-success' : 'text-muted';
}
},
methods: {
showPrevious() {
if( this.n !== 0 ){
this.n--
}
},
showNext() {
if( this.n < this.$store.getters.quiz.length ){
this.n++
}
},
checkAnswer(questionIndex, choice) {
this.answeredQuestions.push(true);
front.send('checkAnswer', {questionIndex: questionIndex, choice: choice});
}
}
}
I think that the problem is with the name attribute of the radio inputs but not sure of this. Any idea of how I can solve this?
Maybe you can try this. bind your questions[n].answer in your input value. Once you get a new question, if the answer haven't fill in before. It will auto become null.
<input class="form-check-input" type="radio" :name="questions[n].questionIndex" :value="questions[n].answer" #change="checkAnswer(questions[n].questionIndex, index)" :disabled="answeredQuestions[n]">

Reuse html template in Angular project

I have this html template file, range-details-dialog.tpl.html
<div class="modal-header clearfix text-left">
<h5>Update Range</h5>
</div>
<div class="modal-body">
<form name="form" role="form" class="ng-pristine ng-valid" novalidate ng-submit="updateRange()">
<div class="form-group-attached">
<div class="row">
<div class="col-sm-12">
<div class="form-group form-group-default input-group p-l-10 p-r-10" ng-class="{ 'has-error' : form.$invalid }">
<p ng-show="form.rangeDaily.$error.min" class="help-block">Daily range more than £5</p>
</div>
</div>
</div>
</div>
</form>
<div class="row">
<div class="col-sm-8"></div>
<div class="col-sm-4 m-t-10 sm-m-t-10">
<button type="button" class="btn btn-primary btn-block m-t-5"
ng-disabled="form.$invalid || promise" promise-btn="promise" ng-click="updateRange()">Update</button>
</div>
</div>
</div>
Then I want to have another file forced-range-details-dialog.tpl.html
These two files could be one file instead with dynamically populated placeholders.
These are the places were substitution would be needed:
<h5>Update Range</h5> would become <h5>Update Forced Range</h5>
<p ng-show="form.rangeDaily.$error.min" class="help-block">Daily range more than £5</p>
would become:
<p ng-show="form.forcedRangeDaily.$error.min" class="help-block">Forced Daily range more than £5</p>
ng-disabled="form.$invalid || promise" promise-btn="promise" ng-click="updateRange()">Update</button>
, ng-disabled="form.$invalid || promise" promise-btn="promise" ng-click="updateForcedRange()">Update</button>
Is there a way to avoid having two separate template files for the above? Could you please provide some examples, links, or pointers as to how that can be achieved?
Also, I see in the answers that a solution would be to add a boolean parameter inside the component and then call it twice. I am not sure how to call the component though. I have pasted my component below:
angular.module('app.investment.rangeDetails')
.component('pxForcedLimitAmount', {
templateUrl: '/assets/js/apps/range/range-details-dialog.tpl.html',
bindings: {
amount: '<',
isRequest: '<?',
requestedAt: '<?',
#Input() isForced: boolean //<<----I added this based on answers below
},
controller: [function () {
var ctrl = this;
ctrl.$onInit = function () {
ctrl.isRequest = ctrl.isRequest === true || false;
};
}],
});
Seems like only the placeholders need to change, so you can use a variable to decide what placeholder to display on the template. For example:
isForced: boolean;
ngOnInit() {
this.isForced = true; // decide how you want to toggle this
}
on the template:
<h5 *ngIf="!isForced">Update Range</h5>
<h5 *ngIf="isForced">Update Forced Range</h5>
and
<p *ngIf="!isForced" ng-show="form.rangeDaily.$error.min" class="help-block">
Daily range more than £5</p>
<p *ngIf="isForced" ng-show="form.forcedRangeDaily.$error.min" class="help-block">
Forced Daily range more than £5</p>
you can do the same for other tags as well.
From the comments, one way to "determine" the value for isForced is to introduce an input property to the component i.e.
#Input() isForced: boolean;
and invoke the component from elsewhere like:
<app-user [isForced]="true"></app-user>
You can use inputs.Write a component which takes input, and render it in html. then call this component in desired places with its selector
For events use output
See the doc https://angular.io/guide/inputs-outputs

How can I separate values from reusable checkbox in vuejs?

My question might not match to the problem that I am facing, So, I will explain it in detail here.
This is code for SeparateTechnology.vue. I have removed a few methods to make code shorter.
<template>
<div>
<div v-for="mss in ms" :key="mss.name">
<section class="container shadow-box techno-box" v-if="mss.name==selected">
<p>Review the software criteria below and ensure that you complete each section</p>
<h4>{{mss.name}}</h4>
<div class="row shadow-box">
<div class="col-sm-12 col-lg-6">
<p>Select all that apply</p>
<div class="dropdown">
<input
v-model.trim="inputValue"
class="dropdown-input"
type="text"
placeholder="Search for your Solution"
/>
<div class="dropdown-list" v-show="selected">
<div
v-show="itemVisible(item)"
v-for="item in mss.subMenu"
:key="item"
class="dropdown-item"
>
{{ item }}
<button
class="button button-mini button-dark button-rounded itemLabel"
#click="selectSolution(item,mss)"
>
<i class="icon-line-plus"></i>
</button>
</div>
</div>
</div>
</div>
<div class="col-sm-12 col-lg-6 rightList">
<div class="dropdown-list" v-show="selected" v-if="mss.selected_solutions!=''">
<div v-for="item in mss.selected_solutions" :key="item" class="dropdown-item">
{{ item }}
<button
class="button button-mini button-dark button-rounded deleteLabel"
#click="deleteSelectedItem(item,mss)"
>
<i class="icon-line-minus"></i>
</button>
</div>
</div>
<div v-else>
<div class="style-msg errormsg">
<div class="sb-msg">
<i class="icon-remove"></i>You have not selected any solutions.
</div>
</div>
</div>
<button
class="button button-mini"
#click="clearUserOptions(mss)"
v-if="mss.selected_solutions.length > 1"
>
<i class="icon-line-cross"></i>Clear all selection
</button>
</div>
<div style="padding:20px;"></div>
</div>
<div class="row">
<div class="col-sm-12 col-md-3 inputTitle">
<h5>Don't see it in the list above:</h5>
</div>
<input
class="col-sm-12 col-md-6"
type="text"
v-model="value"
#keydown.enter="getUserSolution(value,mss)"
placeholder="Enter your solution here.. "
/>
</div>
<div style="padding:20px;"></div>
<div class="row shadow-box">
<h5
class="col-sm-12"
>Identify how the software solution is leveraged within your organization</h5>
<div
v-for="item in mss.selected_solutions"
:key="item"
class="clearfix col-sm-12 col-md-6"
style="padding:20px"
>
<span v-if="mss.isDifferent=='campaign'">
<div class="card">
<h5 class="card-header">{{item}}</h5>
<CheckBox
:groups="campaignMangment"
name="campaignMangment"
:value="item"
classProp="col-sm-12"
#clicked-show-detail="clickedShowDetailModal"
/>
{{item}} and {{productSelected}}
</div>
</span>
</div>
</div>
<button class="btn btn-primary" #click="postUserDetails(mss)">Submit</button>
</section>
</div>
</div>
</template>
<script>
import CheckBox from "../../../components/checkbox/Checkbox";
export default {
name: "technology",
data() {
return {
usageValue: "",
value: "",
campainCheckedNames: [],
checked: "",
productSelected: [],
checkedValues: "",
exists: null,
inputValue: "",
campaignMangment: [
"Business to Customer (B2C)",
"Business to Business (B2B)",
"Customer to Customer (C2C)",
"Customer to Business (C2B)",
"E-Government",
"M-Commerce",
],
};
},
props: ["ms", "selected"],
//method to show all the solutions that contains the user
methods: {
clickedShowDetailModal: function (campainCheckedNames) {
console.log(campainCheckedNames);
this.productSelected.push(campainCheckedNames);
},
},
components: {
CheckBox,
},
};
</script>
This is CheckBox.vue
<template>
<div class="row col-mb-0">
<div
:class="classProp + ' checkbox-margin'"
v-for="singleSelector in groups"
:key="singleSelector"
>
<div>
<input
:id="singleSelector+groupId"
:value="singleSelector"
class="checkbox-style"
type="checkbox"
v-model="checkedValues"
#change="showDetailModal"
/>
<label :for="singleSelector +groupId" class="checkbox-style-3-label">{{singleSelector}}</label>
</div>
</div>
</div>
</template>
<script>
let groupId = 0;
export default {
props: {
groups: Array,
name: String,
classProp: String,
value: String,
},
data() {
return {
groupId: groupId++,
checkedValues: [],
inputs: {},
};
},
methods: {
showDetailModal: function (e) {
this.$set(this.inputs, this.value, e.target.value);
this.$emit("clicked-show-detail", this.inputs);
},
},
};
</script>
<style scoped>
.checkbox-margin {
margin-bottom: 10px;
}
</style>
Right Now, the line {{item}} and {{productSelected}} prints output like below as shown in screenshot
.
Problem: On every click/unclick of checkbox it adds an item to an array and not in the format I want. and if I select the checkbox on left only, it adds that item on right as well as shown in the screenshot above. It is due to the same array declaration, but I couldn't think more than that.
Expected Output: For every selected item, I want to print the list of selected checkboxes in an array format like below.
"selected_solutions": [{
"name": "Adobe Campaign",
"usage": [ "Business to Customer",...]
}, {
"name": "Marin Software",
"usage": ["M-Commerce",...]
}]
Even small hints or tips would be more than appreciated. I don't mind posting code on gist if required. Thank you.
With checkbox-components you want to emit "event.target.checked" likeso:
this.$emit('input', event.target.checked)
That way it'll behave like a checkbox and you can use a v-model on the parent:
<CheckBox v-model="item.checked" ...>
I do not use v-model inside the component and instead just bind :checked="value", but that might be up to preference. But using both :value and v-model could (should?) cause problems because v-model is actually a :value + emitter under the hood (or so I heard).
Anyway, here is my minimal reusable checkbox-wrapper:
<template>
<input type="checkbox" :checked="value" #change="handleChange" />
</template>
<script>
import Vue from 'vue'
export default Vue.extend({
name: 'MyCheckbox',
props: {
value: { type: Boolean, default: false }
},
methods: {
handleChange(event) {
this.$emit('input', event.target.checked)
}
}
})
</script>
After that is done you can just filter on checked items:
computed: {
checkedSolutions(){
return this.ms[0].selected_solutions
.filter( solution => solution.checked );
}
}

Understanding how to use pagination in Bootstrap-vue

I have written the following code:
<div>
<div id="app" class="container">
<div class="grid">
<article v-for="tool in tools">
<div class="title">
<h3>{{capitalizeFirstLetter(tool.name)}}</h3>
</div>
<div class="description">
{{tool.description}}
</div>
<br>
<div>
<toggle-button :value=tool.status color="#82C7EB" :width=100 :height=30 :sync="true" :labels="{checked: 'Following', unchecked: 'Unfollowing'}" #change="onChange(tool)"/>
</div>
</article>
</div>
</div>
</div>
Recently I started using Bootstrap-Vue. I'm trying to figure out how to add pagination on the bottom.
I'm not sure how aria-controls works. My goal is to have 9 blocks of tools on each page. How should I add the pagination so I could move to the next 9 blocks of tools?
Since it wasn't clear if you needed client-side pagination or serverside i made an example of both in the snippet.
For simplicity I've made it 3 per page, but you could change it to 9.
The first one gets the initial page on load, and then calls the API every time the page changes by using a watcher, that calls a method with the new page which then retrieves that place and replaces our old data with the new.
The second one loads all the data on page load, and instead slices the data array based on the per_page property, so that only the items for that page is shown.
For this I've used a computed property which automatically updates based on the properties used inside it.
Both paginations have aria-controls defined with the id of the container of our page elements. This is used to tell screen readers what elements the pagination changes.
The classes row, col-*, border, mx-auto, h2 and text-center is classes used for styling and layout and isn't part of the actual solution, so you can freely change or remove them.
new Vue({
el: '#app',
computed: {
pagination2CurrentItems() {
const startIndex = (this.pagination2.current_page - 1) * this.pagination2.per_page;
const endIndex = startIndex + this.pagination2.per_page;
return this.pagination2.items.slice(startIndex, endIndex)
}
},
created() {
this.getPagination1Data()
this.getPagination2Data()
},
filters: {
capitalizeFirstLetter(value) {
return value.charAt(0).toUpperCase() + value.slice(1)
}
},
data() {
return {
pagination1: {
items: [],
per_page: 3,
total_rows: 0,
current_page: 1
},
pagination2: {
items: [],
per_page: 3,
total_rows: 0,
current_page: 1
}
}
},
methods: {
getPagination1Data(page = 1) {
fetch(`https://reqres.in/api/unknown?page=${page}&per_page=3`)
.then((response) => {
return response.json();
})
.then((data) => {
this.pagination1.total_rows = data.total;
this.pagination1.items = data.data;
});
},
getPagination2Data() {
/*
This endpoint only has 12 items total,
so this will get all in one call
*/
fetch(`https://reqres.in/api/unknown?per_page=12`)
.then((response) => {
return response.json();
})
.then((data) => {
this.pagination2.total_rows = data.total;
this.pagination2.items = data.data;
});
}
},
watch: {
'pagination1.current_page'(newPage) {
this.getPagination1Data(newPage)
}
}
})
<script src="https://unpkg.com/vue#2.6.11/dist/vue.min.js"></script>
<script src="https://unpkg.com/bootstrap-vue#2.5.0/dist/bootstrap-vue.js"></script>
<link href="https://unpkg.com/bootstrap#4.4.1/dist/css/bootstrap.min.css" rel="stylesheet"/>
<link href="https://unpkg.com/bootstrap-vue#2.5.0/dist/bootstrap-vue.css" rel="stylesheet"/>
<div id="app" class="container">
<div id="tools_list_1" class="row">
<div class="col-12 h2 text-center">
Server pagination
</div>
<article class="col-3 mx-auto border" v-for="tool in pagination1.items">
<div class="title">
<h3>{{ tool.name | capitalizeFirstLetter }}</h3>
</div>
<div class="description">
{{ tool.pantone_value }}
</div>
</article>
</div>
<div class="row mt-2">
<div class="col-12">
<b-pagination
v-model="pagination1.current_page"
:per-page="pagination1.per_page"
:total-rows="pagination1.total_rows"
aria-controls="tools_list_1"
>
</b-pagination>
</div>
</div>
<div id="tools_list_2" class="row">
<div class="col-12 h2 text-center">
Client pagination
</div>
<article class="col-3 mx-auto border" v-for="tool in pagination2CurrentItems">
<div class="title">
<h3>{{ tool.name | capitalizeFirstLetter }}</h3>
</div>
<div class="description">
{{ tool.pantone_value }}
</div>
</article>
</div>
<div class="row mt-2">
<div class="col-12">
<b-pagination
v-model="pagination2.current_page"
:per-page="pagination2.per_page"
:total-rows="pagination2.total_rows"
aria-controls="tools_list_2"
>
</b-pagination>
</div>
</div>
</div>
If you are interested in server side pagination with url changes of query params (using vue-router in history mode) and you need browser history (back/forward) to work properly, you can check my answer to a similar question. I think it can be adapted to not use b-table.
Place pagination number in url Vuejs

Categories

Resources