Step 2: Rendering items
The TodoItem component
We're going to want to have a component for rendering the items, so let's make one. Since it's small, we won't have it be its own file -- we'll use a nested module.
TodoApp_2_1.re
type item = {
title: string,
completed: bool
};
let str = ReasonReact.stringToElement;
module TodoItem = {
let component = ReasonReact.statelessComponent("TodoItem");
let make = (~item, children) => {
...component,
render: (self) =>
<input
_type="checkbox"
checked=(Js.Boolean.to_js_boolean(item.completed))
/* TODO make interactive */
/>
(str(item.title))
};
};
type state = {items: list(item)};
type action =
| AddItem;
let component = ReasonReact.reducerComponent("TodoApp");
let newItem = () => {title: "Click a button", completed: true};
let make = (children) => {
...component,
initialState: () => {
items: [{
title: "Write some things to do",
completed: false
}]
},
reducer: (action, {items}) =>
switch action {
| AddItem =>
ReasonReact.Update({items: [newItem(), ...items]})
},
render: ({state: {items}, reduce}) => {
let numItems = List.length(items);
(str("What to do"))
<button onClick=(reduce((_evt) => AddItem))>
(str("Add something"))
(
ReasonReact.arrayToElement(Array.of_list(
List.map((item) => , items)
))
)
(str(string_of_int(numItems) ++ " items"))
}
};
So this is another stateless component, except this one accepts a property: item. The ~argname syntax means "this function takes a labeled argument which is known as item both externally and internally". Swift and Objective C also allow you have labeled arguments, with an external name that is optionally different from the internal one. If you wanted them to be different, you would write e.g. (~externalFacingName as internalFacingName) =>. children is an unnamed argument.
In Reason, named arguments can be given in any order, but unnamed arguments cannot. So if you had a function let myfn = (~a, ~b, c, d) => {} where c was an int and d was a string, you could call it myfn(~b=2, ~a=1, 3, "hi") or myfn(~a=3, 3, "hi", ~b=1) but not myfn(~a=2, ~b=3, "hi", 4).
Rendering a list
Now that we've got a TodoItem component, let's use it! We'll replace the section that's currently just str("Nothing") with this:
TodoApp_2_1.re
type item = {
title: string,
completed: bool
};
let str = ReasonReact.stringToElement;
module TodoItem = {
let component = ReasonReact.statelessComponent("TodoItem");
let make = (~item, children) => {
...component,
render: (self) =>
<input
_type="checkbox"
checked=(Js.Boolean.to_js_boolean(item.completed))
/* TODO make interactive */
/>
(str(item.title))
};
};
type state = {items: list(item)};
type action =
| AddItem;
let component = ReasonReact.reducerComponent("TodoApp");
let newItem = () => {title: "Click a button", completed: true};
let make = (children) => {
...component,
initialState: () => {
items: [{
title: "Write some things to do",
completed: false
}]
},
reducer: (action, {items}) =>
switch action {
| AddItem =>
ReasonReact.Update({items: [newItem(), ...items]})
},
render: ({state: {items}, reduce}) => {
let numItems = List.length(items);
(str("What to do"))
<button onClick=(reduce((_evt) => AddItem))>
(str("Add something"))
(
ReasonReact.arrayToElement(Array.of_list(
List.map((item) => , items)
))
)
(str(string_of_int(numItems) ++ " items"))
}
};
In the center of all this you can see the function that takes our data and renders a react element.
(item) => ;
Another difference from JSX is that an attribute without a value is "punned", meaning that is the same as . In JSX, lone attributes are interpreted as .
ReasonReact.arrayToElement(Array.of_list(List.map(/.../ items)));
And now we've got the nuts and bolts of calling that function for each item and appeasing the type system. Another way to write the above is
List.map /.../ items |> Array.of_list |> ReasonReact.arrayToElement
The pipe |> is a left-associative binary operator that's defined as a |> b == b(a). It can be quite nice when you've got some data and you just need to pipe it through a list of conversions.
Tracking ids w/ a mutable ref
If you're familiar with React, you'll know that we really ought to be using a key to uniquely identify each rendered TodoItem, and in fact we'll want unique keys once we get around to modifying the items as well.
Let's add an id property to our item type, and add an id of 0 to our initialState item.
TodoApp_2_2.re
type item = {
id: int,
title: string,
completed: bool
};
let str = ReasonReact.stringToElement;
module TodoItem = {
let component = ReasonReact.statelessComponent("TodoItem");
let make = (~item, children) => {
...component,
render: (_) =>
<input
_type="checkbox"
checked=(Js.Boolean.to_js_boolean(item.completed))
/* TODO make interactive */
/>
(str(item.title))
};
};
type state = {
items: list(item)
};
type action =
| AddItem;
let component = ReasonReact.reducerComponent("TodoApp");
let lastId = 0;
let newItem = () => {
let lastId = lastId + 1;
{id: lastId, title: "Click a button", completed: true}
};
let make = (children) => {
...component,
initialState: () => {
items: [{
id: 0,
title: "Write some things to do",
completed: false
}]
},
reducer: (action, {items}) => switch action {
| AddItem => ReasonReact.Update({items: [newItem(), ...items]})
},
render: ({state: {items}, reduce}) => {
let numItems = List.length(items);
(str("What to do"))
<button onClick=(reduce((_evt) => AddItem))>
(str("Add something"))
(ReasonReact.arrayToElement(
Array.of_list(
List.map((item) => , items)
)
))
(str(string_of_int(numItems) ++ " items"))
}
};
But then, what do we do for the newItem function? We want to make sure that each item created has a unique id, and one way to do this is just have a variable that we increment by one each time we create a new item.
TodoApp_2_2.re
type item = {
id: int,
title: string,
completed: bool
};
let str = ReasonReact.stringToElement;
module TodoItem = {
let component = ReasonReact.statelessComponent("TodoItem");
let make = (~item, children) => {
...component,
render: (_) =>
<input
_type="checkbox"
checked=(Js.Boolean.to_js_boolean(item.completed))
/* TODO make interactive */
/>
(str(item.title))
};
};
type state = {
items: list(item)
};
type action =
| AddItem;
let component = ReasonReact.reducerComponent("TodoApp");
let lastId = 0;
let newItem = () => {
let lastId = lastId + 1;
{id: lastId, title: "Click a button", completed: true}
};
let make = (children) => {
...component,
initialState: () => {
items: [{
id: 0,
title: "Write some things to do",
completed: false
}]
},
reducer: (action, {items}) => switch action {
| AddItem => ReasonReact.Update({items: [newItem(), ...items]})
},
render: ({state: {items}, reduce}) => {
let numItems = List.length(items);
(str("What to do"))
<button onClick=(reduce((_evt) => AddItem))>
(str("Add something"))
(ReasonReact.arrayToElement(
Array.of_list(
List.map((item) => , items)
)
))
(str(string_of_int(numItems) ++ " items"))
}
};
Of course this won't work -- we're just defining a new variable that's only scoped to the newItem function. At the top level, lastId remains 0. In order to simulate a mutable let binding, we'll use a ref.
TodoApp_2_3.re
type item = {
id: int,
title: string,
completed: bool
};
let str = ReasonReact.stringToElement;
module TodoItem = {
let component = ReasonReact.statelessComponent("TodoItem");
let make = (~item, children) => {
...component,
render: (_) =>
<input
_type="checkbox"
checked=(Js.Boolean.to_js_boolean(item.completed))
/* TODO make interactive */
/>
(str(item.title))
};
};
type state = {
items: list(item)
};
type action =
| AddItem;
let component = ReasonReact.reducerComponent("TodoApp");
let lastId = ref(0);
let newItem = () => {
lastId := lastId^ + 1;
{id: lastId^, title: "Click a button", completed: true}
};
let make = (children) => {
...component,
initialState: () => {
items: [{
id: 0,
title: "Write some things to do",
completed: false
}]
},
reducer: (action, {items}) => switch action {
| AddItem => ReasonReact.Update({items: [newItem(), ...items]})
},
render: ({state: {items}, reduce}) => {
let numItems = List.length(items);
(str("What to do"))
<button onClick=(reduce((_evt) => AddItem))>
(str("Add something"))
(ReasonReact.arrayToElement(Array.of_list(
List.map(
(item) => , items
)
)))
(str(string_of_int(numItems) ++ " items"))
}
};
You update a ref with :=, and to access the value you dereference it with ^. Now we can add our key property to the components.
TodoApp_2_3.re
type item = {
id: int,
title: string,
completed: bool
};
let str = ReasonReact.stringToElement;
module TodoItem = {
let component = ReasonReact.statelessComponent("TodoItem");
let make = (~item, children) => {
...component,
render: (_) =>
<input
_type="checkbox"
checked=(Js.Boolean.to_js_boolean(item.completed))
/* TODO make interactive */
/>
(str(item.title))
};
};
type state = {
items: list(item)
};
type action =
| AddItem;
let component = ReasonReact.reducerComponent("TodoApp");
let lastId = ref(0);
let newItem = () => {
lastId := lastId^ + 1;
{id: lastId^, title: "Click a button", completed: true}
};
let make = (children) => {
...component,
initialState: () => {
items: [{
id: 0,
title: "Write some things to do",
completed: false
}]
},
reducer: (action, {items}) => switch action {
| AddItem => ReasonReact.Update({items: [newItem(), ...items]})
},
render: ({state: {items}, reduce}) => {
let numItems = List.length(items);
(str("What to do"))
<button onClick=(reduce((_evt) => AddItem))>
(str("Add something"))
(ReasonReact.arrayToElement(Array.of_list(
List.map(
(item) => , items
)
)))
(str(string_of_int(numItems) ++ " items"))
}
};
Step 3: Full interactivity
Checking off items
Now that our items are uniquely identified, we can enable toggling. We'll start by adding an onToggle prop to the TodoItem component, and calling it when the div gets clicked.
TodoApp_3_1.re
type item = {
id: int,
title: string,
completed: bool
};
let str = ReasonReact.stringToElement;
module TodoItem = {
let component = ReasonReact.statelessComponent("TodoItem");
let make = (~item, ~onToggle, children) => {
...component,
render: (_) =>
<div className="item" onClick=((_evt) => onToggle())>
(str(item.title))
};
};
type state = {
items: list(item)
};
type action =
| AddItem
| ToggleItem(int);
let component = ReasonReact.reducerComponent("TodoApp");
let lastId = ref(0);
let newItem = () => {
lastId := lastId^ + 1;
{id: lastId^, title: "Click a button", completed: true}
};
let make = (children) => {
...component,
initialState: () => {
items: [{
id: 0,
title: "Write some things to do",
completed: false
}]
},
reducer: (action, {items}) => switch action {
| AddItem => ReasonReact.Update({items: [newItem(), ...items]})
| ToggleItem(id) =>
let items = List.map(
(item) =>
item.id === id ?
{...item, completed: ! item.completed} : item,
items
);
ReasonReact.Update({items: items})
},
render: ({state: {items}, reduce}) => {
let numItems = List.length(items);
(str("What to do"))
<button onClick=(reduce((_evt) => AddItem))>
(str("Add something"))
(ReasonReact.arrayToElement(Array.of_list(
List.map(
(item) => <TodoItem
key=(string_of_int(item.id))
onToggle=(reduce(() => ToggleItem(item.id)))
item
/>, items
)
)))
(str(string_of_int(numItems) ++ " items"))
}
};
So onToggle has the type unit => unit. We now need to define another action, and the way to handle it. And then we pass the action creator to onToggle.
TodoApp_3_1.re
type item = {
id: int,
title: string,
completed: bool
};
let str = ReasonReact.stringToElement;
module TodoItem = {
let component = ReasonReact.statelessComponent("TodoItem");
let make = (~item, ~onToggle, children) => {
...component,
render: (_) =>
<div className="item" onClick=((_evt) => onToggle())>
(str(item.title))
};
};
type state = {
items: list(item)
};
type action =
| AddItem
| ToggleItem(int);
let component = ReasonReact.reducerComponent("TodoApp");
let lastId = ref(0);
let newItem = () => {
lastId := lastId^ + 1;
{id: lastId^, title: "Click a button", completed: true}
};
let make = (children) => {
...component,
initialState: () => {
items: [{
id: 0,
title: "Write some things to do",
completed: false
}]
},
reducer: (action, {items}) => switch action {
| AddItem => ReasonReact.Update({items: [newItem(), ...items]})
| ToggleItem(id) =>
let items = List.map(
(item) =>
item.id === id ?
{...item, completed: ! item.completed} : item,
items
);
ReasonReact.Update({items: items})
},
render: ({state: {items}, reduce}) => {
let numItems = List.length(items);
(str("What to do"))
<button onClick=(reduce((_evt) => AddItem))>
(str("Add something"))
(ReasonReact.arrayToElement(Array.of_list(
List.map(
(item) => <TodoItem
key=(string_of_int(item.id))
onToggle=(reduce(() => ToggleItem(item.id)))
item
/>, items
)
)))
(str(string_of_int(numItems) ++ " items"))
}
};
Let's look a little closer at the way we're handling ToggleItem:
TodoApp_3_1.re
type item = {
id: int,
title: string,
completed: bool
};
let str = ReasonReact.stringToElement;
module TodoItem = {
let component = ReasonReact.statelessComponent("TodoItem");
let make = (~item, ~onToggle, children) => {
...component,
render: (_) =>
<div className="item" onClick=((_evt) => onToggle())>
(str(item.title))
};
};
type state = {
items: list(item)
};
type action =
| AddItem
| ToggleItem(int);
let component = ReasonReact.reducerComponent("TodoApp");
let lastId = ref(0);
let newItem = () => {
lastId := lastId^ + 1;
{id: lastId^, title: "Click a button", completed: true}
};
let make = (children) => {
...component,
initialState: () => {
items: [{
id: 0,
title: "Write some things to do",
completed: false
}]
},
reducer: (action, {items}) => switch action {
| AddItem => ReasonReact.Update({items: [newItem(), ...items]})
| ToggleItem(id) =>
let items = List.map(
(item) =>
item.id === id ?
{...item, completed: ! item.completed} : item,
items
);
ReasonReact.Update({items: items})
},
render: ({state: {items}, reduce}) => {
let numItems = List.length(items);
(str("What to do"))
<button onClick=(reduce((_evt) => AddItem))>
(str("Add something"))
(ReasonReact.arrayToElement(Array.of_list(
List.map(
(item) => <TodoItem
key=(string_of_int(item.id))
onToggle=(reduce(() => ToggleItem(item.id)))
item
/>, items
)
)))
(str(string_of_int(numItems) ++ " items"))
}
};
We map over the list of items, and when we find the item to toggle we flip the completed boolean.
Text input
Having a button that always adds the same item isn't the most useful -- let's replace it with a text input. For this, we'll make a reducer component.
TodoApp_final.re
type item = {
id: int,
title: string,
completed: bool
};
let str = ReasonReact.stringToElement;
module TodoItem = {
let component = ReasonReact.statelessComponent("TodoItem");
let make = (~item, ~onToggle, children) => {
...component,
render: (_) =>
<div className="item" onClick=((evt) => onToggle())>
(str(item.title))
};
};
let valueFromEvent = (evt) : string => (
evt
|> ReactEventRe.Form.target
|> ReactDOMRe.domElementToObj
)##value;
module Input = {
type state = string;
let component = ReasonReact.reducerComponent("Input");
let make = (~onSubmit, _) => {
...component,
initialState: () => "",
reducer: (newText, _text) => ReasonReact.Update(newText),
render: ({state: text, reduce}) =>
<input
value=text
_type="text"
placeholder="Write something to do"
onChange=(reduce((evt) => valueFromEvent(evt)))
onKeyDown=((evt) =>
if (ReactEventRe.Keyboard.key(evt) == "Enter") {
onSubmit(text);
(reduce(() => ""))()
}
)
/>
};
};
type state = {
items: list(item)
};
type action =
| AddItem(string)
| ToggleItem(int);
let component = ReasonReact.reducerComponent("TodoApp");
let lastId = ref(0);
let newItem = (text) => {
lastId := lastId^ + 1;
{id: lastId^, title: text, completed: false}
};
let make = (children) => {
...component,
initialState: () => {
items: [{
id: 0,
title: "Write some things to do",
completed: false
}]
},
reducer: (action, {items}) =>
switch action {
| AddItem(text) => ReasonReact.Update({items: [newItem(text), ...items]})
| ToggleItem(id) =>
let items =
List.map(
(item) =>
item.id === id ?
{...item, completed: ! item.completed} : item,
items
);
ReasonReact.Update({items: items})
},
render: ({state: {items}, reduce}) => {
let numItems = List.length(items);
(str("What to do"))
<Input onSubmit=(reduce((text) => AddItem(text))) />
(
ReasonReact.arrayToElement(Array.of_list(List.map(
(item) =>
<TodoItem
key=(string_of_int(item.id))
onToggle=(reduce(() => ToggleItem(item.id)))
item
/>,
items
)))
)
(str(string_of_int(numItems) ++ " items"))
}
};
For this component, our state type is just string, because that's all we need to keep track of. In fact, for the TodoApp component we could have just had the list(item) be the state, but it was useful to show an example of a record. We only have one kind of action here as well, so we don't need to declare a separate action type -- we just use a string.
Most of this we've seen before, but the onChange and onKeyDown handlers are new.
TodoApp_final.re
type item = {
id: int,
title: string,
completed: bool
};
let str = ReasonReact.stringToElement;
module TodoItem = {
let component = ReasonReact.statelessComponent("TodoItem");
let make = (~item, ~onToggle, children) => {
...component,
render: (_) =>
<div className="item" onClick=((evt) => onToggle())>
(str(item.title))
};
};
let valueFromEvent = (evt) : string => (
evt
|> ReactEventRe.Form.target
|> ReactDOMRe.domElementToObj
)##value;
module Input = {
type state = string;
let component = ReasonReact.reducerComponent("Input");
let make = (~onSubmit, _) => {
...component,
initialState: () => "",
reducer: (newText, _text) => ReasonReact.Update(newText),
render: ({state: text, reduce}) =>
<input
value=text
_type="text"
placeholder="Write something to do"
onChange=(reduce((evt) => valueFromEvent(evt)))
onKeyDown=((evt) =>
if (ReactEventRe.Keyboard.key(evt) == "Enter") {
onSubmit(text);
(reduce(() => ""))()
}
)
/>
};
};
type state = {
items: list(item)
};
type action =
| AddItem(string)
| ToggleItem(int);
let component = ReasonReact.reducerComponent("TodoApp");
let lastId = ref(0);
let newItem = (text) => {
lastId := lastId^ + 1;
{id: lastId^, title: text, completed: false}
};
let make = (children) => {
...component,
initialState: () => {
items: [{
id: 0,
title: "Write some things to do",
completed: false
}]
},
reducer: (action, {items}) =>
switch action {
| AddItem(text) => ReasonReact.Update({items: [newItem(text), ...items]})
| ToggleItem(id) =>
let items =
List.map(
(item) =>
item.id === id ?
{...item, completed: ! item.completed} : item,
items
);
ReasonReact.Update({items: items})
},
render: ({state: {items}, reduce}) => {
let numItems = List.length(items);
(str("What to do"))
<Input onSubmit=(reduce((text) => AddItem(text))) />
(
ReasonReact.arrayToElement(Array.of_list(List.map(
(item) =>
<TodoItem
key=(string_of_int(item.id))
onToggle=(reduce(() => ToggleItem(item.id)))
item
/>,
items
)))
)
(str(string_of_int(numItems) ++ " items"))
}
};
The input's onChange prop is called with a Form event, from which we get the text value and use that as the new state.
TodoApp_final.re
type item = {
id: int,
title: string,
completed: bool
};
let str = ReasonReact.stringToElement;
module TodoItem = {
let component = ReasonReact.statelessComponent("TodoItem");
let make = (~item, ~onToggle, children) => {
...component,
render: (_) =>
<div className="item" onClick=((evt) => onToggle())>
(str(item.title))
};
};
let valueFromEvent = (evt) : string => (
evt
|> ReactEventRe.Form.target
|> ReactDOMRe.domElementToObj
)##value;
module Input = {
type state = string;
let component = ReasonReact.reducerComponent("Input");
let make = (~onSubmit, _) => {
...component,
initialState: () => "",
reducer: (newText, _text) => ReasonReact.Update(newText),
render: ({state: text, reduce}) =>
<input
value=text
_type="text"
placeholder="Write something to do"
onChange=(reduce((evt) => valueFromEvent(evt)))
onKeyDown=((evt) =>
if (ReactEventRe.Keyboard.key(evt) == "Enter") {
onSubmit(text);
(reduce(() => ""))()
}
)
/>
};
};
type state = {
items: list(item)
};
type action =
| AddItem(string)
| ToggleItem(int);
let component = ReasonReact.reducerComponent("TodoApp");
let lastId = ref(0);
let newItem = (text) => {
lastId := lastId^ + 1;
{id: lastId^, title: text, completed: false}
};
let make = (children) => {
...component,
initialState: () => {
items: [{
id: 0,
title: "Write some things to do",
completed: false
}]
},
reducer: (action, {items}) =>
switch action {
| AddItem(text) => ReasonReact.Update({items: [newItem(text), ...items]})
| ToggleItem(id) =>
let items =
List.map(
(item) =>
item.id === id ?
{...item, completed: ! item.completed} : item,
items
);
ReasonReact.Update({items: items})
},
render: ({state: {items}, reduce}) => {
let numItems = List.length(items);
(str("What to do"))
<Input onSubmit=(reduce((text) => AddItem(text))) />
(
ReasonReact.arrayToElement(Array.of_list(List.map(
(item) =>
<TodoItem
key=(string_of_int(item.id))
onToggle=(reduce(() => ToggleItem(item.id)))
item
/>,
items
)))
)
(str(string_of_int(numItems) ++ " items"))
}
};
In JavaScript, we'd do evt.target.value to get the current text of the input, and this is the ReasonReact equivalent. ReasonReact's bindings don't yet have a well-typed way to get the value of an input element, so we take the Dom.element that we got from ReactEventRe.Form.target, convert it into a "catch-all javascript object", and get out the value with the "magic accessor syntax" ##value.
This is sacrificing some type safety, and it would be best for ReasonReact to just provide a safe way to get the input text directly, but this is what we have for now. Notice that we've annotated the return value of valueFromEvent to be string. Without this, OCaml would make the return value 'a (because we used the catch-all JavaScript object) meaning it could unify with anything, similar to the any type in Flow.
TodoApp_final.re
type item = {
id: int,
title: string,
completed: bool
};
let str = ReasonReact.stringToElement;
module TodoItem = {
let component = ReasonReact.statelessComponent("TodoItem");
let make = (~item, ~onToggle, children) => {
...component,
render: (_) =>
<div className="item" onClick=((evt) => onToggle())>
(str(item.title))
};
};
let valueFromEvent = (evt) : string => (
evt
|> ReactEventRe.Form.target
|> ReactDOMRe.domElementToObj
)##value;
module Input = {
type state = string;
let component = ReasonReact.reducerComponent("Input");
let make = (~onSubmit, _) => {
...component,
initialState: () => "",
reducer: (newText, _text) => ReasonReact.Update(newText),
render: ({state: text, reduce}) =>
<input
value=text
_type="text"
placeholder="Write something to do"
onChange=(reduce((evt) => valueFromEvent(evt)))
onKeyDown=((evt) =>
if (ReactEventRe.Keyboard.key(evt) == "Enter") {
onSubmit(text);
(reduce(() => ""))()
}
)
/>
};
};
type state = {
items: list(item)
};
type action =
| AddItem(string)
| ToggleItem(int);
let component = ReasonReact.reducerComponent("TodoApp");
let lastId = ref(0);
let newItem = (text) => {
lastId := lastId^ + 1;
{id: lastId^, title: text, completed: false}
};
let make = (children) => {
...component,
initialState: () => {
items: [{
id: 0,
title: "Write some things to do",
completed: false
}]
},
reducer: (action, {items}) =>
switch action {
| AddItem(text) => ReasonReact.Update({items: [newItem(text), ...items]})
| ToggleItem(id) =>
let items =
List.map(
(item) =>
item.id === id ?
{...item, completed: ! item.completed} : item,
items
);
ReasonReact.Update({items: items})
},
render: ({state: {items}, reduce}) => {
let numItems = List.length(items);
(str("What to do"))
<Input onSubmit=(reduce((text) => AddItem(text))) />
(
ReasonReact.arrayToElement(Array.of_list(List.map(
(item) =>
<TodoItem
key=(string_of_int(item.id))
onToggle=(reduce(() => ToggleItem(item.id)))
item
/>,
items
)))
)
(str(string_of_int(numItems) ++ " items"))
}
};
ReasonReact does provide a nice function for getting the key off of a keyboard event. So here we check if they pressed Enter, and if they did we call the onSubmit handler with the current text and fire off an action to clear out the input.
And now we can replace that filler "Add something" button with this text input. We'll change the AddItem action to take a single argument, the text of the new item, and pass that to our newItem function.
TodoApp_final.re
type item = {
id: int,
title: string,
completed: bool
};
let str = ReasonReact.stringToElement;
module TodoItem = {
let component = ReasonReact.statelessComponent("TodoItem");
let make = (~item, ~onToggle, children) => {
...component,
render: (_) =>
<div className="item" onClick=((evt) => onToggle())>
(str(item.title))
};
};
let valueFromEvent = (evt) : string => (
evt
|> ReactEventRe.Form.target
|> ReactDOMRe.domElementToObj
)##value;
module Input = {
type state = string;
let component = ReasonReact.reducerComponent("Input");
let make = (~onSubmit, _) => {
...component,
initialState: () => "",
reducer: (newText, _text) => ReasonReact.Update(newText),
render: ({state: text, reduce}) =>
<input
value=text
_type="text"
placeholder="Write something to do"
onChange=(reduce((evt) => valueFromEvent(evt)))
onKeyDown=((evt) =>
if (ReactEventRe.Keyboard.key(evt) == "Enter") {
onSubmit(text);
(reduce(() => ""))()
}
)
/>
};
};
type state = {
items: list(item)
};
type action =
| AddItem(string)
| ToggleItem(int);
let component = ReasonReact.reducerComponent("TodoApp");
let lastId = ref(0);
let newItem = (text) => {
lastId := lastId^ + 1;
{id: lastId^, title: text, completed: false}
};
let make = (children) => {
...component,
initialState: () => {
items: [{
id: 0,
title: "Write some things to do",
completed: false
}]
},
reducer: (action, {items}) =>
switch action {
| AddItem(text) => ReasonReact.Update({items: [newItem(text), ...items]})
| ToggleItem(id) =>
let items =
List.map(
(item) =>
item.id === id ?
{...item, completed: ! item.completed} : item,
items
);
ReasonReact.Update({items: items})
},
render: ({state: {items}, reduce}) => {
let numItems = List.length(items);
(str("What to do"))
<Input onSubmit=(reduce((text) => AddItem(text))) />
(
ReasonReact.arrayToElement(Array.of_list(List.map(
(item) =>
<TodoItem
key=(string_of_int(item.id))
onToggle=(reduce(() => ToggleItem(item.id)))
item
/>,
items
)))
)
(str(string_of_int(numItems) ++ " items"))
}
};