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.
Related
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.
In my example, i have two arrays.
First array - values, second - zeros for incremental counter.
Any new item have own counter button for him.
But it is not working, and i do not know why. If i push several buttons, i see chaotioc behavior in arrays
JSfiddle
How to repair it?
How to do counter function without button?
Example: if i loading page, i see 3 elements. Counters begining counting from 0. After 10 seconds i add new element. Old counters continue to work, but counter in new element start from 0.
new Vue({
el: '#page',
data: {
arr: [1, 2 ,3],
count: [0, 0 ,0]
},
methods: {
addEll: function() {
this.arr.push(this.arr.length + 1);
this.count.push(0);
},
incrementio: function(val) {
interval = setInterval(() => {
Vue.set(this.count, this.count[val], 0);
this.count[val]++;
}, 1000);
},
},
computed: {
visibleList: function(){
return this.arr;
}
}
})
<script src="https://unpkg.com/vue"></script>
<div id="page">
<button v-on:click="addEll">Add element</button>
{{ arr }}
{{ count }}
<ul>
<li v-for="(item, index) in visibleList">
{{item}}
<button v-on:click="incrementio(index)">Counter: {{count[index]}}</button>
</li>
</ul>
</div>
I don't clearly understand from question what your counter should do, but i think it needs to change these lines
Vue.set(this.count, this.count[val], 0);
this.count[val]++;
to this 1 line:
Vue.set(this.count, val, this.count[val]+1);
And I think you need to change setInterval to setTimeout.
Here is updated jsfiddle.
If I understand correctly you trying to count lifetime of each value. Here is my approach. Firstly I tied the value and its counter together in one object as I find it more efficient. Moving on, I defined interval property, because not doing so (The compiler automatically defines it in global scope) may cause unwanted behaviour and it is regarded as an error in strict mode. I also removed unnecessary visibleList computed property from your code. And last but not least I added clearInterval function in beforeDestroy hook as it is a good habit. (In your particular case it might be unnecessary to do so but if it was a component reused many times it would be very important to have it as it frees up the memory.)
new Vue({
el: '#page',
data: {
arr: [{
value: 1,
counter: 0
}],
interval: null
},
mounted () {
this.interval = setInterval(() => {
this.arr.map(x => x.counter++);
}, 1000);
},
beforeDestroy () {
clearInterval(this.interval);
},
methods: {
addEll () {
this.arr.push({
value: this.arr.length + 1,
counter: 0
});
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.min.js"></script>
<div id="page">
<button v-on:click="addEll">Add</button> {{ arr }}
<ul>
<li v-for="(item, index) in arr">
{{item.value}} Counter: {{item.counter}}
</li>
</ul>
</div>
As you can see in the Vuejs documentation Vuejs Caveats
Vue cannot detect the following changes to an array:
When you directly set an item with the index, e.g. vm.items[indexOfItem] = newValue
When you modify the length of the array, e.g. vm.items.length = newLength
To overcome caveat 1, both of the following will accomplish the same as vm.items[indexOfItem] = newValue, but will also trigger state updates in the reactivity system:
Vue.set(vm.items, indexOfItem, newValue)
The example would be like this:
new Vue({
el: '#page',
data: {
arr: [1, 2 ,3],
count: [0, 0 ,0]
},
methods: {
addEll: function() {
this.arr.push(this.arr.length + 1);
this.count.push(0);
},
incrementio: function(index) {
this.$set(this.count, index, this.count[index] + 1)
},
},
computed: {
visibleList: function(){
return this.arr;
}
}
})
The select2 component can be configured to accept new values, as ask in Select2 dropdown but allow new values by user?
And you can see it at: http://jsfiddle.net/pHSdP/646/ the code is as below:
$("#tags").select2({
createSearchChoice: function (term, data) {
if ($(data).filter(function () {
return this.text.localeCompare(term) === 0;
}).length === 0) {
return {
id: term,
text: term
};
}
},
multiple: false,
data: [{
id: 0,
text: 'story'
}, {
id: 1,
text: 'bug'
}, {
id: 2,
text: 'task'
}]
});
The problem is that the new value is only added to the list if you enter new value and press enter, or press tab.
Is it possible to set the select2 component to accept this new value when use types and leave the select2. (Just as normal html input tag which keeps the value which you are typing when you leave it by clicking some where on the screen)
I found that the select2 has select2-blur event but I don't find a way to get this new value and add it to list?!
Adding attribute selectOnBlur: true, seems to work for me.
Edit: glad it worked for you as well!
I am using Select2 4.0.3 and had to add two options:
tags: true,
selectOnBlur: true,
This worked for me
And to be able to submit multiple new choices together with the existing ones:
select2({tags: true, selectOnBlur: true, multiple: true})
I am working on a javascript control for work, and integrating it into a grails plugin. All the plugin simply does is populate the options based on a taglib, which looks like this:
$(document).ready(function() {
var ${name} = $("#${name}").tagInput({
$inputListener: $("#${inputListenerName}"),
$errorHandler: $("#${errorHandler}"),
$domainListener: $("#${domainListenerName}"),
errorClass: "${errorClass}",
validClass: "${validClass}",
caseSensitive: ${caseSensitive},
constraints: {
minSize: ${minSize},
maxSize: ${maxSize},
maxTags: ${maxTags},
validationRegex: "${tagRegex}"
},
errorMessages: {
size: "${sizeErrorMessage}",
regex: "${regexErrorMessage}",
maxTags: "${maxTagsError}"
},
responsive: {
length: ${maxTagLength},
lengthxs: ${maxTagLengthxs},
xsMode: ${xsWidth}
}
});
debugger;
});
Which looks like this when evaluated:
var emailTags = $("#emailTags").tagInput({
$inputListener: $("#invitesInput"),
$errorHandler: $("#inviteErrors"),
$domainListener: $("#null"),
errorClass: "label-danger",
validClass: "label-primary",
caseSensitive: false,
constraints: {
minSize: 1,
maxSize: 255,
maxTags: 100,
validationRegex: "[^#]+#[^#]+\.[^#]+"
},
errorMessages: {
size: "",
regex: "Must be a valid email string.",
maxTags: "You have entered too many recipients. Please send out invites before adding more recipients."
},
responsive: {
length: 50,
lengthxs: 20,
xsMode: 768
}
});
When Chrome hits the debugger statement, I have the correct object. Which is:
tagInput {parseTags: function, clear: function, serialize: function}
If I step out of this, I immediately entry jQuery and my object is instantly turned into
div#emailTags.turningTags
which eventually turns into
<div id="emailTags" name="emailTags" class="turningTags ">…</div>
If it helps, here is the current code for the tagInput object.
https://gist.github.com/anonymous/e785ec24e0c1388cd599
Why is this happening? Why is my object being turned into this HTML element? I have tried changing the name of the variable to no avail, no matter what I change the name of this variable to, it happens every time. I have tried making it a standalone object and not a jQuery function and the same thing STILL keeps happening.
I am running a weird problem when I try to set Grid Filter list dynamically.
Let me explain by my code snippets
I have a column with filter list is defined as
{
text : 'Client',
dataIndex : 'topAccount',
itemId : 'exTopAccount',
filter: {
type: 'list',
options:[]
}
}
I initialize list from store in 'viewready'
viewready: function(cmp,eOpts){
cmp.getHeaderCt().child('#exTopAccount').initialConfig.filter.options = clientsStore.collect('topAccount');
}
===> WORKS GOOD
Now, I have to build the new client store based on the records when user moves to next page. Therefore I build the store in the 'change' event of paging
listeners: {
'change' :function( toolbar, pageData, eOpts ) {
var store = Ext.StoreManager.get('ExceptionRecords');
clientsStore.removeAll(true);
store.each(function(record){
if(clientsStore.findRecord('topAccount',record.data.topAccount.trim()) == null ) {
clientsStore.add({topAccount: record.data.topAccount.trim()})
}
})
Ext.getCmp('exceptionGridContainer').view.refresh;
Ext.getCmp('exceptionGridContainer').view.getHeaderCt().doLayout;
console.log(clientsStore);
Ext.getCmp('exceptionGridContainer').view.getHeaderCt().child('#exTopAccount').initialConfig.filter.options = clientsStore.collect('topAccount');
}
}
I can now see the new data in clientsStore . But Grid filter list is not updated. still showing old data. I tried refresh,layout etc. Nothing helps
Any help will be appreciated
Thanks
Tharahan
Just changing the value of a property does not affect the component rendered or computed state. The menu is created when the list is first initialized. The first time you do that, it works because that's before the initialization, but the second time, that's too late.
If you can grab a reference to the instantiated ListFilter, I think you could force the recreation of the menu this way:
listFilter.menu = listFilter.createMenu({
options: [ ... ] // new options
// rest of the filter config
});
So, supposing you have a reference to your target grid, you could change the options for the column with dataIndex of "topAccount" by a call similar to this:
var listFilter = grid
.findFeature('filters') // access filters feature of the grid
.get('topAccount'); // access the filter for column
listFilter.menu = listFilter.createMenu({
options: [ ... ] // new options
// rest of the filter config
});
--- Edit ---
OK, complete example. Tested, working.
Ext.widget('grid', {
renderTo: Ext.getBody()
,height: 400
,features: [{
ftype: 'filters'
,local: true
}]
,columns: [{
dataIndex: 'a'
,text: 'Column A'
,filter: {
type: 'list'
,options: ['Foo', 'Bar']
}
},{
dataIndex: 'b'
,text: 'Column B'
},{
dataIndex: 'c'
,text: 'Column C'
}]
,store: {
fields: ['a', 'b', 'c']
,autoLoad: true
,proxy: {
type: 'memory'
,reader: 'array'
,data: [
['Foo', 1, 'Bar']
,['Bar', 2, 'Baz']
,['Baz', 1, 'Bar']
,['Bat', 2, 'Baz']
]
}
}
,tbar: [{
text: 'Change list options'
,handler: function() {
var grid = this.up('grid'),
// forget about getFeature, I read the doc and found something!
filterFeature = grid.filters,
colAFilter = filterFeature.getFilter('a');
// If the filter has never been used, it won't be available
if (!colAFilter) {
// someone commented that this is the way to initialize filter
filterFeature.view.headerCt.getMenu();
colAFilter = filterFeature.getFilter('a');
}
// ok, we've got the ref, now let's try to recreate the menu
colAFilter.menu = colAFilter.createMenu({
options: ['Baz', 'Bat']
});
}
}]
});
I was solving similar problem and answers to this question helped me a lot. Local List filter menu is in fact lazy loaded (only created when clicked) and I needed to set filter menu to be reloaded if the grid store has been reloaded with different data. Solved it by destroying of menu after each reload, so on next click menu is recreated:
var on_load = function() {
var grid_header = me.gridPanel.filters.view.headerCt
if (grid_header.menu) {
grid_header.menu.destroy();
grid_header.menu = null;
}
}