In aurelia: I have a string interpulation over object property that works fine in the app.html - it shows number of accounts:
ALL ACCOUNTS (${userAccountsData.length})
In the initial loading, I see that the value changes after few milliseconds from 0 to the actual value (data is retrieving from the service), but - when trying to show aggregate data (count number of active accounts) over the same data in a template (custom element) - the data stays as 0 and not updated as the userAccountsData.length
*When refreshing again after the initial loading - the data is shown as it should be.
This is the custom element instance in the app.html:
<account-status-selection-bar accounts-data.bind="userAccountsData"></account-status-selection-bar>
And this is part of the HTML of the custom element itself:
<template>
<div ref="active"
class="selection">${accountActivationDistribution.numberOfActiveAccounts}
This is the relevant part of the custom element VM:
"use strict";
import { bindable} from 'aurelia-framework';
export class accountStatusSelectionBar {
#bindable accountsData;
constructor() {
this.accounts = [];
this.accountActivationDistribution = { numberOfActiveAccounts: 0,
numberOfInactiveAccounts : 0,
numberOfTotalAccounts : 0
}
get activeAccounts() {
var activeAccounts = this.accounts.filter(function(account) {
return account.IsApproved;
});
return activeAccounts.length;
}
attached()//bind()
{
this.accounts = this.accountsData;
this.accountActivationDistribution.numberOfActiveAccounts =
this.activeAccounts
}
In the app.js I use observerLocator - here is the code related to the working part of userAccountsData.length:
constructor() {
this.userAccountsData = [];
....
this.subscribe = this.observerLocator.getObserver(accounts, "all")
.subscribe((value) => {
if (!value)
return;
this.userAccountsData = value;
**A work around I found (although I'm not sure this is the best way) is to do the aggregation in the app.js (in the observer part) in object and bind the already aggregated object to the custom element - this is working. I'm still looking for the mentioned above solution.
It looks like the problem is you're binding userAccountsData to accountsData on your custom control; then assigning this.accounts = this.accountsData; and finally later you're reassigning userAccountsData in app.js.
Because accounts is not observing or bound to the original userAccountsData, it maintains the reference to the original array (which is set to an empty array) and never gets updated.
There is a race condition on refresh, where some cache probably means that userAccountsData gets the updated value before the binding occurs, which is why it works sometimes.
The solution is to remove some of the reassignment and just bind directly to accounts and forget the intermediate accountsData.
I created a gist here showing the different behaviour.
Related
I have a component that renders a table of Inventoried computer equipment. Here is the relevant code for initial render:
let oEquiptByType = reactive({
Laptop: [],
iPad: [],
"Document Camera": [],
"Overhead Projector": [],
Chromebook: [],
Desktop: [],
MacBook: [],
Scanner: [],
});
// ======== Props =========== //
const props = defineProps({
propFormData: {},
});
// Now let's use Stein to retrieve the SS data
// eslint-disable-next-line no-unused-vars
const fetchSheetsData = function () {
const store = new SteinStore(
"https://api.steinhq.com/v1/storages/618e81028d29ba2379044caa"
);
store
.read("HS - Classrooms")
.then((data) => {
scrapDataHSClassrooms.value = data;
emptyRowsRemoved.value.forEach((item) => {
// Let's construct an object that separates equipment by type
// Check if property exists on oEquiptByType object
const exists = Object.prototype.hasOwnProperty.call(
oEquiptByType,
item["Equipment"]
);
// If item(row) is good lets push the row onto the corresponding Object Array
// in oEquiptByType. This will construct an object where each object property corresponds
// to an equipment category. And each oEquiptByType entry is an array where each array
// element is a row from the SS. e.g., oEquiptByType["Laptop"][3] is a row from
// SS and is a laptop.
if (exists) {
oEquiptByType[item["Equipment"]].push(item);
}
});
})
.catch((e) => {
console.error(e);
failure.value = true;
});
};
// =============== Called on component mount =============================== //
onMounted(fetchSheetsData);
The initial render is fine. Now I have a watcher on the prop so when someone submits a new item for the inventory I push that data onto the corresponding object array (ie, a laptop would be pushed onto the oEquiptByType[props.propFormData.Equipment] via oEquiptByType[props.propFormData.Equipment].push(props.propFormData);
// ================================================================ //
// ======================= Watch effects ========================== //
// ================================================================ //
watch(props.propFormData, () => {
// Push the submitted form item onto the reactive
// oEquiptByType object array. This update of Vue state
// will then be injected into DOM and automagically update browser display.
oEquiptByType[props.propFormData.Equipment].push(props.propFormData);
});
This works fine for the first item I add to backend as you can see here with original and then adding first item :
and after first item added (a laptop)
Notice the oEquiptByType[props.propFormData.Equipment] has the new item added. Great.
But now when I add a second item (a MacBook) is added this is resulting state:
Notice the Macbook array has been updated but also the Laptop array's last item has been overwritten with the Mac book entry??? And this behavior continues for any additional items added from a user. I have read docs over and do not see anything that would explain this behavior. I'm hoping maybe someone with more than my limited experience with Vue can help me out. Any additional info needed please let me know. Thanks...
Update:
Put a JSON.Stringify in watch function
Update two:
here is lineage of prop.FormData-
we start in form-modal and emit the form data like:
emit("emiterUIUpdate", formAsPlainObject);
then catch the data in the parent App.vue:
<FormModal
v-show="isModalVisible"
#close="closeModal"
#emiterUIUpdate="updateUI"
>
<DisplayScrap :propFormData="formData" />
const formData = reactive({});
// Method to be called when there is an emiterUIUpdate event emiited
// from form-modal.vue #param(data) is the form data sent from the
// form submission via the event bus. We will then send this data back
// down to child display-scrap component via a prop.
const updateUI = (data) => {
Object.assign(formData, data);
};
and then as posted previous in display-scrap.vue the prop propFormData is defined and watched for in the watch function. hope that helps..
It seems like the watch is getting triggered more often than you expect.
Might be that changes to props.propFormData are atomic and every incremental change triggers changes to the props, which in turn triggers the watch.
Try console logging the value of props.propFormData with JSON.stringify to see what changes are triggering it.
What happens here:
Your form modal emits the emiterUIUpdate event on Ok or Save (button)
Parent takes the object emitted and use Object.assing to copy all properties of emitted object to a formData reactive object. Instead of creating completely new object, you are just replacing the values of all properties of that object all and over again
The formData object is passed by a prop to child component and whenever it changes, it is pushed to target array
As a result, you have a multiple references to same object (formData hold by a parent component) and all those references are to same object in memory. Every Object.assign will overwrite properties of this object and all references will reflect those changes (because all references are pointing to the same object in memory)
Note that this has nothing to do with Vue reactivity - this is simple JavaScript - value vs reference
There is no clear answer to what to do. There are multiple options:
Simplest (and not clearest)
just do not use Object.assign - create new object every time "Save" is clicked
change formData to a ref - const formData = ref({})
replace the value of that ref on emiterUIUpdate event - formData.value = { ...data }
your watch handler in the child will stop working because you are watching props in a wrong way - instead of watch(props.propFormData, () => { use watch(() => props.propFormData, () => {
Better solution
the data should be owned by parent component
when modal emits new data (Save), Parent will just add the newly generated object into a list
share the data with DisplayScraps component using a prop (this can be a simple list or a computed creating object similar to oEquiptByType)
(ref http://jsfiddle.net/kapLv0mt/4/)
I have
Member = Ractive.extend({
template : "<div>{{name}}</div>",
computed : {male : function(){this.get('gender')=='m'}}
})
ff = new Ractive({
el : '#container',
template : "there are {{male_count}} males{{#family}}<member/>{{/family}}",
components : {member : Member},
data : {family : [
{name:'Fred',gender:'m'},
{name:'Wilma',gender:'f'},
{name:'Rocky',gender: 'm'},
{name:'Bubbles',gender: '?'}
]},
computed : { male_count : function(){
return _(this.findAllComponents('member')).filter(function(mem){return mem.get('male')}).length;
}
}
})
An error is thrown during ractive initialization, and also the computed property "male_count" does not return the correct value. The initialization error results from the property being included in the template, but even then the computed property does not return the correct value, probably since it was incorrectly computed at initialization.
How can I initialize a Ractive object attribute that depends on components?
Edit:
Based on a responder's suggestion, I'll mention here that the code example is very much simplified from my actual code. The component filter algorithm in the actual code is based on about 9 comparison criteria, including dates, text matches, numerical ranges, where the thresholds are input via the UI for a list filter. For this reason, I would like to stick with the idea (as in the example) of the filter test being implemented in the component.
First of all, you don't even need to inquire the member component to count the males in the data. The data is already in the parent component, the computed is also in the parent component. Why not just inquire to itself?
computed: {
male_count: function () {
return this.get('family').filter(function (familyMember) {
return familyMember.gender === 'm'
}).length;
}
}
It's also a good thing to keep in mind that in Ractive, you operate on the data, not on DOM (or in this case, components). Once you start using DOM operations, or finding components, your design needs some rethinking.
The application shows work-shifts for certain time-period. firebaseConn.getShifts is the API-function to get the shiftData for the given time period.
versions:
firebase: 2.0.6
angularFire: 0.9.0 (confirmed with 0.8.2 also)
This is my firebase schema:
And this is the code:
.factory('watchers', function(bunch-of-dependencies) {
var unbindShifts = function() {};
var inited = false;
var shifts = {};
... some irrelevant code in between ...
function initShifts() {
unbindShifts();
shifts.object = firebaseConn.getShifts( false, from, to, $scope );
$scope.shifts = shifts.object;
shifts.object.$bindTo($scope, "shifts").then(function(unbind) {
unbindShifts = unbind;
});
}
The firebase-queries (that have worked fine before adding the unbind / bind and possibly time-based querying might cause issues too):
firebaseConn.getShifts = function(asArray, from, to, scope) {
return cacheRequest(FBURL + "shifts", asArray, [from, to]);
};
function cacheRequest(url, asArray, limits) {
var type = asArray ? "array" : "object";
var startAt = limits ? limits[0] : undefined;
var endAt = limits ? limits[1] : undefined;
var retObj, FBRef;
cached[url] = cached[url] || {};
/* If there are limits-parameters we don't cache at all atm. Since those queries should be checked differently than static urls */
if(!limits && cached[url][type]) {
FBRef = cached[url][type];
} else {
FBRef = cached[url][type] = createFBRef(url, startAt, endAt);
}
if(asArray) {
retObj = FBRef.$asArray();
} else {
retObj = FBRef.$asObject();
}
return retObj;
}
function createFBRef(resourceURL, startAt, endAt) {
var modifiedObject = $firebase( createRef( resourceURL ).orderByKey().startAt(startAt).endAt(endAt) );
return modifiedObject;
}
function createRef(resourceURL) {
return new Firebase( resourceURL );
}
Now I have located the problem to be with the query limiting. If the from and to Dates are undefined, this works without problems. But I need to be able to limit the amount of data, since loading many years of workshift-data, to show a weeks time, won't be good :).
The actual problem is not displaying and fetching the data, everything works fine, it's related to the times and re-binding.
If I do any changes to e.g. "20150115"-table. For example I add another "groups"-child there. When i unbind and rebind, the whole "20150115"-table gets deleted and this holds true only to the latest changes. If I add multiple child to different dates e.g. "20150113", "20150114", "20150115" and the latest change is in "20150115" and then I unbind + re-bind another time from firebase, all the other root-paths will stay as they are, but the latest change in "20150115" will make the whole tree deleted.
I hope I make myself clear, so for safety I try to explain it again in simpler way.
- Changes to 1. "20150113", 2. "20150114", 3. "20150115" through the app.
- Changing timeline from UI causes: unbind + re-bind
- As a side-effect the whole "20150114" tree gets deleted.
The problem is somehow related to advanced querying with orderByKey().startAt(startAt).endAt(endAt) and binding.
Also for additional info. The data which is added through the UI gets added to the firebase database, but when the re-binding happens, the data is deleted from the database. Specifically on rebind, unbinding causes no issues, if I delay rebinding with timeout.
EDIT:
I have found the source of the actual issue. After the new binding is in place and everything seems to be in order, there is an angular watch event that kicks in. The event tries to save the last change user made before re-binding.
So if I have and active timeline for december (20141201 - 20141230) and I change "20141225"-data. Then change the timeline to 20150101 - 20150130, causing unbind and rebind (or manually fetching new data). There will be an event, after the binding has been done and everything seems to be in order, trying to save 20141225 data to either the new timeline (20150101 - 20150130) or the old one, not sure which one. This causes the firebase to actually delete the whole 20141225-tree, instead of saving the data.
The new data makes it into your Firebase fine, which you can see by either checking your Firebase dashboard or by running a quick snippet like this in your browser's dev console:
new Firebase("https://firebaseurl").once('value', function(s) { console.log(s.val()); })
The data even makes it back into your application. The only problem is that Angular doesn't know that new data has arrived, so it doesn't update the view with the new data.
Normally AngularFire's $asObject and $asArray methods take care of notifying AngularJS when new data arrives from Firebase. But since you are constantly creating new queries, you'll have to take care of that yourself.
There are a few ways to signal the new data to AngularJS and I'm definitely not an expert on which one is best. But if you add $scope.$apply(); to your setDays function it works:
function setDays(ref) {
var FBRange = setFBRange(ref, from, to);
var days;
unbindDays();
days = $firebase(FBRange).$asObject();
$scope.days = days;
days.$bindTo($scope, "days").then(function(unbind) {
unbindDays = unbind;
// As a result of the new binding entry gets mysteriously deleted from firebase
});
$scope.$apply(); // Tell AngularJS about the new data, so that it updates the view
function setFBRange(ref, from, to) {
return ref.orderByKey().startAt(""+from).endAt(from + to + "");
}
}
Updated Plunkr with this change (and some others to help in debugging): http://plnkr.co/edit/YZtkzUNtjQUCcw4xb2mj?p=preview
I have the following client-side code (with two collections A and B):
var id = A.insert({name: 'new element of A');
var b = B.findOne({name: 'parent of new element of A'});
B.update(b._id, { $set: { child: id } });
The above code works fine, i.e., the server side collections are correctly updated. The problem happens on the client where I have a template that reacts on both A and B collection changes.
The template reacts as follows:
It immediately redraws itself, based on the latency compensation mechanism, showing the correct changes for both collections
Soon after, it redraws itself again but without the changes (as if they had been rejected)
It does not refresh automatically anymore afterwards. But, if I hit the refresh button, the template redraws once more and now shows the correctly updated collections (both A and B)
On the second refresh only one of the collection (the parent B) has been updated and the template displays incoherent data (as if the updates had not happened).
I think this is because I am not dealing here with one single transaction that updates both collections at the same time, confusing the client side template.
How can I solve this?
EDIT:
I must add that in my case I have two complementing subscriptions to the child database:
var A = new Meteor.Collection('children');
handle1 = Meteor.subscribe('children1');
handle2 = Meteor.subscribe('children2');
and on the server
Meteor.publish('children1', function () {
return A.find({ sex: male });
}
Meteor.publish('children2', function () {
return A.find({ sex: female });
}
Could this be the reason for, when I insert a new element in A, I get the weird behavior described above?
it might work better if you do it all in one command
B.update({name: 'parent of new element of A'}, { $set: { child: A.insert({name: 'new element of A') } });
you'll have to put this in a method since you arent updating based on the id
you could also try to manually call Deps.flush() after the operations, that shouldnt be needed, but its something to try
Is there a way to get a callback for when an entire list renders?
I've tried
Template.articles.rendered = function() {
var lastChapter = Chapters.findOne({}, {
sort: {
createdTime: -1
}
})
if (lastChapter._id != this.data._id)
return
doSomething()
};
But this is unreliable because chapters are added 1 by 1 instead of all at once, so this actually fires multiple times.
Thanks.
rendered is called when a part of the template is re-rendered, so you should check inside your rendered method whether you want to do anything now. When does "the entire list renders" happen? You know that in your code, for instance by checking if the list is of an expected length yet.