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

Merge branch 'master' into vue-solution

parents 97bd1327 1ff7350a
No related branches found
No related tags found
No related merge requests found
...@@ -117,7 +117,7 @@ export interface GameConfig { ...@@ -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. 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 ## Solving this with Vue
...@@ -270,7 +270,7 @@ template: ` ...@@ -270,7 +270,7 @@ template: `
<input type="number" v-model:value="config.w" /> <input type="number" v-model:value="config.w" />
Rows: Rows:
<input type="number" v-model:value="config.w" /> <input type="number" v-model:value="config.h" />
</div> </div>
`, `,
``` ```
...@@ -301,7 +301,7 @@ Reload and check that logs to the browser console when you hit the button (and t ...@@ -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. 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 ```ts
import { GameConfig } from "./gameConfig" import { GameConfig } from "./gameConfig"
...@@ -388,3 +388,298 @@ Period: ...@@ -388,3 +388,298 @@ Period:
``` ```
Notice this button is bound so it's disabled until we've created the game. It's onClick is bound to the `toggleTimer` method, and the text changes from Start to Stop depending on whether the timer is already started. Notice this button is bound so it's disabled until we've created the game. It's onClick is bound to the `toggleTimer` method, and the text changes from Start to Stop depending on whether the timer is already started.
## Solving this tutorial with React
There's a worked solution to this tutorial using React.js on the `react-solution` branch. But let's talk you through it.
### Some config changes
First, there's some config changes we'll make to make things easier. By default, React won't give you nice error messages -- it left them out of the library to save space. So make the HTML load the development scripts from our node_modules directory:
```html
<script src="./node_modules/react/umd/react.development.js"></script>
<script src="./node_modules/react-dom/umd/react-dom.development.js"></script>
```
And tell webpack not to put React itself into bundle.js (because we're loading its development scripts separately). In webpack.config.js, inside `module.exports`:
```js
externals: {
"react": "React",
"react-dom": "ReactDOM"
}
```
We're also going to want to use TSX, so let's update compilerOptions in `tsconfig.json`:
```js
"jsx": "react"
```
And let's also rename `index.ts` to `index.tsx`, and then update `webpack.congfig.js` to tell it about the new entry point:
```js
entry: './src/index.tsx',
```
### Getting React on-screen
Let's get React rendering something, even if just Hello World.
In index.html, create a div that Vue can render to. Let's put this above the svg for the game:
```html
<div id="app"></div>
```
And inside index.tsx, let's import React and ReactDOM, and something to that div
```ts
import * as React from "react";
import * as ReactDOM from "react-dom";
```
and now for our first bit of TSX:
```ts
ReactDOM.render(
<div>Hello React</div>,
document.getElementById("app")
);
```
Set webpack watching the files:
```sh
npm run build -- --watch
```
Reload index.html in the browser, and check we get "Hello Vue" appearing. If so, good -- React is working and we can progress.
### Creating a ConfigView component
Let's create a file called `gameConfig.tsx'.
First, let's import react:
```tsx
import * as React from "react"
import * as ReactDOM from "react-dom"
```
Then let's define two types -- one for the config's definable properties, and another that has some additional state:
```tsx
export interface ConfigProps {
w:number
h:number
}
```
```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<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 ConfigProps interface -- so, it has properties for `w` and`h`.
And we've said its state is defined by the `ConfigState` interface.
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.
Let's go back to `index.tsx` and use our component.
First, we need to import `ConfigView`:
```tsx
import { ConfigView } from "./gameConfig"
```
and then let's alter our React call to use it:
```tsx
ReactDOM.render(
<ConfigView w={40} h={20}></ConfigView>,
document.querySelector("#app")
);
```
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
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment