ababup1192
12/19/2016 - 11:38 PM

関数型ReactでビジュアルFizzBuzz ref: http://qiita.com/ababup1192/items/bf6d25845a4556a9e0ca

関数型ReactでビジュアルFizzBuzz ref: http://qiita.com/ababup1192/items/bf6d25845a4556a9e0ca

// 複数のイベントに対応するためにBusを増やす。
const addItemBus: Bacon.Bus<any, any> = new Bacon.Bus();
const removeItemBus: Bacon.Bus<any, any> = new Bacon.Bus();

// 2つのイベント
const addItem = (list: List<string>, message: string): List<string> => 
  list.push(message);
const removeItem = (list: List<string>, message:   string): List<string> =>  
  list.remove(list.indexOf(message));

// イベントとBusの紐付け。型パラメータが少し複雑・・・。
// update<addItem, removeItem, 畳み込んだ後の型, init>
// => update<string, string, List<string>, List<string>>
const property = Bacon.update<string, string, List<string>, List<string>>(List<string>(),
  [addItemBus], addItem,
  [removeItemBus], removeItem
);

// 変わらず、畳み込み演算(イベントの監視)開始。
property.onValue((list) => console.log(list));

console.log("----");
addItemBus.push("hello");
addItemBus.push("baconjs");
addItemBus.push("world");
console.log("~~~~");
removeItemBus.push("baconjs");
// List []
// -------
// List [ "hello" ]
// List [ "hello", "baconjs" ]
// List [ "hello", "baconjs", "world" ]
// ~~~~
// List [ "hello", "world" ]
// カスタムイベントBusの生成。
const bus: Bacon.Bus<any, any> = new Bacon.Bus();

// 畳み込み演算に利用するための関数。
// 現在の文字列の長さを得るのに前の値は要らない。
const func = (_, message: string): number => message.length;

  // 生成したBusと関数funcの紐付け。長さの初期値を-1としておく。
  // 型パラメータは、update<func, init> => <message, length, init>
  const property = Bacon.update<string, number, number>(-1,
   [bus], func
);

// 畳み込み演算の開始
property.onValue((length) => console.log(length));

// Busにpushでイベントを発火
console.log("----");
bus.push("hello");
console.log("^^^^");
bus.push("FRP");
console.log("~~~~");
bus.push("world!");

// -1
// ----
// 5
// ^^^^
// 3
// ~~~~
// 6
};
["hello", "FRP", "world!"].reduce((_, message: string) => 
  console.log(message.length)
)
[event, event, event, event].reduce((currentState, e) => 
    // currentStateとeを使用した新しいstate
);
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].reduce((sum, x) => sum + x);
// => 55
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].reduce((sum, x) => sum + x);
// => 55
interface IMaxInputProps {
  max: number;
  handleMaxChange: (max: number) => void;
}

interface IMaxInputState {
  max: number;
}
/**** MaxInput.tsx *****/
<input
  type="number"
  value={this.state.max}
  onChange={(e) => {
    const max = Number(e.target.value);
    const validatedMax = Math.abs(max) > 1000 ? 1000 : Math.abs(max);
    handleMaxChange(validatedMax);
    this.setState({ max: validatedMax });
} } />

/**** App.tsx *****/
class App extends React.Component {
  // 中略
  private handleMaxChange(max: number) {
    // Object.assign(this.state, {list: Range(...)}); と同じ。
    this.setState({ ...this.state, ...{ list: Range(1, max + 1).toList() } });
  }

  render{
      // 中略
     return ...
       <MaxInput
         max={max}
         handleMaxChange={(max) => this.handleMaxChange(max)} />
  }
}
/**** index.tsx *****/
ReactDOM.render(<App max={100} fizz={3} buzz={5} />, document.getElementById("content"));

/**** App.tsx *****/
interface IAppProps {
    max: number;
    fizz: number;
    buzz: number;
}

interface IAppState {
    list: List<number>;
    fizz: number;
    buzz: number;
}

export default class App extends React.Component<IAppProps, IAppState> {
    constructor(props) {
        super(props);

        const list = Range(1, this.props.max + 1).toList();
        const {fizz, buzz, max} = this.props;

        this.state = { list, fizz, buzz };
    }

    // 中略

    render() {
        const {max, fizz, buzz} = this.props;

        return <div>
            <div className="fizzbuzzInputs">
                <MaxInput
                    max={max}
                    handleMaxChange={(max) => this.handleMaxChange(max)} />
                <FizzBuzzInput
                    fizz={fizz}
                    buzz={buzz}
                    handleFizzChange={(fizz) => this.handlFizzChange(fizz)}
                    handleBuzzChange={(buzz) => this.handlBuzzChange(buzz)}
                    />
            </div>
            <FizzBuzzContainer {...this.state} />
        </div>;
    }
}
const initialMax = 100;
const initialFizz = 3;
const initialBuzz = 5;

const dispatcher = new Dispatcher();
const maxAction = new MaxAction(dispatcher, initialMax);
const maxProperty = maxAction.createProperty();
const fizzbuzzAction = new FizzbuzzAction(dispatcher, initialFizz, initialBuzz);
const fizzbuzzProperty = fizzbuzzAction.createProperty();

Bacon.onValues(maxProperty, fizzbuzzProperty, (max: number, fizzBuzz: IFizzbuzz) => {
  const { fizz, buzz } = fizzBuzz;
  const fizzbuzzList = FizzBuzz.createFizzbuzzList(max, fizz, buzz);
  const props = { fizz, buzz, max, fizzbuzzList, maxAction, fizzbuzzAction };

  ReactDOM.render(<App {...props} />, document.getElementById("content"));
});
import { List, is } from "immutable";
import { FizzBuzz } from "../fizzbuzz";

describe("fizzbuzz", () => {
  it("should return fizzbuzz list", () => {
     const expected = List.of("1", "2", "fizz", "4", "buzz",
       "fizz", "7", "8", "fizz", "buzz", "11", "fizz", "13", "14", "fizzbuzz");
     const actual = FizzBuzz.createFizzbuzzList(15, 3, 5);

     expect(is(expected, actual)).toBeTruthy();
  });

  it("should return normal number", () => {
    const expected = 50;
    const actual = FizzBuzz.validateMax(50);

    expect(expected).toBe(actual);
  });

  it("should return abs of negative number", () => {
    const expected = 50;
    const actual = FizzBuzz.validateMax(-50);

    expect(expected).toBe(actual);
  });

  it("should return limited number", () => {
    const expected = 1000;
    const actual = FizzBuzz.validateMax(99999);

    expect(expected).toBe(actual);
  });
});
/**** maxAction.ts ****/
import * as Bacon from "baconjs";
import Dispatcher from "./dispatcher";
import { FizzBuzz } from "../models/fizzbuzz";

const CHANGE_MAX = "CHANGE_MAX";

export default class MaxAction {
  private d: Dispatcher;
  private initialValue: number;

  constructor(dispatcher: Dispatcher, initialValue: number) {
    this.d = dispatcher;
     this.initialValue = initialValue;
  }

  public changeMax(max: number) {
    this.d.push(CHANGE_MAX, max);
  }

  public createProperty(): Bacon.Property<number, number> {
    return Bacon.update<number, number, number>(this.initialValue,
      [this.d.stream(CHANGE_MAX)], this._changeMax.bind(this)
    );
   }

  private _changeMax(_, newMax: number): number {
    return FizzBuzz.validateMax(newMax);
  }
}

/**** fizzbuzzAction.ts ****/
const CHANGE_FIZZ = "CHANGE_FIZZ";
const CHANGE_BUZZ = "CHANGE_BUZZ";

export interface IFizzbuzz {
  fizz: number;
  buzz: number;
}

export class FizzbuzzAction {
  private d: Dispatcher;
  private initialFizz: number;
  private initialBuzz: number;

  constructor(dispatcher: Dispatcher, initialFizz: number, initialBuzz: number) {
      this.d = dispatcher;
      this.initialFizz = initialFizz;
      this.initialBuzz = initialBuzz;
  }

  public changeFizz(fizz: number) {
    this.d.push(CHANGE_FIZZ, fizz);
  }

  public changeBuzz(buzz: number) {
    this.d.push(CHANGE_BUZZ, buzz);
  }

  public createProperty(): Bacon.Property<IFizzbuzz, IFizzbuzz> {
    const initialValue = { fizz: this.initialFizz, buzz: this.initialBuzz };

    return Bacon.update<IFizzbuzz, number, number, IFizzbuzz>(initialValue,
      [this.d.stream(CHANGE_FIZZ)], this._changeFizz.bind(this),
      [this.d.stream(CHANGE_BUZZ)], this._changeBuzz.bind(this));
    }

  private _changeFizz(oldFizzbuzz: IFizzbuzz, newFizz: number): IFizzbuzz {
    return { ...oldFizzbuzz, ...{ fizz: newFizz } };
  }
  private _changeBuzz(oldFizzbuzz: IFizzbuzz, newBuzz: number): IFizzbuzz {
    return { ...oldFizzbuzz, ...{ buzz: newBuzz } };
  }
}

/**** fizzbuzz.ts ****/
import { Range, List } from "immutable";

export namespace FizzBuzz {
  export function createFizzbuzzList(max: number, fizz: number, buzz: number): List<string> {
    const fizzbuzz = fizz * buzz;

    return Range(1, max + 1).map((n) =>
      n % fizzbuzz === 0 ? "fizzbuzz" :
      n % fizz === 0 ? "fizz" :
      n % buzz === 0 ? "buzz" :
      n.toString()).toList();
  }

  export function validateMax(max: number): number {
      return Math.abs(max) > 1000 ? 1000 : Math.abs(max);
    }
}
export default class Dispatcher {
  private handlers: Map<string, Bacon.Bus<any, any>>;

  constructor() {
    this.handlers = Map<string, Bacon.Bus<any, any>>();
  }
  // busを文字列で指定する
  public stream(name: string): Bacon.Bus<any, any> {
    return this.bus(name);
  }
  // 文字列で指定したbusにデータを流す
  public push(name: string, value: any): void {
    this.bus(name).push(value);
  }
  // busをplugする
  public plug(name: string, value: any): void {
    this.bus(name).plug(value);
  }
  // busを生成する
  private bus(name: string): Bacon.Bus<any, any> {
    if (this.handlers.has(name)) {
      return this.handlers.get(name);
    } else {
      const newBus = new Bacon.Bus();
      this.handlers = this.handlers.set(name, newBus);
      return newBus;
    }
  }
}
class MessageAction {
  private bus: Bacon.Bus<any, any>;

  constructor() {
      this.bus = new Bacon.Bus();
  }

  // イベントを発火するメソッドを用意
  pushMessage(message: string) {
      this.bus.push(message);
  }

  createProperty(): Bacon.Property<string, number> {
    return Bacon.update<string, number, number>(-1,
          [this.bus], this._pushMessage.bind(this)
      );
  }

  // 畳み込みに使われる関数内容は、private
  private _pushMessage(_, message: string): number {
    return message.length;
  }
}

class ListAction {
  private addItemBus: Bacon.Bus<any, any>;
  private removeItemBus: Bacon.Bus<any, any>;

  constructor() {
      this.addItemBus = new Bacon.Bus();
      this.removeItemBus = new Bacon.Bus();
  }

  addItem(message: string) {
      this.addItemBus.push(message);
  }

  removeItem(message: string) {
      this.removeItemBus.push(message);
  }

  createProperty(): Bacon.Property<string, List<string>> {
    return Bacon.update<string, string, List<string>, List<string>>(List<string>(),
      [this.addItemBus], this._addItem.bind(this),
       [this.removeItemBus], this._removeItem.bind(this)
    );
  }

  private _addItem(list: List<string>, message: string): List<string> {
    return list.push(message);
  };

  private _removeItem(list: List<string>, message: string): List<string> {
    return list.remove(list.indexOf(message));
  };
}

// Busの生成は、コンストラクタに任せる。
const messageAction = new MessageAction();
const listAction = new ListAction();

const messageProperty = messageAction.createProperty();
const listProperty = listAction.createProperty();

// Bacon.onValuesメソッドで、複数の畳込み演算をマージできる。
// ユーザは、マージされた結果のみに集中して処理を書ける。
Bacon.onValues(messageProperty, listProperty, 
  (length: number, list: List<string>) => {
    console.log("-------");
    console.log(`length = ${length}`);
    console.log(`list = ${list}`);
    console.log("-------");
});

// イベントの発火は、Action経由で行う。
messageAction.pushMessage("hello");
listAction.addItem("hello");

messageAction.pushMessage("baconjs");
listAction.addItem("baconjs");

messageAction.pushMessage("world");
listAction.addItem("world");

listAction.removeItem("baconjs");

// -------
// length = -1
// list = List []
// -------
// -------
// length = 5
// list = List []
// -------
// -------
// length = 5
// list = List [ "hello" ]
// -------
// -------
// length = 7
// list = List [ "hello" ]
// -------
// -------
// length = 7
// list = List [ "hello", "baconjs" ]
// -------
// -------
// length = 5
// list = List [ "hello", "baconjs" ]
// -------
// -------
// length = 5
// list = List [ "hello", "baconjs", "world" ]
// -------
// -------
// length = 5
// list = List [ "hello", "world" ]
// -------
<div id="messages">
   <p>Hello.</p>
   <p>How are you?</p>
   <p>I'm FIne!</p>
</div>
<div id="messages">
   <p>Hello.</p>
   <p>How are you?</p>
   <p>I'm FIne!</p>
</div>
function createParagraph(text) {
    const paragraph = document.createElement("p");
    const textNode = document.createTextNode(text);
    paragraph.appendChild(textNode);
    return paragraph;
}

const messageTexts = ["Hello.", "How are you?", "I'm Fine!"];
const messages = document.getElementById("messages");

messageTexts.map(createParagraph).forEach(function (message) {
    messages.appendChild(message);
});
function createParagraph(text) {
    const paragraph = document.createElement("p");
    const textNode = document.createTextNode(text);
    paragraph.appendChild(textNode);
    return paragraph;
}

const messageTexts = ["Hello.", "How are you?", "I'm Fine!"];
const messages = document.getElementById("messages");

messageTexts.map(createParagraph).forEach(function (message) {
    messages.appendChild(message);
});