jetix
v0.2.3
Published
Type-safe HyperScript components using pure functions.
Downloads
10
Maintainers
Readme
Jetix
Minimal wiring for TypeScript components made of pure functions.
- Effects as data for separation and cleaner tests
- Snabbdom VDOM for a unidirectional data flow
- hyperscript-helpers means the view is just functions
- Optimized for fewer renders/patches
- High type coverage
Also contains lightweight prevention of anti-patterns like state mutation and manually calling declarative actions.
Examples:
- Single page app demo [ source ]
- Hello World [ source ]
Actions and tasks
The component
callback receives an object exposing action
, task
, rootAction
and rootTask
functions.
export default component(
({ action, task, rootAction, rootTask }) => ({
// Initial action
init: action( "ShowMessage", { text: "Hello World!" } ),
})
);
Props and state
All action
handlers, task
callbacks and view
functions receive props
, state
and rootState
via a Context
input.
view(id, { props, state, rootState }) {
return div(`#${id}-message`, [
// Render from props and state
h1(props.title),
div(state.text)
]);
}
Hello World!
import { component, html, mount, Config, Next, Task, VNode } from "jetix";
import { setDocTitle} from "../services/browser";
const { div } = html;
export type Props = Readonly<{
placeholder: string;
}>;
export type State = Readonly<{
text: string;
done: boolean;
}>;
export type Actions = Readonly<{
ShowMessage: { text: string };
PageReady: { done: boolean };
}>;
export type Tasks = Readonly<{
SetDocTitle: { title: string };
}>;
type Component = {
Props: Props;
State: State;
Actions: Actions;
Tasks: Tasks;
};
const app = component<Component>(
({ action, task }): Config<Component> => ({
// Initial state
state: ({ placeholder }): State => ({
text: placeholder,
done: false
}),
// Initial action
init: action(
"ShowMessage",
{ text: "Hello World!" }
),
// Action handlers return new state, and any next actions/tasks
actions: {
ShowMessage: ({ text }, { state }): { state: State; next: Next } => {
return {
state: { ...state, text },
next: task("SetDocTitle", { title: text })
};
},
PageReady: ({ done }, { state }): { state: State } => {
return {
state: { ...state, done }
};
},
},
// Task handlers provide callbacks for effects and async operations that may fail
tasks: {
SetDocTitle: ({ title }): Task<null, State> => ({
perform: (): Promise<void> => setDocTitle(title),
success: (): Next => action("PageReady", { done: true }),
failure: (): Next => action("PageReady", { done: false })
})
},
// View renders from props & state
view(id, { state }): VNode {
return div(`#${id}-message`, [
div(state.text),
div(state.done ? '✅' : '❎')
]);
}
})
);
document.addEventListener(
"DOMContentLoaded",
(): void => mount({ app, props: { placeholder: "Loading" } })
);
export default app;
Unit tests
For tests the action
and task
functions just return data, so component logic can be tested without mocks.
import { testComponent, NextData } from "jetix";
import app, { State } from "./app";
describe("App", () => {
const { action, task, config, initialState } = testComponent(app, { placeholder: "placeholder" });
it("should set initial state", () => {
expect(initialState).toEqual({ text: "placeholder", done: false });
});
it("should run initial action", () => {
expect(config.init).toEqual({
name: "ShowMessage",
data: { text: "Hello World!" }
});
});
describe("'ShowMessage' action", () => {
const { state, next } = action<State>("ShowMessage", { text: "Hello World!"});
it("should update state", () => {
expect(state).toEqual({
...initialState,
text: "Hello World!"
});
});
it("should return next", () => {
const { name, data } = next as NextData;
expect(name).toBe("SetDocTitle");
expect(data).toEqual({ title: "Hello World!" });
});
});
describe("'SetDocTitle' task", () => {
const { perform, success, failure } = task("SetDocTitle", { title: "test" });
it("should provide perform", () => {
expect(perform).toBeDefined();
});
it("should handle success", () => {
const { name, data } = success() as NextData;
expect(name).toBe("PageReady");
expect(data).toEqual({ done: true });
});
it("should handle failure", () => {
const { name, data } = failure() as NextData;
expect(name).toBe("PageReady");
expect(data).toEqual({ done: false });
});
});
});