I've ran into a bug that I can't seem to solve. I have a table with selectable rows. When a checkbox is checked, the amount column is summed up.
But, when the data in the table changes using a datepicker, the merchant total on the right and the amount in the selected checkboxes object do not update and reflect what is in the table.
Below are two images illustrating what's happening.
Here is a codepen: https://codepen.io/anon/pen/NmKBjm
Solved the issue but I believe there's a more optimal method using v-model but I just can't get it to work. I'm basically fetching the data and then re-assigning the selected object with the data.
this.volume = response.data;
this.total = this.volume.reduce(function(p, n) {
return p + parseFloat(n.amount);
}, 0);
let merchants = this.selected.map(m => m.merchantName);
let filterMerchants = this.volume.filter(e => e ? merchants.includes(e.merchantName) : null);
this.selected = filterMerchants;
this.onCheckboxChange();
And here is my code.
<v-data-table v-model="selected" id="transactions-volume-table" :headers="tableHeaders" :items="volume" item-key="merchantName" :loading="loading" :search="searchTable" hide-actions class="elevation-1">
<v-progress-linear slot="progress" color="blue" indeterminate></v-progress-linear>
<template v-slot:items="props">
<td><v-checkbox v-model="props.selected" #change="onCheckboxChange" primary hide-details></v-checkbox></td>
<td class="text-xs-left">{{ props.item.divisionName }}</td>
<td class="text-xs-left">{{ props.item.merchantName }}</td>
<td class="text-xs-left">£{{ props.item.amount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") }}</td>
</template>
</v-data-table>
data() {
return {
search: {
fromDate: new Date().toISOString().substr(0, 10),
toDate: new Date().toISOString().substr(0, 10)
},
fromDateModal: false,
toDateModal: false,
searchTable: '',
loading: true,
volume: [],
total: null,
merchantTotal: 0,
tableHeaders: [{ text: 'Select', sortable: false },
{ text: 'Division', value: 'divisionName', sortable: true },
{ text: 'Merchant', value: 'merchantName', sortable: true },
{ text: 'Amount (£)', value: 'amount', sortable: true }],
selected: []
}
}
onCheckboxChange() {
console.log(this.selected);
this.merchantTotal = this.selected.reduce(function(p, n) {
return p + parseFloat(n.amount);
}, 0);
}
You update the merchant total only if a checkbox changes. You should update the merchant total in case your dates change as well. Because the date change it does not trigger a checkbox to change event.
EDIT
No, because the checkbox change event only gets triggered when it changes(click on it or manually fired). Your merchantTotal isn't a computed property so it isn't acting on anything. You can try and make it computed, actually it's a computed action you are trying to mimic.
EDIT
Check this fiddle I created. Like your example everything is working around the selected rows. In cases the selected variable changes (after applied filters or toggle a checkbox on or of) it will re-computed the total. In my example I use products in stead of merchants, but in function it's doing the same.
EDIT
Are you able to add a unique identifier foreach merchant in the dataset? Or you can create a ID based on 2 rows (division and merchantname). Its the only way around this is calculate the data via the volume (based on the selected merchant id or unique identifier, like I illustrated). Another way to do it is when data changes you unselect the merchants, but this is not really user friendly.
I thought it was a Vue implementation problem, but I think the problem lies within the working of the datatable from Vuetify (in your situation).
Sorry, my bad. Reformulation.
Vue has a limitation in reactivity to detect changes in objects. That is the problem, you can not know when your volume array change because datepicker changes. You should use this.$set to detect the changes in objects (an array is an object).
Link: https://v2.vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats
This is a sample code:
<div id="app">
{{ message[0] }}
<button v-on:click="change">Change</button>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
message: [45,35,50]
},
methods: {
change: function () {
//this.$set(this.message, 0, 35);
message[0] = 35;
console.log(this.message);
}
}
});
</script>
uncomment the line commented and try, you will see the difference.
Related
I am trying to scroll down on new appeared element in vue:
I have chat messages where I have two arrays:
data() {
return {
activeChats: [],
openedChats: [],
maxOpened: math.round(window.innerWidth / 300),
}
}
When I get new message and the conversation is not in active and opened arrays I add to both and it appears on screen because I used v-for on both arrays.
This everything works but I am not sure how to scroll down on div when new chat appears, I tried using refs but had no luck:
<div class="chat" v-for="chat in openedChats" ref="chat-{chat.id}"></div>
Or even just chat testing with one chat opened..
And inside axios then() after success I said:
this.$refs.chat['-'+response.data.id].scrollTo(9999999,99999999);
or
this.$refs.chat['-'+response.data.id].scrollTop = 99999999;
or
this.$refs.chat.scrollTo(9999999,99999999);
or
this.$refs.chat.scrollTop = 99999999;
And neither worked...
Any help ? :D
Can it be done without additional library, I need no animatios just to appear at the bottom of the element...
Thanks
See this example:
https://jsfiddle.net/eywraw8t/430100/
Use "watch" (https://v2.vuejs.org/v2/guide/computed.html) to detect changes in the message array (either from ajax or simply a button like in the example).
Set the id (or you can use ref if you prefer) based on the index of the message.
Then scroll to the last element in your array (get the last one via array.length).
new Vue({
el: "#app",
data: {
messages: [
{ id: 1, text: 'message' },
],
},
watch:
{
messages: function() {
let id = this.messages.length;
//takes a bit for dom to actually update
setTimeout(function() {
document.getElementById('message-' + id).scrollIntoView();
}, 100);
},
},
methods: {
addMessage: function(){
let id = this.messages.length + 1;
this.messages.push({ id: id, text: 'message'});
}
}
})
<div id="app">
<button v-on:click="addMessage()" style="position:fixed">
Add message
</button>
<div class="message" v-for="message in messages" :id="'message-' + message.id">
{{message.text}} {{ message.id}}
</div>
</div>
This is very much related to my first question about nousliders here: How to update div in Meteor without helper? (this title was not well chosen, because it was not about avoiding helpers)
The answer provided by Jankapunkt works very well, for example I can have 4 sliders, and reordering works without loosing slider states like min/max like this:
now I want some of the elements to be non-sliders, for example change 1 to a dropdown:
but when I click the switch button, 1 slider dissapears (the one that moves to the dropdown spot), and I get an error in console:
Exception in template helper: Error: noUiSlider (11.1.0): create requires a single element, got: undefined
I don't understand why adding if/else makes any difference ... the slider helper is waiting for ready {{#if ready}}...{{/if}} so it should work ? anyone understand why it doesn't ? and how to fix it ?
template onCreated looks now like this:
Template.MyTemplate.onCreated(function() {
const sliders = [{
id: 'slider-a',
prio: 1,
type: "NumberRange",
options: {
start: [0, 100],
range: {
'min': [0],
'max': [100]
},
connect: true
}
}, {
id: 'slider-b',
prio: 2,
type: "NumberRange",
options: {
start: [0, 100],
range: {
'min': [0],
'max': [100]
},
connect: true
}
}, {
id: 'dropdown-c',
prio: 3,
type: "Dropdown"
}, {
id: 'slider-d',
prio: 4,
type: "NumberRange",
options: {
start: [0, 100],
range: {
'min': [0],
'max': [100]
},
connect: true
}
}, ]
const instance = this
instance.state = new ReactiveDict()
instance.state.set('values', {}) // mapping values by sliderId
instance.state.set('sliders', sliders)
})
and template now looks like this, there is an if else statement to show Dropdown or NumberRange (slider):
<template name="MyTemplate">
{{#each sliders}}
<div class="range">
<div>
<span>id:</span>
<span>{{this.id}}</span>
</div>
{{#if $eq type 'Dropdown'}}
<select id="{{this.id}}" style="width: 200px;">
<option value="">a</option>
<option value="">b</option>
<option value="">c</option>
</select>
{{else if $eq type 'NumberRange'}}
<div id="{{this.id}}">
{{#if ready}}{{slider this}}{{/if}}
</div>
{{/if}}
{{#with values this.id}}
<div>
<span>values: </span>
<span>{{this}}</span>
</div>
{{/with}}
</div>
{{/each}}
<button class="test">Switch Sliders</button>
</template>
First of all you should be aware of the situation:
You are mixing classic ui rendering (the sliders) with Blaze rendering (the dropdown). This will bring you a lot of design problems and the solution below is more a hack than a clean appraoch using Blaze's API
Since your components are not only sliders anymore, you should rename your variables. Otherwise it is hard for a foreign person to decode your variable's context.
Your dropdown has currently no values saved, switching the button resets the dropdown.
You can't destroy a noUiSlider on the dropdown when hitting the switch button, causing the error you described above.
Therefore I want to give you some advice on restructuring your code first.
1. Renaming variables
You can use your IDE's refactoring functionality to easily rename all your variable names. If you don't have such functionality in your IDE / editor I highly suggest you to start your search engine to get one.
Since you have more input types than sliders, you should use a more generic name, like inputs which indicates a broader range of possible types.
There should also be a value entry on your dropdown, do be able to restore the last selection state when re-rendering:
Template.MyTemplate.onCreated(function () {
const inputs = [{
id: 'slider-a',
prio: 1,
type: 'NumberRange',
options: {
start: [0, 100],
range: {
'min': [0],
'max': [100]
},
connect: true
}
}, {
id: 'slider-b',
prio: 2,
type: 'NumberRange',
options: {
start: [0, 100],
range: {
'min': [0],
'max': [100]
},
connect: true
}
}, {
id: 'dropdown-c',
prio: 3,
type: 'Dropdown',
value: '', // default none
}, {
id: 'slider-d',
prio: 4,
type: 'NumberRange',
options: {
start: [0, 100],
range: {
'min': [0],
'max': [100]
},
connect: true
}
},]
const instance = this
instance.state = new ReactiveDict()
instance.state.set('values', {}) // mapping values by sliderId
instance.state.set('inputs', inputs)
})
Now you also have to rename your helpers and the template helper calls:
<template name="MyTemplate">
{{#each inputs}}
...
{{/each}}
</template>
Template.MyTemplate.helpers({
inputs () {
return Template.instance().state.get('inputs')
},
...
})
2. Handle multiple input types on switch event
You also should rename your variables in the switch event. Furhtermore, you need to handle different input types here. Dropdowns have no .noUiSlider property and they also receive not an array but a string variable as value:
'click .test': function (event, templateInstance) {
let inputs = templateInstance.state.get('inputs')
const values = templateInstance.state.get('values')
// remove current rendered inputs
// and their events / prevent memory leak
inputs.forEach(input => {
if (input.type === 'Dropdown') {
// nothing to manually remove
// Blaze handles this for you
return
}
if (input.type === 'NumberRange') {
const target = templateInstance.$(`#${input.id}`).get(0)
if (target && target.noUiSlider) {
target.noUiSlider.off()
target.noUiSlider.destroy()
}
}
})
// assign current values as
// start values for the next newly rendered
// inputs
inputs = inputs.map(input => {
const currentValues = values[input.id]
if (!currentValues) {
return input
}
if (input.type === 'Dropdown') {
input.value = currentValues
}
if (input.type === 'NumberRange') {
input.options.start = currentValues.map(n => Number(n))
}
return input
}).reverse()
templateInstance.state.set('inputs', inputs)
},
3. Correct rendering / update display list
Now comes the problem of mixing Blaze rendering with classic DOM updates: until this point you will run into an error. This is mainly because now our createSliders function will expect a div element with a certain id at the place where the dropdown has been rendered before the switch has been pressed. It won't be there because the Blaze render invalidation will not be finished at this point.
Fixing this using autorun in onCreated or onRendered will easily increase complexity or even messes up your code. A simpler solution is to use a short timeout here:
Template.MyTemplate.helpers({
// ...
slider (source) {
const instance = Template.instance()
setTimeout(()=> {
createSliders(source.id, source.options, instance)
}, 50)
},
// ...
})
4. Bonus: saving state of dropdown
In order to save the state of the dropdown, you need to hook into it's change event. You therefore need to assign it a class to map the event independently of the id:
<select id="{{this.id}}" class="dropdown" style="width: 200px;">...</select>
For which now you can create an event:
'change .dropdown'(event, templateInstance) {
const $target = templateInstance.$(event.currentTarget)
const value = $target.val()
const targetId = $target.attr('id')
const valuesObj = templateInstance.state.get('values')
valuesObj[targetId] = value
templateInstance.state.set('values', valuesObj)
}
Now you have saved your current dropdown value but in order to restore it in the next render, you need to extend the options in the html:
<select id="{{this.id}}" class="dropdown" style="width: 200px;">
<option value="a" selected="{{#if $eq this.value 'a'}}selected{{/if}}">a</option>
<option value="b" selected="{{#if $eq this.value 'b'}}selected{{/if}}">b</option>
<option value="c" selected="{{#if $eq this.value 'c'}}selected{{/if}}">c</option>
</select>
This should now also display the last selected state of the dropdown.
Summary
You can use this pattern to include even more input components.
Be aware, that mixing Blaze rendering with traditional DOM manipulations can increase the complexity of your code a lot. Same applies for many other rendering systems / libraries / frameworks out there.
The setTimeout solution should be the last approach to be used when other approaches are even less feasible.
Variable and method naming should always represent their context. If the context changes -> rename / refactor variables and methods.
Please next time post the full code here again. Your other post might get updated or deleted and the full code won't be accessible here anymore, making it hard for others to find a good solution.
I know this is a simple change but I have not been able to achieve it despite researching and trying many things. And I am new to Knockout.
I have this select option of a list of objects Payors which has IsValueChecked boolean property.
<select name="InsuranceId" data-bind="options:Payors ,
optionsValue: 'Id',
optionsText: 'Text',
value:InsuranceId">
</select>
I want to create an alert if IsValueChecked is true, however the value that I am updating is InsuranceId. I am trying to achieve this by subscribing to InsuranceId.
vm.InsuranceId.subscribe(function (newValue) {
//doing something here
}
How do I write this logic?
Payors has to be an array or observableArray, with the options that you want to choose from.
When you subscribe to InsuranceId you will get the selected Id. Use this to filter through Payors.
vm.Payors = ko.observableArray([
{IsValueChecked : false, Id : 1, Text: 'False'},
{IsValueChecked : true, Id: 2, Text: 'True'}
]);
vm.InsuranceId.subscribe(function (newValue) {
var boolean = vm.Payors().find(function(payorObject){
if (newValue === payorObject.Id) {
return payorObject.IsValueChecked;
}
});
if (boolean) alert ("IsValueChecked is true");
}
I am using Jquery Chosen along with Vue. This is my Vue directive:
Vue.component("chosen-select", {
props: {
value: [String, Array],
multiple: Boolean
},
template: `<select :multiple="multiple"><slot></slot></select>`,
mounted() {
$(this.$el)
.val(this.value)
.chosen({ width: '100%' })
.on("change", e => this.$emit('input', $(this.$el).val()))
},
watch: {
value(val) {
$(this.$el).val(val).trigger('chosen:updated');
}
},
destroyed() {
$(this.$el).chosen('destroy');
}
});
And using it like this:
<chosen-select v-model="basicDetailsModel.stateID" v-validate="'required'" data-vv-as="state" :state="errors.has('stateID') ? 'invalid' : 'valid'" name="stateID">
<option :value="null">Please select an option</option>
<option v-for="(state, index) in states" :key="index" :value="state.sid">{{state.nm}}</option>
</chosen-select>
If the states are assigned static value it works fine as per expectation but if I fetch the states value dynamically the chosen is not updated with latest values. It stays with the initial values.
How would I fix this issue?
Edit: This one works. Do you think this is the right way?
Vue.component("chosen-select", {
data() {
return { observer: null }
},
props: {
value: [String, Array],
multiple: Boolean
},
template: `<select :multiple="multiple"><slot></slot></select>`,
mounted() {
// Create the observer (and what to do on changes...)
this.observer = new MutationObserver(function (mutations) {
$(this.$el).trigger("chosen:updated");
}.bind(this));
// Setup the observer
this.observer.observe(
$(this.$el)[0],
{ childList: true }
);
$(this.$el)
.val(this.value)
.chosen({ width: '100%' })
.on("change", e => this.$emit('input', $(this.$el).val()))
},
watch: {
value(val) {
$(this.$el).val(val);
}
},
destroyed() {
$(this.$el).chosen('destroy');
}
});
The easiest way to fix this issue is simply not to render the select until you have options to render using v-if.
<chosen-select v-if="states && states.length > 0" v-model="basicDetailsModel.stateID" v-validate="'required'" data-vv-as="state" :state="errors.has('stateID') ? 'invalid' : 'valid'" name="stateID">
You could also play around with emitting the chosen:updated event when the component is updated.
updated(){
$(this.$el).trigger("chosen:updated")
},
which works for multiple selects, but mysteriously not for single selects.
I am not sure how you are fetching the states dynamically, but if you're using jQuery to get them, then I think that is your problem. Vue doesn't get notified if non-Vue things (like jQuery) change anything.
Even if that's not the case, this is worth reading to see why jQuery and Vue don't get along.
Can you add how you are fetching them dynamically?
Also, consider using a Vue framework like Vuetify which has a pretty good select control and is totally in Vue.
I'm currently building an application using knockoutjs for the MVVM pattern, and Kendo Web for the controls.
I have somme issues with filtering/grouping the data in the kendo grid.
I need to have highly customizable rows, and so I choose to use row template according to this sample :
http://rniemeyer.github.io/knockout-kendo/web/Grid.html
I also need to have a two way binding with the grid, cause I need to add/remove/update items.
The grid :
<div data-bind="kendoGrid: {
data: LienActionIndicateurPourFicheCollection,
widget: indicateurWidget,
rowTemplate: 'indicateurRowTmpl',
useKOTemplates: true,
dataSource : {
schema: {
model: {
fields: {
Code: { type: 'string' },
Titre: { type: 'string' },
Note: { type: 'number' }
}
}
},
},
columns: [
{ title: '#', width: 30 },
{ field: 'Code', title: 'Code', width: 80 },
{ field: 'Titre', title: 'Titre', width: 150 },
{ field: 'Note', title: 'Note', width: 80 }]
}">
</div>
The row template :
<script id="indicateurRowTmpl" type="text/html">
<tr">
<td>
<button data-bind="visible: $root.isInEditMode, click: removeIndicateur"
class="common-button delete-button"></button>
</td>
<td data-bind='text: Code'></td>
<td data-bind='text: Titre'></td>
<td data-bind='text: Note'></td>
</tr>
</script>
When I'm using the grid, it works fine, expect when I use grouping/filtering : it's like the grid is using the observable objet instead of the value to perform the operations.
Example : When I'm grouping on 'Note' integer value :
To prevent that, I have replaced in columns definition "field: 'Note'" by "field: 'Note()'" : the grouping works fine now, since grid use the integer value instead of a function.
But the filtering remain impossible : the column filter menu has changed from number filter to string filter when I have make the 'Note()' change.
I suppose it's because the fields entry key 'Note' does not match the columns entry key 'Note()' anymore !
I've tried to replace 'Note' by 'Note()' in fields definition : does not work.
I've replace Note observable by a non observable variable in my item model : all is working fine, but i'm not enable to edit those values anymore, and I want to.
Thanks for your help !
EDIT : here a jsfiddle reproducting the bug : http://jsfiddle.net/camlaborde/htq45/1/
EDIT#2 here's the final solution, thanks to sroes : http://jsfiddle.net/camlaborde/htq45/7/
EDIT#3 final solution plus inline grid edition : http://jsfiddle.net/camlaborde/8aR8T/4/
It works if you create a computed which returns the items as a plain JS object:
this.items.asJS = ko.computed(function() {
return ko.toJS(this.items());
}, this);
http://jsfiddle.net/htq45/2/
The reason why putting ko.toJS(this.items) directly in the binding doesn't work is because the way kendo tracks individual options in the bindings. Knockout.js man RP Niemeyer taught me this: Dynamically enable/disable kendo datepicker with Knockout-Kendo.js
I solved this issue by using Knockout ES5. Then, when assigning my data to my model, I used knockout-mapping with a mapping object like this:
var dataMapper = {
create: function(o) {
return ko.track(o.data), o.data;
}
};
ko.mapping.fromJS(json, dataMapper, self.data);
This makes the filtering and sorting work out of the box for the knockout kendo grid.