Skip to content
Snippets Groups Projects
Commit b135e650 authored by Will Billingsley's avatar Will Billingsley
Browse files

Merge branch 'master' into react-solution

parents 6f16f00b 1ff7350a
Branches react-solution
No related tags found
No related merge requests found
......@@ -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,69 @@ 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.
......@@ -530,3 +539,147 @@ Reload and see if the message is in the UI.
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!
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment