Skip to content

Instantly share code, notes, and snippets.

@nathan-muir
Last active January 28, 2016 07:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nathan-muir/415f0167a091f80b1fda to your computer and use it in GitHub Desktop.
Save nathan-muir/415f0167a091f80b1fda to your computer and use it in GitHub Desktop.
Meteor - Connect
connect = function connect(observeable, ...selectors) {
return function (ChildComponent) {
const childDisplayName =
ChildComponent.displayName ||
ChildComponent.name ||
'ChildComponent';
return React.createClass({
displayName: `ConnectedObserver(${childDisplayName})`,
componentWillUnmount(){
if (this.handle) {
this.handle.stop();
this.handle = null;
}
this.lastArgs = null;
},
installComputation(args, requestChange){
if (this.handle) {
this.handle.stop();
}
this.handle = observeable(...args.concat(requestChange));
this.lastArgs = args;
},
getInitialState() {
// special case, we need to get the first result synchronously
const args = selectors.map((selector)=>selector(this.props));
var init = true, result = null;
const requestChange = (_result)=>{
if (init){
result = _result;
} else {
this.handleChange(_result);
}
};
try {
this.installComputation(args, requestChange);
} finally{
init = false;
}
return result;
},
handleChange(result){
if (this.isMounted()) {
this.replaceState(result);
}
},
componentWillReceiveProps(nextProps){
const args = selectors.map((selector)=> selector(nextProps));
const changed = this.lastArgs.some((value, idx)=> args[idx] !== value);
if (changed) {
this.installComputation(args, this.handleChange);
}
},
render(){
return <ChildComponent {...this.props} {...this.state} />
}
})
};
};
connectWithTracker = function connectWithTracker(fn, ...selectors){
function wrappedFn(...args){
const onData = args.pop();
return Tracker.autorun(function(){
onData(fn(...args));
});
}
return connect(wrappedFn,...selectors);
};
connectWithDescriptor = function connectWithDescriptor(descriptor, ...selectors){
function wrappedFn(...args){
const onData = args.pop();
const results = {};
const handles = {};
for (let name in descriptor){
let fn = descriptor[name];
handles[name] = Tracker.autorun(function(c){
results[name] = fn(...args);
if (!c.firstRun) onData(results);
});
}
onData(results);
return {
stop(){
for(let name in handles){
handles[name].stop();
}
}
}
}
return connect(wrappedFn,...selectors);
};
TestMessage = function(props){
return (
<div>
<span>Name: {props.name}</span>
<br/>
<span>Message: {props.message}</span>
</div>
)
};
TestMessage = connectWithDescriptor({
// These computations are re-run independently.
// Because they don't have any selectors, they don't
// wont re-run when props change.
message() {
return Session.get('lastMessage')
},
name() {
return Session.get('playerName')
}
})(TestMessage)
Messages = function(props){
/* todo- rendering */
}
// Have just put these separately so they're described.
const userSelector = (props)=>props.user;
const limitSelector = (props)=>props.limit;
const dateSelector = (props)=>props.date;
// This function will re-run whenever the result of a
// selector changes.
// Or when the result of the query changes
Messages = connectWithTracker(function(user, limit, date){
let messages = Messages.find({user: user, date: {$gte: date}}, {sort: {date: 1}, limit: limit}).fetch();
let unreadMessageCount = messages.reduce((count, msg)=>(count + (msg.unread ? 1 : 0)), 0);
return {messages, unreadMessageCount};
}, userSelector, limitSelector, dateSelector)(Messages)
Messages = function(props){
/* todo- rendering */
}
// Have just put these separately so they're described.
const userSelector = (props)=>props.user;
const limitSelector = (props)=>props.limit;
const dateSelector = (props)=>props.date;
// This version will re-run when the selectors change
// But keeps streaming new values via `onData`
// This example would probably be better if we used some sort of immutable wrapper
Messages = connect(function(user, limit, date, onData){
var unreadMessageCount = 0;
var messages = [];
var init = true;
const cursor = Messages.find({user: user, date: {$gte: date}}, {sort: {date: 1}, limit: limit});
// By manually observing changes, we can just emit changes
const handle = cursor.observe({
addedAt(message, atIndex, before){
if (message.unread) unreadMessageCount++;
messages.splice(atIndex, 0, message);
if (!init) onData({messages, unreadMessageCount});
},
changedAt(message, oldMessage, atIndex){
messages.splice(atIndex, 1, message);
if (message.unread !== oldMessage.unread){
if (message.unread) unreadMessageCount++;
else unreadMessageCount--;
}
onData({messages, unreadMessageCount})
},
removedAt(message, atIndex){
if (message.unread) unreadMessageCount--;
messages.splice(atIndex, 1);
onData({messages, unreadMessageCount})
},
movedTo(message, fromIndex, toIndex){
messages.splice(fromIndex, 1);
messages.splice(toIndex, 0, message);
onData({messages, unreadMessageCount});
}
})
onData({messages, unreadMessageCount});
return {
stop(){
handle.stop();
}
}
}, userSelector, limitSelector, dateSelector)(Messages)
// This version will re-run when the selectors change
// But keeps streaming new values from the query via `onData`
// This is using https://github.com/mindfront/meteor-immutable-observer
Messages = connect(function(user, limit, date, onData){
const cursor = Messages.find({user: user, date: {$gte: date}}, {sort: {date: 1}, limit: limit});
const observer = new ImmutableObserver.List(cursor);
const computation = Tracker.autorun(function(){
// This inner computation will re-run whenever
// the immutable driver computes a new documents list.
const messages = observer.documents();
onData({
messages,
unreadMessageCount: messages.reduce((count, msg)=>(count + (msg.get('unread') ? 1 : 0)), 0)
})
});
return {
stop(){
computation.stop();
observer.stop();
}
};
}, userSelector, limitSelector, dateSelector)(Messages);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment