From 1ff7350a636fe869d970fd8bbabf75426ac1a5aa Mon Sep 17 00:00:00 2001 From: William Billingsley <wbilling@une.edu.au> Date: Tue, 7 Aug 2018 18:34:58 +1000 Subject: [PATCH] Added React instructions --- README.md | 203 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 180 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index b085477..e370eb1 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ export interface GameConfig { The next task I would suggest is just creating this object and rendering it to a little UI using your chosen framework. That'll let you fiddle around with the layout. -Then, wire up the functionality. Note that you'll probably need to change `life`. At the moment, it's a constant in that file. You will probably want it to be a variable that can be set by calling a function. +Then, wire up the functionality. Note that you'll probably need to change `life`. At the moment, it's a constant in the render file. You will probably want it to be a variable that can be set by calling a function. ## Solving this with Vue @@ -301,7 +301,7 @@ Reload and check that logs to the browser console when you hit the button (and t We need to let our component actually configure our `Life` instance. -First, in `life.ts`, let's turn `life` into a variable, and also add a create function that accepts a `GameConfig` and a function for getting the value of life. +First, in `render.ts`, let's turn `life` into a variable, and also add a create function that accepts a `GameConfig` and a function for getting the value of life. ```ts import { GameConfig } from "./gameConfig" @@ -469,60 +469,217 @@ import * as React from "react" import * as ReactDOM from "react-dom" ``` -Then let's define our GameConfig type: +Then let's define two types -- one for the config's definable properties, and another that has some additional state: ```tsx -export interface GameConfig { +export interface ConfigProps { w:number h:number - started:boolean +} +``` + +```tsx +export interface ConfigState extends ConfigProps { created:boolean period:number + intervalId?: number } ``` +intervalId is optional -- hence the `?` + And then let's define a React component: ```tsx -export class ConfigView extends React.Component<GameConfig, {}> { +export class ConfigView extends React.Component<ConfigProps, ConfigState> { render() { return <div>This will render some game config</div> } + + constructor(props:ConfigProps) { + super(props) + this.state = { + ...props, + created: false, + period: 500 + } + } + } ``` -Notice that in the angle brackets, we've said this component's properties are defined by the GameConfig interface -- so, it has properties for `w`, `h`, etc. +Notice that in the angle brackets, we've said this component's properties are defined by the ConfigProps interface -- so, it has properties for `w` and`h`. +And we've said its state is defined by the `ConfigState` interface. -Let's go back to `index.tsx` and use it. +In the constructor, we've also taken the component's properties, and augmented them with the missing compulsory variables. `...props` is the "spread" operator that sets all the name-value pairs from +`props` on the new object we're creating. -First, we need to import `GameConfig` and `ConfigView`: +Let's go back to `index.tsx` and use our component. -```tsx -import { GameConfig, ConfigView } from "./gameConfig" -``` - -Then we need to create some config: +First, we need to import `ConfigView`: ```tsx -let config:GameConfig = { - w: 40, - h: 20, - created: false, - started: false, - period: 500 -} +import { ConfigView } from "./gameConfig" ``` and then let's alter our React call to use it: ```tsx ReactDOM.render( - <ConfigView {...config}></ConfigView>, + <ConfigView w={40} h={20}></ConfigView>, document.querySelector("#app") ); ``` -We've used the *spread* operator `{...config}` here to set all of the ConfigView's props at once. +Notice that we've been able to set the `w` and `h` properties as attributes on the component. This sets them in the `props` object. Reload and see if the message is in the UI. +### Render some controls + +It's time to get our ConfigView to render some real controls. Let's alter `gameConfig.tsx`. + +```tsx + render() { + return <div> + Cols: <input type="number" value={this.state.w} onChange={(e) => this.setW(+e.target.value)} /> + Rows: <input type="number" value={this.state.h} onChange={(e) => this.setH(+e.target.value)} /> + <button onClick={(e) => this.create()}>Create</button> + </div> + } +``` + +In this, we've bound the input fields to render the `w` and `h` fields from the current state. + +We've then needed to bind an `onChange` handler for each field. This takes a lambda function. +The parameter `(e)` is the event that has happened. And we're getting `e.target.value` from it to get the number out +of the input field. This comes out as a string, even though the input field is for numbers. So in order to convert it +to a number, we use `+e.target.value`. This then needs to get passed to `this.setW` and `this.setH` -- methods on our class. So we'd better write those! + +Likewise, we've boud the button's click handler to `this.create()`. + +Let's write `setW` and `setH`, which are methods on the `ConfigView` class: + +```tsx + setW(w:number) { + this.setState((state:ConfigState) => { + state.w = w + return state + }) + } + + setH(h:number) { + this.setState((state:ConfigState) => { + state.h = h + return state + }) + } +``` + +In both cases, we call `setState`, which is a method from the React API (remember, `ConfigView` inherits from `React.Component`). `setState` gives you the current state, and asks you to return the new state. It will then automatically trigger re-rendering the component. + +Now let's write `create` method, and just get it to log to the console: + +```tsx + create() { + console.log("I was clicked") + } +``` + +Reload the page, and check the code so far works + +### Making it create the Life game + +Let's alter `render.ts`. + +First, import the ConfigProps interface + +```ts +import { ConfigProps } from "./gameConfig" +``` + +and now let's make `life` a variable instead of a constant. We'll initialise it to a zero-sized game so it won't be null but won't be visible on the screen. And we'll define a `create` method that takes ConfigProps. + +```ts +let life = new Life(0, 0) +``` + +```ts +function create(config:ConfigProps) { + life = new Life(config.w, config.h) +} +``` + +We'd best make sure those are exported: + +```ts +export { + life as life, + render as render, + create as create +} +``` + +Now let's go back to `gameConfig.tsx` and make it set up the game by altering what happens when we click the button + +First, let's import the functions from render.ts + +```tsx +import { life, create, render} from "./render" +``` + +Then let's alter the `create` method on `ConfigView`: + +```tsx + create() { + create(this.state) // note this calls the function imported from render.ts + render() // note this calls the function imported from render.ts + } +``` + +Reload, and check it works + + +### Setting up the timer + +The last part we'll do is setting up the timer. In `gameConfig.tsx`, let's alter the render() method for our component: + +```tsx + render() { + return <div> + Cols: <input type="number" value={this.state.w} onChange={(e) => this.setW(+e.target.value)} /> + Rows: <input type="number" value={this.state.h} onChange={(e) => this.setH(+e.target.value)} /> + <button onClick={(e) => this.create()}>Create</button> + + Period: <input type="number" value={this.state.period} onChange={(e) => this.setPeriod(+e.target.value)} /> + <button onClick={(e) => this.toggleTimer()}>{ this.state.intervalId ? "Stop" : "Start" }</button> + </div> +``` + +We've added an input field, very similar to the ones for `w` and `h` but operating on `period`. + +We've also added a button that will call `toggleTimer`, which we need to define. Its text is set by a ternary expression on whether `intervalId` in the component's state is defined. + +`toggleTimer` can work as another call to `setState`. Only this time, the call is also going to either start or cancel a JavaScript timer. We need to use `window.setInterval` because in Node setInterval has a different return type than in the browser -- calling `window.setInterval` makes sure TypeScript uses the browser's version. + +```ts + toggleTimer() { + this.setState((state:ConfigState) => { + + if (state.intervalId) { + clearInterval(state.intervalId) + state.intervalId = undefined + return state + } else { + state.intervalId = window.setInterval(() => { + life.stepGame() + render() + }, this.state.period) + return state + } + + }) + } +``` + +Reload, and see if it all works! \ No newline at end of file -- GitLab