Skip to content

Instantly share code, notes, and snippets.

@diestrin
Created February 22, 2017 16:46
Show Gist options
  • Select an option

  • Save diestrin/6cec6fa951b28603325a304b37065d28 to your computer and use it in GitHub Desktop.

Select an option

Save diestrin/6cec6fa951b28603325a304b37065d28 to your computer and use it in GitHub Desktop.
// lib imports
import { ReplaySubject } from 'rxjs/ReplaySubject';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import 'rxjs/add/operator/scan';
// external imports
import { mergeDeep, isFunction } from '@wu/utils';
export interface IAction<Actions> {
type: Actions;
payload?: any;
}
export const ReducerMethod = (action: number) => {
return (target: Reducer<any, any>|ReducerSimple<any, any>,
key: string,
descriptor: TypedPropertyDescriptor<(state: any, action: any) => any>
) => {
if (!('cases' in target)) {
(target as any).cases = {};
}
(target as any).cases[action] = target[key];
return descriptor;
};
};
type reducer = <State, Action>(state: State, action: Action) => State;
type reducerOrSubReducer = reducer|{stateKey: string, reducer: reducer};
export abstract class ReducerSimple<State, Action extends IAction<number>> {
public state$: BehaviorSubject<State>;
public dispatcher: ReplaySubject<Action>;
public dispatch: (value: Action) => void;
public dispatchNext: (value: Action) => void;
public cases: {[key: number]: reducer};
public get currentState(): State {
return this.state$.getValue();
}
}
export abstract class Reducer<State, Action extends IAction<number>> {
public readonly dispatcher = new ReplaySubject<Action>();
public readonly state$ = new BehaviorSubject(this.intialState);
public get currentState(): State {
return this.state$.getValue();
}
private cases: {[key: number]: reducerOrSubReducer};
constructor(private intialState: State) {
if (!this.cases) {
this.cases = {};
} else {
Object.keys(this.cases)
.map(num => {
this.cases[num] = this.cases[num].bind(this);
});
}
this.dispatcher
.scan((state, action) => this.scan(state, action), this.intialState)
.subscribe(state => this.state$.next(state));
}
public dispatch(action: Action): void {
this.dispatcher.next(action);
}
public dispatchNext(action: Action): void {
setTimeout(() => this.dispatcher.next(action), 0);
}
protected mergeReducer(
stateKey: string,
reducer: ReducerSimple<State, Action>
): void {
reducer.state$ = this.state$;
reducer.dispatcher = this.dispatcher;
reducer.dispatch = this.dispatch;
reducer.dispatchNext = this.dispatchNext;
Object.keys(reducer.cases)
.map(num => {
this.cases[num] = {
stateKey,
reducer: reducer.cases[num].bind(reducer)
};
});
}
private scan(state: State, action: Action): State {
if (action.type in this.cases) {
const match = this.cases[action.type];
if (isFunction(match)) {
return match(state, action);
} else {
const newState = mergeDeep(state);
newState[match.stateKey] = match.reducer(state[match.stateKey], action);
return newState;
}
} else {
return state;
}
}
}
// lib imports
import { Component } from '@angular/core';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/do';
// external imports
import { VideoReducer, VideoActions } from '@wu/common/video.reducer';
@Component({
moduleId: __filename,
selector: 'video-thumbnails',
templateUrl: `./templates/video-thumbnails.html`
})
export class VideoThumbnails {
// comes from API
public videos: any[];
public state = this.videoReducer.currentState;
constructor(public videoReducer: VideoReducer) { }
public play(index: number): void {
if (index in this.videos) {
this.videoReducer.dispatch({
type: VideoActions.PLAY_VIDEO,
payload: {
index,
video: this.videos[index]
}
});
}
}
}
<div id="playlist">
<div id="playlist-topstories">
<ul class="row small-up-2 medium-up-3 large-up-4">
<li class="column" *ngFor="let video of videos; let i = index">
<div class="thumb-div shadow-box"
(click)="play(i)"
[attr.id]="video.source_guid"
[class.selected]="i === (state | async)?.index">
<img class="thumb-image"
[attr.src]="video.mediaUrl"
[attr.alt]="video.title" />
<div class="thumb-title">{{ video.title }}&nbsp;</div>
</div>
</li>
</ul>
</div>
</div>
// lib imports
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/scan';
import 'rxjs/add/operator/share';
import 'rxjs/add/operator/startWith';
// external imports
import { Reducer, IAction, ReducerMethod } from '@wu/common/reducer';
export class VideoState {
public video: any = {};
public ended = false;
public index = -1;
}
export const enum VideoActions {
/**
* Payload sould contain the video and index
*/
PLAY_VIDEO,
/**
* Payload can be empty
*/
END_VIDEO
}
type State = VideoState
type Action = IAction<VideoActions>
@Injectable()
export class VideoReducer extends Reducer<State, Action> {
constructor() {
super(new VideoState());
}
@ReducerMethod(VideoActions.PLAY_VIDEO)
protected playVideo(state: State, action: Action): State {
return Object.assign({}, state, {
video: action.payload.video,
ended: false,
index: action.payload.index
});
}
@ReducerMethod(WuVideoActions.END_VIDEO)
protected endVideo(state: State, action: Action): State {
return Object.assign({}, state, {ended: true});
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment