Skip to content

Instantly share code, notes, and snippets.

@spion
Last active December 1, 2016 23:34
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 spion/1c3a945e2afe059d443fcc2800fb553e to your computer and use it in GitHub Desktop.
Save spion/1c3a945e2afe059d443fcc2800fb553e to your computer and use it in GitHub Desktop.
import {render} from 'react-dom'
import {observable, computed, action} from 'mobx'
import {observer} from 'mobx-react'
import * as React from 'react'
import {fromPromise, IPromiseBasedObservable} from 'mobx-utils'
import 'whatwg-fetch'
import {computedAsync, matchAsync} from './computed-async'
function delay<T>(n:number) {
return function(v:T) {
return new Promise<T>(resolve => setTimeout(resolve, n, v))
}
}
class Model {
@observable path = '/'
@observable selectedFile:{name: string} = null;
@computedAsync get directoryContent() {
return fetch('/files' + this.path)
.then(delay(1000))
.then(res => res.json())
.then(res => (res as any).list)
}
@computedAsync get selectedFileContent() {
if (this.selectedFile) {
return this.directoryContent.then(files => {
if (files.indexOf(this.selectedFile) < 0) return null;
return fetch('/files' + this.path + '/' + this.selectedFile.name)
.then(delay(1000))
.then(res => res.text())
})
}
return null;
}
@action selectFile(f:any) {
if (f.dir) {
this.path += f.name + '/';
} else {
this.selectedFile = f;
}
}
@action upOneLevel() {
this.path = this.path.split('/').slice(0, -2).join('/') + '/'
}
}
@observer
class View extends React.Component<{model:Model}, {}> {
render() {
let model = this.props.model;
let pleaseSelect = <p>Please click on a file to show it here</p>;
return (
<div>
<p>
Current directory path: {model.path}
<button onClick={() => model.upOneLevel()}>..</button>
</p>
{matchAsync(model.directoryContent, {
pending: () => <p>Loading...</p>,
fulfilled: (files:any[]) =>
<div>
{files.map(f => <div>
<button onClick={() => model.selectFile(f)}>{f.name}</button>
</div>)}
</div>,
rejected: (e:any) => <p>Error loading directory listing</p>
})}
<div>
{matchAsync(model.selectedFileContent, {
pending: () => <p>Loading file...</p>,
fulfilled: (v:any) => v?<p>Content: {v.toString()}:</p>:pleaseSelect,
rejected: (e:any) => <p>Error loading file: {e.message}</p>
})}
</div>
</div>
)
}
}
render(<View model={new Model()} />, document.getElementById('app'))
import {Atom, Reaction} from 'mobx'
interface Handlers<T, U> {
null?: () => U;
pending?: () => U;
fulfilled?: (t: T) => U;
rejected?: (e: any) => U;
}
type PromiseState = "fulfilled" | "rejected" | "pending"
class ObservablePromise<T> implements PromiseLike<T> {
private promise: PromiseLike<T>;
private state: PromiseState = "fulfilled"
private value: T = null;
private error: any = null;
private atom: Atom;
private reaction: Reaction;
constructor(private getter:() => PromiseLike<T> | T, thisObj:any) {
let getSetter = () => {
// cancel previous request, if cancellable
if (this.promise && typeof (this.promise as any).cancel == 'function') {
(this.promise as any).cancel()
}
this.promise = getter.call(thisObj)
if (this.promise && typeof this.promise.then === 'function') {
this.state = "pending"
this.promise.then(val => {
this.state = "fulfilled"
this.value = val;
this.atom.reportChanged()
}, err => {
this.state = "rejected"
this.error = err;
this.atom.reportChanged()
});
} else {
this.state = "fulfilled"
this.value = this.promise as any;
}
this.atom.reportChanged()
}
let stopReactions = () => {}
this.atom = new Atom('ComputedAsync',
() => {
this.reaction = new Reaction("ComputedAsync", function () {
this.track(getSetter);
});
stopReactions = this.reaction.getDisposer()
this.reaction.runReaction()
},
() => { stopReactions() });
}
get() {
this.atom.reportObserved()
return this;
}
then<TResult>(onfulfilled?: (value: T) => TResult | PromiseLike<TResult>, onrejected?: (reason: any) => TResult | PromiseLike<TResult>): Promise<TResult> {
return this.promise.then(onfulfilled, onrejected) as Promise<TResult>
}
catch<TResult>(handler:(reason:any) => PromiseLike<TResult> | TResult):Promise<TResult> {
return this.promise.then(null, handler) as Promise<TResult>
}
public [Symbol.toStringTag]:"Promise"
}
export function computedAsync<T>(target: any, name: string, desc:TypedPropertyDescriptor<Promise<T>>) {
let getter:ObservablePromise<T> = null;
return {
get: function() {
if (!getter) {
getter = new ObservablePromise<T>(desc.get, this)
}
return getter.get();
}
} as TypedPropertyDescriptor<ObservablePromise<T>>
}
export function matchAsync<T, U>(p:Promise<T>, handlers: Handlers<T, U>): U {
let state = (p as any).state as PromiseState;
if (typeof state != 'string') { throw new TypeError("Not an observable promise!"); }
switch(state) {
case "pending": return handlers.pending && handlers.pending()
case "fulfilled": return handlers.fulfilled && handlers.fulfilled((p as any).value)
case "rejected": return handlers.rejected && handlers.rejected((p as any).error)
}
}
@benjamingr
Copy link

benjamingr commented Nov 8, 2016

 function computedAsync(target, name, desc) {
   const res = computed(desc.get), value = observable("loading..."), pending = Promise.resovle();
   res.observe(change => {
     if(change.then) { // promise
       change.then(v => {
         if(pending !== change) return;
         value = change;
       }, e => {
         if(pending !== change) return;
         value = ":(";
       });
       pending = change;
     } else {
       value = change;
     }
   });
    return {
        get: function() {
            return value;
        }
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment