Identity in ractive data arrays - ractivejs

I have an object of message streams that looks like this:
ractive.data.messages:
{
stream_id1: {
some_stream_metadata: "foo",
stream: [
{id: "someid1", message: "message1"},
{id: "someid2", message: "message2"}
]
},
stream_id2: {
some_stream_metadata: "bar",
stream: [
{id: "someid3", message: "message3"},
{id: "someid4", message: "message4"}
]
}
}
main_template:
{{#messages[ current_stream_id ]}}
{{>render_message_stream}}
{{/messages[ current_stream_id ]}}
render_message_stream:
{{#stream}}
<div class="stream">
...someotherstuff...
{{>render_message}}
</div>
{{/stream}}
render_message:
<div class="message">
...someotherstuff...
{{message}}
</div>
I change "current_stream_id" to change the rendered stream of messages.
On updates, i change the contents of the message streams like this:
ractive.merge(
"messages.stream_id1.stream",
new_message_stream,
{
compare: function ( item ) { return item.id; }
});
I also tried the compare: true option instead of the function, with the same results:
Ractive always thinks that these two messages belong effectively to the same DOM element, even though they live in a completely different message stream:
ractive.data.messages[ "stream_id1" ].stream[1].message
ractive.data.messages[ "stream_id2" ].stream[1].message
Problems:
When there are intro/outro animations ractive animates always just the end of the messages stream, even when a message in the middle of the stream was deleted, i need help to make ractive understand which messages are identical.
When i change the current_stream_id, ractive does not rerender the complete {{>render_message_stream}} partial, but goes inside the existing dom and changes the {{message}} field in all existing messages, though this might be good for dom element reuse, this triggers a lot of animations that are wrong. (Eg. it triggers intro/outro animations for the last message in the stream if stream1 has one message more than stream2).

One of these issues has a straightforward answer; unfortunately the other one doesn't.
I'll start with the easy one - the fact that
ractive.data.messages[ "stream_id1" ].stream[1].message
ractive.data.messages[ "stream_id2" ].stream[1].message
belong to the same DOM element. You're correct in that Ractive updates the existing elements rather than removing them and creating new ones - this is a core part of its design. In this case that's undesirable behaviour, but you can work around it like so:
// instead of immediately switching to a new stream ID like this...
ractive.set( 'current_stream_id', 'stream_id2' );
// you can set it to a non-existent ID. That will cause the existing DOM
// to be removed. When you set it to an ID that *does* exist, new DOM
// will be created:
ractive.set( 'current_stream_id', null );
ractive.set( 'current_stream_id', 'stream_id2' );
// or, if you'd like the initial transitions to complete first...
ractive.set( 'current_stream_id', null ).then(function () {
ractive.set( 'current_stream_id', 'stream_id2' );
});
The other issue - that merge() isn't merging, but is instead behaving as though you were doing ractive.set('messages.stream_id1.stream', new_message_stream) - is tougher. The problem is that while you and I know that {{#messages[ current_stream_id ]}} equates to messages.stream_id1 when current_stream_id === 'stream_id1, Ractive doesn't.
What it does know is that we have an expression whose value is determined by messages and current_stream_id. When the value of either of those references changes, the expression is re-evaluated, and if that value changes, the DOM gets updated - but using a standard set(). When you do ractive.merge('messages.stream_id1.stream', ...), Ractive updates all the things that depend on keypaths that are 'upstream' or 'downstream' of messages.stream_id1.stream - which includes messages. So that's how the expression knows that it needs to re-evaluate.
It's possible that a future version of Ractive will be able to handle this case in a smarter fashion. Perhaps it could make a note of arrays that are subject to merge operations, and check evaluator results to see if they're identical to one of those arrays, and if so use merge() rather than set(). Perhaps it could analyse the function in some way to see if the {{#messages[ current_stream_id ]}} section should register itself as a dependant of messages.stream_id1 for as long as current_stream_id === 'stream_id1', rather than the internally-generated ${messages-current_stream_id-} keypath.
None of that helps you in the meantime though. The only way to use merge() in your current situation is to have a separate reference that doesn't use an expression, and a bit of magic with pattern observers:
main_template:
{{#current_messages}} <!-- rather than `messages[ current_stream_id ]` -->
{{>render_message_stream}}
{{/current_messages}}
render_message_stream:
{{#current_message_stream}} <!-- rather than `stream` -->
<div class="stream">
{{>render_message}}
</div>
{{/current_message_stream}}
code:
ractive.observe( 'current_stream_id', function ( id ) {
var current_messages = this.get( 'messages.' + id );
this.set( 'current_messages', current_messages );
// hide existing stream, then show new stream
this.set( 'current_message_stream', null ).then(function () {
this.set( 'current_message_stream', current_messages.stream );
});
});
// when ANY message stream changes, we see if it's the current one - if so, we
// perform a merge on the top-level `current_message_stream` array
ractive.observe( 'messages.*.stream', function ( new_stream, old_stream, keypath, id ) {
// the value of any * characters are passed in as extra arguments, hence `id`
if ( id === this.get( 'current_stream_id' ) ) {
this.merge( 'current_message_stream', new_stream, {
compare: function ( item ) {
return item.id;
}
});
}
});
I've set up a JSFiddle demonstrating this. I hope it makes sense, let me know if not - and sorry I didn't get round to answering this question much sooner.

Related

Flow error when using react-apollo Query component render prop

I have the following code:
import { Query } from 'react-apollo';
type Post = {
id: string
};
interface Data {
posts: Array<Post>;
}
class PostsQuery extends Query<Data> {}
When using the above as follows:
<PostsQuery query={POSTS_QUERY}>
{({ loading, data }) => {
...
{data.posts.map(...)}
...
}
</PostsQuery>
I get the following error from flow:
Error:(151, 27) Cannot get 'data.posts' because property 'posts' is missing in object type [1].
Any idea why?
I did use flow-typed to add apollo-client_v2.x.x.js to my project by the way
Solution to the problem
Continued from the answer explaining how to make a verifiable example and research the problem.
So it looks like this part of react-apollo isn't typed in such a way to make accessing the data contents straightforward. Okay, that's fine, we can take their recommendation on destructuring and check for empty data. At the same time, we can also add an id property to the Post type so flow stops complaining about that:
(Try - Scroll to bottom for relevant code)
type Post = {
id: string,
title: string;
};
...snip...
// Look ma, no errors
class Search extends React.Component<{}> {
render = () => (
<PostsQuery query={QUERY}>
{({ loading, error, data }) => {
if (error) {
return <p>Error</p>
}
if (loading) return <p>Loading</p>
const nonNullData = (data || {})
const dataWithAllPosts = {allPosts: [], ...nonNullData}
const {allPosts} = dataWithAllPosts
if (allPosts.length == 0) {
return <p>Empty response or something</p>
}
return (
<div>
{allPosts.map(post => {
return <div key={post.id}>{post.title}</div>;
})}
</div>
);
}}
</PostsQuery>
);
}
I'm not familiar with the react-apollo library, so I'm not sure how you want to handle that case where there are no posts. I just added a message as seen above. It's entirely possible that the case never occurs (again, you would know better than I do). If that's the case, you might want to skip some of the above steps and just assert the desired type with a cast through any.
How to make a reproducible example and research the problem
So the first thing we need to do while analyzing these types is to go lookup the typedefs in the flow-typed repo. I went ahead a copy-pasted the react-apollo typedefs into flow.org/try, modified them slightly (added an any somewhere, set gql to any), and was able to replicate your errors:
(Try - Scroll to the bottom for your code)
Referencing the relevant lines of the QueryRenderProps type, we can see why flow is throwing the error:
{
data: TData | {||} | void,
...
}
It looks like data can either be TData (probably what you want), an empty object, or undefined. Cross checking this with the typescript typings for react-apollo, we can see why the type is the way it is:
{
...
// we create an empty object to make checking for data
// easier for consumers (i.e. instead of data && data.user
// you can just check data.user) this also makes destructring
// easier (i.e. { data: { user } })
// however, this isn't realy possible with TypeScript that
// I'm aware of. So intead we enforce checking for data
// like so result.data!.user. This tells TS to use TData
// XXX is there a better way to do this?
data: TData | undefined;
...
}
Unfortunately, due to the extreme length of these links and stackoverflow's limit on answer lengths, I have to continue my answer in another answer. I guess this answer can serve as an explanation of how to start debugging the problem.

How to call a function in data-binding when lots of parameters are involved

At the moment I have this in my template:
WAY 1
<template is="dom-if" if="{{_showCancel(userData.generic.id,info.userId,info._children.gigId.userId,info.accepted,info.cancelled,info.paymentTerms)}}">
And then this in my element:
_showCancel: function(viewerUserId,offerUserId,gigUserId, accepted, cancel, paymentTerms) {
// Offer needs to be accepted and NOT cancelled
if (!info.accepted || info.cancelled) return false;
// Gig owner can ALWAYS cancel an offer
if (viewerUserId == gigUserId ) return true;
// Offer maker can only cancel if the gig involved pre-payment
if (viewerUserId == offerUserId && paymentTerms != 'on-day') return true;
return false;
},
Do I have just too many parameters to this function?
WAY 2
Should I just have something like this instead:
<template is="dom-if" if="{{_showCancel(userData, info)}}">
WAY 3
Although I would want to check if their sub-properties change too... so I would need:
<template is="dom-if" if="{{_showCancel(userData, info, userData.*, info.*)}}">
WAY 4
But then again I probably should just look for the properties and use the value property like so:
<template is="dom-if" if="{{_showCancel(userData.*, info.*)}}">
And then the function would be:
_showCancel: function(userDataObs, infoObs) {
var userData = userDataObs.value;
var info = infoObs.value;
if( !userData || !info) return;
...
Questions:
Do you see any fundamental mistakes with ways 1 to 4?
Is WAY 1 really the best way to go about it? (it feels like it right now)
Is WAY 3 an acceptable pattern?
ADDENDUM
What would I do in cases where I have:
<paper-button
raised
hidden$="{{!_showCancel(item.id,userData.config.usersCategories,userData.config.userCategories,userData.config.usersCategories.*)}}"
>
Cancel
</paper-button>
(NOTE: usersCategories is an Array)
And _showCancel being:
_showCancel: function(categoryId, userCategories) {
for (var k in usersCategories) {
var $ucat = usersCategories[k];
if ($ucat.categoryId == categoryId) {
if ($ucat.status == 'applied' || $ucat.status == 'granted') return true;
}
}
return false;
},
The point being: I want both easy access to usersCategories, but don't want to get the value out of the "strange" array modifiers etc. So, is this a good pattern?
The "Way 1" is the right one. But, you should only reference the variables that you need, and you should always use them as they are defined in your function header.
For example, you use:
{{_showCancel(userData.generic.id,info.userId,info._children.gigId.userId,info.accepted,info.cancelled,info.paymentTerms)}}
with the following function header:
_showCancel: function(viewerUserId,offerUserId,gigUserId, accepted, cancel, paymentTerms).
But then inside the function, you reference info.accepted and info.cancelled, whereas you should used accepted and cancelled.
This is because inside the function, the referenced values will always be up-to-date, whereas referencing variables via this.<variable-name> can sometimes contain older values.
In order for my answer to be complete, I will also explain certain "problems" with other ways.
Way 2: Here, you only reference the Object as a whole. This won't trigger the call via subproperty change, so it won't work as desired.
Way 3 and Way 4 are similar, and both are overkills. With the object.* notation, you listen to all subproperty changes, which you most likely don't want.
tl;dr
Go with "Way 1" and make things simpler by using computed properties.
To do this, change:
<template is="dom-if" if="{{_showCancel(userData.generic.id,info.userId,info._children.gigId.userId,info.accepted,info.cancelled,info.paymentTerms)}}">
To:
<template is="dom-if" if="{{isCanceled}}">
And add the following computed property to the Polymer element:
isCanceled: {
type: Boolean,
computed: '_showCancel(userData.generic.id,info.userId,info._children.gigId.userId,info.accepted,info.cancelled,info.paymentTerms)'
}
You already have _showCancel defined, so this is actually it. The code should work the same as your "Way 1" example, only the dom-if is cleaner. This is especially useful if you re-use the condition on multiple occurences.
If you have any questions, don't hesitate do add a comment about it.

Returned value from Meteor Helper not showing up in template

I have a Meteor Helper that does a GET request and am supposed to get response back and pass it back to the Template, but its now showing up the front end. When I log it to console, it shows the value corerctly, for the life of mine I can't get this to output to the actual template.
Here is my helper:
UI.registerHelper('getDistance', function(formatted_address) {
HTTP.call( 'GET', 'https://maps.googleapis.com/maps/api/distancematrix/json? units=imperial&origins=Washington,DC&destinations='+formatted_address+'&key=MYKEY', {}, function( error, response ) {
if ( error ) {
console.log( error );
} else {
var distanceMiles = response.data.rows[0].elements[0].distance.text;
console.log(response.data.rows[0].elements[0].distance.text);
return distanceMiles;
}
});
});
In my template I pass have the following:
{{getDistance formatted_address}}
Again, this works fine and shows exactly what I need in the console, but not in the template.
Any ideas what I'm doing wrong?
I posted an article on TMC recently that you may find useful for such a pattern. In that article the problem involves executing an expensive function for each item in a list. As others have pointed out, doing asynchronous calls in a helper is not good practice.
In your case, make a local collection called Distances. If you wish, you can use your document _id to align it with your collection.
const Distances = new Mongo.collection(); // only declare this on the client
Then setup a function that either lazily computes the distance or returns it immediately if it's already been computed:
function lazyDistance(formatted_address){
let doc = Distances.findOne({ formatted_address: formatted_address });
if ( doc ){
return doc.distanceMiles;
} else {
let url = 'https://maps.googleapis.com/maps/api/distancematrix/json';
url += '?units=imperial&origins=Washington,DC&key=MYKEY&destinations=';
url += formatted_address;
HTTP.call('GET',url,{},(error,response )=>{
if ( error ) {
console.log( error );
} else {
Distances.insert({
formatted_address: formatted_address,
distanceMiles: response.data.rows[0].elements[0].distance.text
});
}
});
}
});
Now you can have a helper that just returns a cached value from that local collection:
UI.registerHelper('getDistance',formatted_address=>{
return lazyDistance(formatted_address);
});
You could also do this based on an _id instead of an address string of course. There's a tacit assumption above that formatted_address is unique.
It's Meteor's reactivity that really makes this work. The first time the helper is called the distance will be null but as it gets computed asynchronously the helper will automagically update the value.
best practice is not to do an async call in a helper. think of the #each and the helper as a way for the view to simply show the results of a prior calculation, not to get started on doing the calculation. remember that a helper might be called multiple times for a single item.
instead, in the onCreated() of your template, start the work of getting the data you need and doing your calculations. store those results in a reactive var, or reactive array. then your helper should do nothing more than look up the previously calculated results. further, should that helper be called more times than you expect, you don't have to worry about all those additional async calls being made.
The result does not show up because HTTP.call is an async function.
Use a reactiveVar in your case.
Depending on how is the formated_address param updated you can trigger the getDistance with a tracker autorun.
Regs
Yann

How can I update multiple collections in Meteor in the same transaction?

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

Adding a model to Collection in Backbone [duplicate]

I'm running into an odd issue with a Backbone.js Model where an array member is being shown as blank. It looks something like this:
var Session = Backbone.Model.extend({
defaults: {
// ...
widgets: []
},
addWidget: function (widget) {
var widgets = this.get("widgets");
widgets.push(widget);
this.trigger("change:widgets", this, widgets);
},
// ...
// I have a method on the model to grabbing a member of the array
getWidget: function (id) {
console.log(this.attributes);
console.log(this.attributes.widgets);
// ...
}
});
I then add a widget via addWidget. When trying getWidget the result I get (in Chrome) is this:
Object
widgets: Array[1]
0: child
length: 1
__proto__: Array[0]
__proto__: Object
[]
It's showing that widgets is not empty when logging this.attributes but it's shown as empty when logging this.attributes.widgets. Does anyone know what would cause this?
EDIT
I've changed the model to instantiate the widgets array in the initialization method to avoid references across multiple instances, and I started using backbone-nested with no luck.
Be careful about trusting the console, there is often asynchronous behavior that can trip you up.
You're expecting console.log(x) to behave like this:
You call console.log(x).
x is dumped to the console.
Execution continues on with the statement immediately following your console.log(x) call.
But that's not what happens, the reality is more like this:
You call console.log(x).
The browser grabs a reference to x, and queues up the "real" console.log call for later.
Various other bits of JavaScript run (or not).
Later, the console.log call from (2) gets around to dumping the current state of x into the console but this x won't necessarily match the x as it was in (2).
In your case, you're doing this:
console.log(this.attributes);
console.log(this.attributes.widgets);
So you have something like this at (2):
attributes.widgets
^ ^
| |
console.log -+ |
console.log -----------+
and then something is happening in (3) which effectively does this.attributes.widgets = [...] (i.e. changes the attributes.widget reference) and so, when (4) comes around, you have this:
attributes.widgets // the new one from (3)
^
|
console.log -+
console.log -----------> widgets // the original from (1)
This leaves you seeing two different versions of widgets: the new one which received something in (3) and the original which is empty.
When you do this:
console.log(_(this.attributes).clone());
console.log(_(this.attributes.widgets).clone());
you're grabbing copies of this.attributes and this.attributes.widgets that are attached to the console.log calls so (3) won't interfere with your references and you see sensible results in the console.
That's the answer to this:
It's showing that widgets is not empty when logging this.attributes but it's shown as empty when logging this.attributes.widgets. Does anyone know what would cause this?
As far as the underlying problem goes, you probably have a fetch call somewhere and you're not taking its asynchronous behavior into account. The solution is probably to bind to an "add" or "reset" event.
Remember that [] in JS is just an alias to new Array(), and since objects are passed by reference, every instance of your Session model will share the same array object. This leads to all kinds of problems, including arrays appearing to be empty.
To make this work the way you want, you need to initialize your widgets array in the constructor. This will create a unique widget array for each Session object, and should alleviate your problem:
var Session = Backbone.Model.extend({
defaults: {
// ...
widgets: false
},
initialize: function(){
this.set('widgets',[]);
},
addWidget: function (widget) {
var widgets = this.get("widgets");
widgets.push(widget);
this.trigger("change:widgets", this, widgets);
},
// ...
// I have a method on the model to grabbing a member of the array
getWidget: function (id) {
console.log(this.attributes);
console.log(this.attributes.widgets);
// ...
}
});
Tested in a fiddle with Chrome and Firefox: http://jsfiddle.net/imsky/XBKYZ/
var s = new Session;
s.addWidget({"name":"test"});
s.getWidget()
Console output:
Object
widgets: Array[1]
__proto__: Object
[
Object
name: "test"
__proto__: Object
]

Resources