こんな感じにすれば良さそう
import { h, app, ActionsType, View } from "hyperapp";
namespace Counter {
export interface State {
count: number;
}
export interface Actions {
down(): State;
up(): State;
}
export const state: State = {
count: 0
};
export const actions: ActionsType<State, Actions> = {
down: () => state => {
if (state.count > 0) {
return { count: state.count - 1 };
}
return state;
},
up: () => state => ({
count: state.count + 1
})
};
export const view: View<State, Actions> = (state, actions) => (
<div>
<div>{state.count}</div>
<button onclick={actions.down}>-</button>
<button onclick={actions.up}>+</button>
</div>
);
}
namespace CounterTwice {
export interface State extends Counter.State {}
export interface Actions extends Counter.Actions {}
export const state: State = Counter.state;
export const actions: ActionsType<State, Actions> = {
down: () => state => {
if (state.count > 0) {
return { count: state.count - 2 };
}
return state;
},
up: () => state => ({
count: state.count + 2
})
};
export const view = Counter.view;
}
namespace MultiCounter {
export interface State {
c1: Counter.State;
c2: CounterTwice.State;
c3: Counter.State;
}
export interface Actions {
c1: Counter.Actions;
c2: CounterTwice.Actions;
c3: Counter.Actions;
alldown(): void;
allup(): void;
}
export const state: State = {
c1: Counter.state,
c2: CounterTwice.state,
c3: Counter.state
};
export const actions: ActionsType<State, Actions> = {
c1: Counter.actions,
c2: CounterTwice.actions,
c3: Counter.actions,
allup: () => (state, actions) => {
actions.c1.up();
actions.c2.up();
actions.c3.up();
return;
},
alldown: () => (state, actions) => {
actions.c1.down();
actions.c2.down();
actions.c3.down();
return;
}
};
export const view: View<MultiCounter.State, MultiCounter.Actions> = (
state,
actions
) => (
<div>
{Counter.view(state.c1, actions.c1)}
{CounterTwice.view(state.c2, actions.c2)}
{Counter.view(state.c3, actions.c3)}
<button onclick={actions.alldown}>-</button>
<button onclick={actions.allup}>+</button>
</div>
);
}
const main = app<MultiCounter.State, MultiCounter.Actions>(
MultiCounter.state,
MultiCounter.actions,
MultiCounter.view,
document.body
);
// hot reload対策
if ((window as any).isSet === undefined) {
setInterval(main.c3.up, 250, 1);
setInterval(main.c3.down, 500, 1);
}
(window as any).isSet = true;
ハマリポイントはallup: () => (state, actions)
のところくらいです。
引数にactions
を省略すると補完が効かなくなり悩みました。
routerもTypeScriptから使いたい
大きめのアプリを書くにはrouterも必要そうですよね。
現状本家のrouterの型情報がないみたいなので作りました
mytypes/@hyperapp/router.d.ts
に配置して下さい。
mytypes/@hyperapp/router.d.ts
import { VNode } from "hyperapp";
/** Link */
interface LinkProps {
to: string;
location?: Location;
}
export function Link(props: LinkProps): VNode<LinkProps>;
/** Route */
interface Match<P> {
url: string;
path: string;
isExact: boolean;
params: P;
}
interface RenderProps<P> {
location: Location;
match: Match<P>;
}
interface RouteProps<P> {
parent?: boolean;
path: string;
location?: Location;
render: (props: RenderProps<P>) => VNode<RenderProps<P>>;
}
export function Route<P>(
props: RouteProps<P>
): VNode<RenderProps<P>> | undefined;
/**Switch */
export function Switch<P>(
props: object,
children: Array<VNode<Match<P>>>
): VNode<object>;
/** Redirect */
type RedirectProps = LinkProps;
export function Redirect(props: RedirectProps): VNode<RedirectProps>;
/** location */
interface LocationState {
pathname: string;
previous: string;
}
interface LocationActions {
go: (pathname: string) => void;
set: (data: LocationState) => LocationState;
}
interface RouterLocation {
state: LocationState;
actions: LocationActions;
subscribe: (actions: LocationActions) => Function;
}
export declare const location: RouterLocation;
tsconfigに自作の型情報を読むための設定を追加します
{
"compilerOptions": {
"strict": true,
"jsx": "react",
"jsxFactory": "h",
"baseUrl": ".",
"sourceMap": true,
"paths": {
"*": ["mytypes/*", "node_modules/@types/*", "*"]
}
}
}
parcelからtypescript + hyperappが使えますが、
"sourceMap": true,
にすることで、TypeScriptデバッグできるようにもなっているみたいです。
routerの書き方
react-routerとおんなじ感じです
sample-code
import { h, app, View, ActionsType } from "hyperapp";
import {
Link,
Route,
RenderProps,
location,
LocationActions,
LocationState
} from "@hyperapp/router";
const Home = () => <h2>Home</h2>;
const About = () => <h2>About</h2>;
const Topic = ({ match }: RenderProps<{ topicId: string }>) => (
<h3>{match.params.topicId}</h3>
);
const TopicsView = ({ match }: RenderProps<{ topicId: string }>) => (
<div>
<h2>Topics</h2>
<ul>
<li>
<Link to={`${match.url}/components`}>Components</Link>
</li>
<li>
<Link to={`${match.url}/single-state-tree`}>Single State Tree</Link>
</li>
<li>
<Link to={`${match.url}/routing`}>Routing</Link>
</li>
</ul>
{match.isExact && <h3>Please select a topic.</h3>}
<Route parent path={`${match.path}/:topicId`} render={Topic} />
</div>
);
interface RouteState {
location: LocationState;
}
const state: RouteState = {
location: location.state
};
interface RouteActions {
location: LocationActions;
}
const routeActions: ActionsType<RouteState, RouteActions> = {
location: location.actions
};
const view: View<RouteState, RouteActions> = (state: RouteState) => (
<div>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/about">About</Link>
</li>
<li>
<Link to="/topics">Topics</Link>
</li>
</ul>
<hr />
<Route path="/" render={Home} />
<Route path="/about" render={About} />
<Route parent path="/topics" render={TopicsView} />
</div>
);
const main = app(state, routeActions, view, document.body);
const unsubscribe = location.subscribe(main.location);
最後に
hyperappはREADMEだけで使いかたが完結していて凄く使いやすそうです。
READMEを翻訳してみたので参照してみて下さい