Ember multiple property changes but want to trigger single event - properties

I have a ArrayController for date picker with properties to and from. Now when user selects a new date range from the UI both to and from value changes.
So a function which observers on to and from is trigger twice for a single change in date range.
I want to trigger the function only one every date range change. Any suggestions how to do that with ember
app = Ember.Application.create();
app.Time = Ember.ArrayController.create({
to: null,
from: null,
rootElement: "#time",
debug1: function() {
this.set('to', 1);
this.set('from', 2);
},
debug2: function() {
this.setProperties( {
'to': 3,
'from': 4
});
},
debug3: function() {
this.beginPropertyChanges();
this.set('to', 5);
this.set('from', 6);
this.endPropertyChanges();
},
search: function() {
alert('called');
}.observes('to', 'from')
});
View in JS Fiddle

Perhaps you can introduce a computed property on from and to, and then plug the observer on this property. As CPs are cached by default, I think the observer will be triggered only once.
date: function(){
// return the computed date
}.property('from', 'to')
dateDidChange: function(){
}.observes('date')
EDIT Thanks to your fiddle, it seems to work using setProperties, or begin/end propertyChanges http://jsfiddle.net/Sly7/zf2q3/1/

You can wrap observer in a Em.run.once
_dateDidChange: function(){
Ember.run.once(this, 'dateDidChange');
}.observes('from', 'to'),
dateDidChange: function() {
// should be called only once
}

Related

Vuejs - update array of an object which is in an array

I'm developing a helpdesk tool in which I have a kanban view.
I previously used nested serializers in my backend and I managed to have everything working with a single query but it's not scalable (and it was ugly) so I switched to another schema :
I query my helpdesk team ('test' in the screenshot)
I query the stages of that team ('new', 'in progress')
I query tickets for each stage in stages
So when I mount my component, I do the following :
async mounted () {
if (this.helpdeskTeamId) {
await this.getTeam(this.helpdeskTeamId)
if (this.team) {
await this.getTeamStages(this.helpdeskTeamId)
if (this.stages) {
for (let stage of this.stages) {
await this.getStageTickets(stage)
}
}
}
}
},
where getTeam, getTeamStages and getStageTickets are :
async getTeam (teamId) {
this.team = await HelpdeskTeamService.getTeam(teamId)
},
async getTeamStages (teamId) {
this.stages = await HelpdeskTeamService.getTeamStages(teamId)
for (let stage of this.stages) {
this.$set(stage, 'tickets', [])
}
},
async getStageTickets (stage) {
const tickets = await HelpdeskTeamService.getTeamStageTickets(this.helpdeskTeamId, stage.id)
// tried many things here below but nothing worked.
// stage.tickets = stage.tickets.splice(0, 0, tickets)
// Even if I try to only put one :
// this.$set(this.stages[this.stages.indexOf(stage)].tickets, 0, tickets[0])
// I see it in the data but It doesn't appear in the view...
// Even replacing the whole stage with its tickets :
// stage.tickets = tickets
// this.stages.splice(this.stages.indexOf(stage), 1, stage)
},
In getTeamStages I add an attribute 'tickets' to every stage to an empty list. The problem is when I query all the tickets for every stage. I know how to insert a single object in an array with splice or how to delete one object from an array but I don't know how to assign a whole array to an attribute of an object that is in an array while triggering the Vue reactivity. Here I'd like to put all the tickets (which is a list), to stage.tickets.
Is it possible to achieve this ?
If not, what is the correct design to achieve something similar ?
Thanks in advance !
EDIT:
It turns out that there was an error generated by the template part. I didn't think it was the root cause since a part of the view was rendered. I thought that it would have prevent the whole view from being rendered if it was the case. But finally, in my template I had a part doing stage.tickets.length which was working when using a single query to populate my view. When making my API more granular and querying tickets independently from stages, there is a moment when stage has no tickets attribute until I set it manually with this.$set(stage, 'tickets', []). Because of that, the template stops rendering and raises an issue. But the ways of updating my stage.tickets would have worked without that template issue.
I could update the stages reactively. Here is my full code; I used the push method of an array object and it works:
<template>
<div>
<li v-for="item in stages" :key="item.stageId">
{{ item }}
</li>
</div>
</template>
<script>
export default {
data() {
return {
stages: [],
};
},
methods: {
async getTeamStages() {
this.stages = [{ stageId: 1 }, { stageId: 2 }];
for (let stage of this.stages) {
this.$set(stage, "tickets", []);
}
for (let stage of this.stages) {
await this.getStageTickets(stage);
}
},
async getStageTickets(stage) {
const tickets = ["a", "b", "c"];
for (let ticket of tickets) {
this.stages[this.stages.indexOf(stage)].tickets.push(ticket);
}
},
},
mounted() {
this.getTeamStages();
},
};
</script>
It should be noted that I used the concat method of an array object and also works:
this.stages[this.stages.indexOf(stage)].tickets = this.stages[this.stages.indexOf(stage)].tickets.concat(tickets);
I tried your approaches some of them work correctly:
NOT WORKED
this.$set(this.stages[this.stages.indexOf(stage)].tickets, tickets)
WORKED
this.$set(this.stages[this.stages.indexOf(stage)].tickets, 0, tickets[0]);
WORKED
stage.tickets = tickets
this.stages.splice(this.stages.indexOf(stage), 1, stage)
I'm sure it is XY problem..
A possible solution would be to watch the selected team and load the values from there. You seem to be loading everything from the mounted() hook, and I suspect this won't actually load all the content on demand as you'd expect.
I managed to make it work here without needing to resort to $set magic, just the pure old traditional vue magic. Vue will notice the properties of new objects and automatically make then reactive, so if you assign to them later, everything will respond accordingly.
My setup was something like this (showing just the relevant parts) -- typing from memory here, beware of typos:
data(){
teams: [],
teamId: null,
team: null
},
watch:{
teamId(v){
this.refreshTeam(v)
}
},
methods: {
async refreshTeam(id){
let team = await fetchTeam(id)
if(!team) return
//here, vue will auomaticlly make this.team.stages reactive
this.team = {stages:[], ...team}
let stages = await fetchStages(team.id)
if(!stages) return
//since this.team.stages is reactive, vue will update reactivelly
//turning the {tickets} property of each stage reactive also
this.team.stages = stages.map(v => ({tickets:[], ...v}))
for(let stage of this.team.stages){
let tickets = await fetchTickets(stage.id)
if(!tickets) continue
//since tickets is reactive, vue will update it accordingly
stage.tickets = tickets
}
}
},
async mounted(){
this.teams = fetchTeams()
}
Notice that my 'fetchXXX' methods would just return the data retrieved from the server, without trying to actually set the component data
Edit: typos

Can VueX's store also emit events to components?

I have the following component tree:
App
List
ItemDetails
Widget1
...
the List component has a filters form, those are applied on button press
Widget1 has a button which applies another filter (by id) to the list, applying that one removes filters from the filter form and vice versa
the list is also live-updated via polling (later will be via WebSockets), so I have to separate v-model of the filter fields in List and the actually applied filters (those are in the store)
Currently, I've put the following state to the VueX store:
state: {
filters: {
// these come from the List's filter panel
result: '',
name: '',
date: '',
// this one comes from Widget1
id: '',
},
pagination: {
perPage: 10,
page: 0,
total: 0,
},
items: [],
selectedItem: null,
},
and both filters from the List and from the button in Widget1 are applied via dispatching the following action to the store:
applyFilters ({ state, commit, dispatch }, filters) {
if(filters.id) {
for(let filterName in state.filters)
if(filterName !== 'id')
filters[filterName] = '';
} else {
filters.id = '';
}
commit('setFilters', filters);
commit('setPage', 0);
dispatch('loadExaminations');
},
But here's the problem: the List component has its model of filter fields (on press of the button those are gathered and applyFilters is dispatched). This works well except when the id filter (from Widget1) is applied, the filter fields in the List component are not emptied.
One obvious option to handle this is to move the model of those filter fields to the store as well. That doesn't look that nice since for each field (that uses v-model) I have to create a computed (in the List component) and write a setter and a getter from the store. It seems nicer to send an event from applyFilters to List saying "empty the filter fields", but I'm not sure if VueX can do this. Is this possible? Or should I stick with the "move filter fields' model to the store" plan? Or do you know a nice alternative? I'm aware of the event bus concept, but that one uses Vue instance which shouldn't be used in store, I guess.
You can listen to vuex's actions by using subscribeAction.
// List.vue
mounted() {
this.$store.subscribeAction({
before: (action, state) => {
if (action.type === "applyFilters" && action.payload.id) {
this.emptyComponentFields();
}
},
after: (action, state) => {}
});
}
CodeSandbox.

Input field not reacting to data changes after being written to by a user

While creating a Vue.js application I have become stuck at a weird problem. I want to be able to manipulate an input field (think increment and decrement buttons and erasing a zero value on focus, so the user doesn't have to) and up until a user writes to the input field, everything is fine. After that, however, further changes in the data are no longer represented in the input field.
As I was sure I could not be the only one with this particular problem, I searched extensively, but had no luck. What baffles me the most is that everything works until the field is written to, since I can not really imagine why this would remove the data binding.
The following code should show the same behavior. It is an input field component, which is initialized with a zero value. On focus the zero gets removed. This works, until a user manually writes to the field after which zero values will no longer be removed, even though the focus method fires, the if-condition is met and the data in the amount-variable is changed.
Vue.component('item', {
data: function () {
return {
amount: 0
}
},
render: function (createElement) {
var self = this;
return createElement('input', {
attrs: {
//bind data to field
value: self.amount,
type: 'number'
},
on: {
//update data on input
input: function (event) {
self.amount = event.target.value;
},
//remove a zero value on focus for user convenience
focus: function (event) {
if (self.amount == 0 || self.amount == "0") {
self.amount = '';
}
}
}
})
}
})
I think you need to use domProps instead of attrs to make it reactive. But I would suggest you use vue's template syntax or if you insist on using the render function I would also suggest you to use JSX.

Rerender list in vue.js 2

I use moment.js to display how many time has passed from some event.
I have a list (rendered using vue.js)
event 3, 5 seconds ago
event 2, 1 minute ago
event 1, 5 minutes ago
The problem is: list is not updated frequently (new items are added, for example, every 2 minutes).
I want to update n (seconds|minutes) ago strings.
Should I do simple loop using setInterval?
for(let i = 0; i < this.list.length; i++) {
let item = this.list[i];
item.created_at_diff = moment(item.created_at).fromNow();
this.$set(this.list, i, item);
}
or there is a better approach?
Here is how I would do such a thing, tell me if I am wrong:
First, I would create a component that will make the timer:
let Timer = Vue.extend({
template: '#timer',
props: ['timestamp'],
data () {
return {
formatted: ''
}
},
methods: {
format () {
let self = this
this.formatted = moment().to(moment(this.timestamp,'X'))
// Uncomment this line to see that reactivity works
// this.formatted = moment().format('X')
setTimeout(() => {
self.format()
},500)
}
},
created () {
this.format()
}
})
The timer takes one property, a UNIX timestamp in seconds. The component contains one method called format() that will update the only data formatted of the component. The method is recursive and calls itself every 500ms (with the setTimeout()), you can make this number bigger if you want.
Here is a jsFiddle I made if you want to test it:
https://jsfiddle.net/54qhk08h/

dgrid inside ContentPane - Scroll error

I have a problem with dgrid.... I have an AccordionContainer, and in each ContentPane of it,
I place a dgrid. The problems with the dgrid are:
1- Error with scroll: when scrolling down, in certain moment the scroll "skips" and jumps into the end and there's no way to scroll up and show the first records.
(I have seen in Firebug the error TypeError: grid._rows is null when the scroll fails).
2- Trying to change a value: sounds like no dgrid-datachange event is emitted,
no way to capture the event after editing a value.
I think these errors has to do with having dgrid inside layouts (dgrid inside ContentPane, inside AccordionContainer). I also included the DijitRegistry extension but even with this extensions I can't get
rid of this errors.
I have prepared this fiddle which reproduces the errors:
https://jsfiddle.net/9ax3q9jw/5/
Code:
var grid = new (declare([OnDemandGrid, DijitRegistry,Selection, Selector, Editor]))({
collection: tsStore,
selectionMode: 'none',
columns:
[
{id: 'timestamp', label:'Timestamp', formatter: function (value,rowIndex) {
return value[0];
}},
{id: 'value', label: 'Value',
get: function(value){
return value[1];
},
editor: "dijit/form/TextBox"
}
],
showHeader: true
});
grid.startup();
grid.on('dgrid-datachange',function(event){
alert('Change!');
console.log('Change: ' + JSON.stringify(event));
});
//Add Grid and TextArea to AccordionContainer.
var cp = new ContentPane({
title: tsStore.name,
content: grid
},"accordionContainer");
Any help will be appreciated,
Thanks,
Angel.
There are a couple of issues with this example that may be causing you problems.
Data
The store used in the fiddle is being created with an array of arrays, but stores are intended to work with arrays of objects. This is the root of the scrolling issue you're seeing. One property in each object should uniquely identify that object (the 'id' field). Without entry IDs, the grid can't properly keep track of the entries in your data set. The data array can easily be converted to an object array with each entry having timestamp and value properties, and the store can use timestamp as its ID property (the property it uses to uniquely identify each record).
var records = [];
var data = _globalData[0].data;
var item;
for (var i = 0; i < data.length; i++) {
item = data[i];
records.push({
timestamp: item[0],
value: item[1]
});
}
var tsStore = new declare([Memory, Trackable])({
data: records,
idProperty: 'timestamp',
name: 'Temperature'
});
_t._createTimeSeriesGrids(tsStore);
Setting up the store this way also allows the grid column definitions to be simplified. Using field names instead of IDs will allow the grid to call formatter functions with the corresponding field value for each row object.
columns: [{
field: 'timestamp',
label: 'Timestamp',
formatter: function (value) {
return value;
}
}, {
field: 'value',
label: 'Value',
formatter: function (value) {
return value;
},
editor: "dijit/form/TextBox"
}],
Loading
The fiddle is using declarative widgets and Dojo's automatic parsing functionality to build the page. In this situation the loader callback does not wait for the parser to complete before executing, so the widgets may not have been instantiated when the callback runs.
There are two ways to handle this: dojo/ready or explicit use of the parser.
parseOnLoad: true,
deps: [
...
dojo/ready,
dojo/domReady!
],
callback: function (..., ready) {
ready(function () {
var _t = this;
var _globalData = [];
...
});
}
or
parseOnLoad: false,
deps: [
...
dojo/parser,
dojo/domReady!
],
callback: function (..., parser) {
parser.parse().then(function () {
var _t = this;
var _globalData = [];
...
});
}
Layout
When adding widgets to containers, use Dijit's methods, like addChild and set('content', ...). These typically perform actions other than just adding a widget to the DOM, like starting up child widgets.
var cp = new ContentPane({
title: tsStore.name,
content: grid
});
registry.byId('accordionContainer').addChild(cp);
instead of
var cp = new ContentPane({
title: tsStore.name,
content: grid
}, "accordionContainer");
In the example code a ContentPane isn't even needed since the dgrid inherits from DijitRegistry -- it can be added directly as a child of the AccordionContainer.
This will also call the grid's startup method, so the explicit call in the code isn't needed.
registry.byId('accordionContainer').addChild(grid);
It also often necessary to re-layout the grid's container once the grid has been initially rendered to ensure it's properly sized.
var handle = grid.on('dgrid-refresh-complete', function () {
registry.byId('accordionContainer').resize();
// only need to do this the first time
handle.remove();
});