NiceScript is free JavaScript library distributed under MIT license. It is designed to simplify development of web applications by offering declarative approach to data flow and concise JS syntax to create HTML.

In this tutorial we will use NiceScript to write Tic-Tac-Toe game. The game is similar to one in React tutorial so you can compare approaches. Article assumes you have basic knowledge of JavaScript and CSS. You can play with final result here.

Getting Started

Create HTML file and import nice.js from NiceScript npm module using unpkg cdn. This adds nice variable to global context.



<head>

<title>Tic Tac Toe</title>

<script src="

</head>

<body>

<script>

nice.H1('Tic Tac Toe').show();

</script>

</body>

</html> Tic Tac Toe https://unpkg.com/nicescript/nice.js</a> "> nice.H1('Tic Tac Toe').show();

NiceScript is a collection of types and functions. Type name always starts with capital letter and functions with lowercase letter. To create an instance you have to call type as function. There is no need to use new operator. NiceScript has embedded types for most HTML tags.

Method .show() of HTML element creates DOM node and attach it to document.body or provided parent element. On server side you can use string .html property.

Types are independent of nice variable so we can save ones we going to use as constants.

const { Div, H1, OL, LI, Box, Button } = nice;

Setting style and attributes

Object properties are functions. Calling one with an argument sets value and return object so you can chain calls. HTML objects has properties corresponding to all CSS properties and HTML attributes written in camelCase.

nice.Input().placeholder('search').marginLeft('1rem')

You can use format similar to node.js util.format() to set values.

nice.Div().margin('%drem %drem', 1, 2).html

// <div style="margin:1rem 2rem"></div>

Use .on function to set event handlers.

nice.Input().on('keyup', e => console.log(e.keyCode));

Adding children

There is three way you can add children to HTML elements.

// 1. call add method

Div().add(B('Nice').color('red')).add(Span('Script'));

// add accepts any number of arguments

Div().add(B('Nice').color('red'), Span('Script'));

// arrays are flattened

Div().add([B('Nice').color('red'), Span('Script')]); // 2. pass as arguments on creation

Div(B('Nice').color('red'), Span('Script'))

The third way is little more complicated but it is most convenient to create big chunks of code. HTML objects has methods with all HTML objects types names that create child, add it to object and return child. Use .up property to go back to parent object.

Div()

.B('Nice').color('red').up

.Span('Script').up

All above examples produce element with the following HTML:

<div><b style="color:red">Nice</b><span>Script</span></div>

Introducing Boxes

Boxes are observable components that keep state and track its changes. To put value in a Box call it with value as argument. To read current value call Box without arguments. Let’s create Box to keep current turn number.

const step = nice.Box(0);

// call step.listen(console.log) to track changes

You can also change the state of the box with type specific functions.

const step = nice.Box(0);

step.inc(); const a = nice.Box([]);

a.push(1);

Reactive boxes

You can make Box follow changes in other boxes and update its state according to provided function. Function called with states of used boxes as arguments. А returned value is set as new state.

const whoseTurn = n => n % 2 ? 'o' : 'x'; // verbose version

const current = Box.use(step).by(whoseTurn);

// short version

const current = Box.by(step, whoseTurn);

Now value of current will be ‘x’ on even steps and ‘o’ on odd steps.

Boxes and Tags together

Now create boxes for game state and it’s view.

const state = Box([]).fill(null, 0, 9); const createCellView = (v, i) => Div(v, ' ')

.textAlign('center').font('3.2rem Arial')

.width('4rem').height('4rem')

.border('1px #000 solid')

.margin('-1px');



Box.by(state, s => Div(s.map(createCellView))

.width('12rem')

.display('flex')

.flexWrap('wrap')

.margin('1rem 0')

).show();

Boxes have .show() method too. It create DOM node and update it when necessary.

Winning condition

Let’s create a box that follows game state and checks if game is over.

const lines = [

[0, 1, 2],

[3, 4, 5],

[6, 7, 8],

[0, 3, 6],

[1, 4, 7],

[2, 5, 8],

[0, 4, 8],

[2, 4, 6]

]; const gameOver = Box.by(state, s =>

lines.some(l => l.map(i => s[i]).reduce((a, b) => a === b && a)));

Now we can create component that shows game status.

Box.by(gameOver, current, (o, c) => o

? 'Winner: ' + whoseTurn(step() - 1)

: 'Next player: ' + c).show();

Action

Final piece so we can play.

const handleClick = i => {

state.set(i, current());

step.inc();

}; //add one line to createCellView function

.on('click', () => gameOver() || state()[i] || handleClick(i))

Note that we only update state and step boxes. Others follow changes.

History and immutability

Important note is that state of a box is immutable. When we call mutating method on box it doesn’t change state but replace it with a new one. We can use that to store game history.

const history = Box([state()]); const handleClick = i => {

state.set(i, current());

history().length > step + 1 && history.splice(step + 1);

history.push(state());

step.inc();

}; const historyButton = (s, i) =>

LI(Button('Go to move #' + i, () => { state(s); step(i); }));



Box.by(history, h => OL(h.map(historyButton))).show();

The line with splice call is most tricky one. It cuts the history if user went back and start to play from there.

Final Result

Couple things i like about final code:

It’s only about fifty lines of code in a single file.

It’s all valid JavaScript. We can use functions to structure our code. Also this drastically reduce demand for build tools or any other configuration efforts.

You can easily insert final solution in existing project as a component. Just wrap it into a function to avoid name conflicts.

There is no variables, no loops and very few conditional expressions. So there is a little chance of typical errors.

I encourage you to improve the game by implementing these features borrowed from React tutorial:

When someone wins, highlight the three squares that caused the win.

When no one wins, display a message about the result being a draw.

Post links to your solutions and feedback on NiceScript in the comments.