├── Controller
└── Index
│ └── Index.php
├── README.md
├── composer.json
├── etc
├── frontend
│ └── routes.xml
└── module.xml
├── registration.php
└── view
└── frontend
├── layout
└── vinaikopp_breakout_index_index.xml
├── requirejs-config.js
├── templates
└── breakout.phtml
└── web
├── css
└── breakout.css
├── js
├── dom-node-binding.js
├── game.js
├── game
│ ├── ball.js
│ ├── brick.js
│ ├── frame.js
│ ├── game-component.js
│ ├── paddle.js
│ ├── score-board.js
│ ├── state
│ │ ├── ball-state.js
│ │ ├── brick-state-factory.js
│ │ ├── frame-state.js
│ │ ├── game-state.js
│ │ ├── paddle-state.js
│ │ ├── score-board-state.js
│ │ └── wall-state.js
│ └── wall.js
└── writable-computed.js
└── template
├── game-component.html
└── score-board.html
/Controller/Index/Index.php:
--------------------------------------------------------------------------------
1 | pageFactory = $pageFactory;
21 | }
22 |
23 | public function execute()
24 | {
25 | return $this->pageFactory->create();
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # UiComponents Breakout
2 |
3 | This is a small example module showing how to manage slightly more complex interdependent shared view state with Magento UiComponents.
4 |
5 | It uses a simple version of the 80s arcade game Breakout as a scenario.
6 |
7 | 
8 |
9 | I think UiComponents where not designed to be used in this manner, but it works as a demo for external state management.
10 |
11 | This module was created written as an example for my presentation at MageTitansIT 2018 in Milano.
12 |
13 | (c) Vinai Kopp
14 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vinaikopp/module-breakout",
3 | "description": "n.a.",
4 | "require": {
5 | "php": "^7.0.0"
6 | },
7 | "type": "magento2-module",
8 | "license": [
9 | "BSD-3-Clause"
10 | ],
11 | "autoload": {
12 | "files": [
13 | "registration.php"
14 | ],
15 | "psr-4": {
16 | "VinaiKopp\\Breakout\\": ""
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/etc/frontend/routes.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/etc/module.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/registration.php:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 | Breakout
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/view/frontend/requirejs-config.js:
--------------------------------------------------------------------------------
1 | var config = {
2 | map: {
3 | '*': {
4 | gameComponent: 'VinaiKopp_Breakout/js/game/game-component',
5 | writableComputed: 'VinaiKopp_Breakout/js/writable-computed',
6 | gameState: 'VinaiKopp_Breakout/js/game/state/game-state',
7 | frameState: 'VinaiKopp_Breakout/js/game/state/frame-state',
8 | scoreBoardState: 'VinaiKopp_Breakout/js/game/state/score-board-state',
9 | ballState: 'VinaiKopp_Breakout/js/game/state/ball-state',
10 | paddleState: 'VinaiKopp_Breakout/js/game/state/paddle-state',
11 | wallState: 'VinaiKopp_Breakout/js/game/state/wall-state',
12 | brickComponent: 'VinaiKopp_Breakout/js/game/brick',
13 | brickFactory: 'VinaiKopp_Breakout/js/game/state/brick-state-factory'
14 | }
15 | }
16 | };
17 |
--------------------------------------------------------------------------------
/view/frontend/templates/breakout.phtml:
--------------------------------------------------------------------------------
1 |
6 |
38 |
39 |
44 |
--------------------------------------------------------------------------------
/view/frontend/web/css/breakout.css:
--------------------------------------------------------------------------------
1 |
2 | @import url('https://fonts.googleapis.com/css?family=Press+Start+2P');
3 |
4 | #breakout-wrapper {
5 | height: 600px;
6 | font-family: 'Press Start 2P', cursive;
7 | }
8 |
9 | .breakout {
10 | position: relative;
11 | }
12 |
13 | .breakout div {
14 | position: absolute;
15 | }
16 |
17 | .score-board {
18 | padding-top: .4em;
19 | padding-left: .5em;
20 | }
21 |
22 | .score-board h1 {
23 | margin-bottom: 0.2em;
24 | }
25 |
--------------------------------------------------------------------------------
/view/frontend/web/js/dom-node-binding.js:
--------------------------------------------------------------------------------
1 | define(['ko'], function(ko) {
2 | 'use strict';
3 |
4 | ko.bindingHandlers.bindDomNode = {
5 | init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
6 | if (valueAccessor()) {
7 | bindingContext.$data.domNode = element;
8 | }
9 | }
10 | };
11 | });
12 |
--------------------------------------------------------------------------------
/view/frontend/web/js/game.js:
--------------------------------------------------------------------------------
1 | define(['gameComponent', 'ko', 'gameState'], function (GameComponent, ko, game) {
2 | 'use strict';
3 |
4 | return GameComponent.extend({
5 | initialize: function () {
6 | this._super();
7 | this.initDimensions(game);
8 | ko.defineProperty(this, 'running', function () {
9 | return game.running;
10 | });
11 | this.setStartState();
12 | },
13 | setStartState: function () {
14 | game.width = 600;
15 | game.height = 500;
16 | game.running = false;
17 | game.status = 'pending';
18 | this._super();
19 | },
20 | run: function () {
21 | this._super();
22 | if (game.status !== 'started' && game.running) {
23 | game.running = false;
24 | }
25 | if (game.running) {
26 | window.requestAnimationFrame(this.run.bind(this));
27 | }
28 | },
29 | start: function () {
30 | if (game.status === 'win' || game.status === 'fail') {
31 | this.reset();
32 | } else {
33 | if (game.status === 'pending') {
34 | game.status = 'started';
35 | }
36 | if (!game.running) {
37 | game.running = true;
38 | this.run();
39 | }
40 | }
41 | },
42 | stop: function () {
43 | game.running = false;
44 | }
45 | });
46 | });
47 |
--------------------------------------------------------------------------------
/view/frontend/web/js/game/ball.js:
--------------------------------------------------------------------------------
1 | define(['gameComponent', 'ballState', 'frameState'], function (GameComponent, ball, frame) {
2 | 'use strict';
3 |
4 | return GameComponent.extend({
5 | defaults: {
6 | backgroundColor: 'red',
7 | initialVelocity: 5
8 | },
9 | initialize: function () {
10 | this._super();
11 | this.initDimensions(ball);
12 | this.setStartState();
13 | },
14 | setStartState: function () {
15 | ball.width = 20;
16 | ball.height = 20;
17 | ball.left = (frame.width / 2) - (ball.width / 2);
18 | ball.top = (frame.height * .60) - (ball.height / 2);
19 | ball.xV = ball.yV = this.initialVelocity;
20 | },
21 | run: function () {
22 | this._super();
23 | ball.left += ball.xV;
24 | ball.top += ball.yV;
25 | }
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/view/frontend/web/js/game/brick.js:
--------------------------------------------------------------------------------
1 | define(
2 | ['gameComponent', 'wallState', 'ballState', 'scoreBoardState'],
3 | function (GameComponent, wall, ball, scoreBoard) {
4 | 'use strict';
5 |
6 | function transposeBallFromFrameToWallCoordinates(ballInFrameCoords, wall) {
7 | const ballInWallCoords = {};
8 |
9 | ['left', 'top'].forEach(function (p) {
10 | Object.defineProperty(ballInWallCoords, p, {
11 | get: function () {
12 | return ballInFrameCoords[p] - wall[p];
13 | },
14 | set: function (v) {
15 | ballInFrameCoords[p] = v + wall[p];
16 | }
17 | });
18 | });
19 | ['width', 'height'].forEach(function (p) {
20 | Object.defineProperty(ballInWallCoords, p, {
21 | get: function () {
22 | return ballInFrameCoords[p];
23 | }
24 | });
25 | });
26 | ['xV', 'yV'].forEach(function (p) {
27 | Object.defineProperty(ballInWallCoords, p, {
28 | get: function () {
29 | return ballInFrameCoords[p];
30 | },
31 | set: function (v) {
32 | ballInFrameCoords[p] = v;
33 | }
34 | });
35 | });
36 | return ballInWallCoords;
37 | }
38 |
39 | function bottomSide(thing) {
40 | return thing.top + thing.height;
41 | }
42 |
43 | function rightSide(thing) {
44 | return thing.left + thing.width;
45 | }
46 |
47 | function horizontallyIntersects(ball, brick) {
48 | return rightSide(ball) > brick.left && ball.left < rightSide(brick);
49 | }
50 |
51 | function verticallyIntersects(ball, brick) {
52 | return bottomSide(ball) > brick.top && ball.top < bottomSide(brick);
53 | }
54 |
55 | function goingUp(ball) {
56 | return ball.yV < 0;
57 | }
58 |
59 | function goingDown(ball) {
60 | return ball.yV > 0;
61 | }
62 |
63 | function goingRight(ball) {
64 | return ball.xV > 0;
65 | }
66 |
67 | function goingLeft(ball) {
68 | return ball.xV < 0;
69 | }
70 |
71 | function ballBelow(ball, brick) {
72 | return ball.top > bottomSide(brick);
73 | }
74 |
75 | function ballAbove(ball, brick) {
76 | return bottomSide(ball) < brick.top;
77 | }
78 |
79 | function ballRightOf(ball, brick) {
80 | return ball.left > brick.left + brick.width;
81 | }
82 |
83 | function ballLeftOf(ball, brick) {
84 | return rightSide(ball) < brick.left;
85 | }
86 |
87 | function hitFromBelow(ball, brick) {
88 | const touch = ball.top < bottomSide(brick);
89 | return goingUp(ball) && horizontallyIntersects(ball, brick) && !ballAbove(ball, brick) && touch;
90 | }
91 |
92 | function hitFromAbove(ball, brick) {
93 | const touch = bottomSide(ball) > brick.top;
94 | return goingDown(ball) && horizontallyIntersects(ball, brick) && !ballBelow(ball, brick) && touch;
95 | }
96 |
97 | function hitFromLeft(ball, brick) {
98 | const touch = rightSide(ball) > brick.left;
99 | return goingRight(ball) && verticallyIntersects(ball, brick) && touch && !ballRightOf(ball, brick);
100 | }
101 |
102 | function hitFromRight(ball, brick) {
103 | const touch = ball.left < rightSide(brick);
104 | return goingLeft(ball) && verticallyIntersects(ball, brick) && touch && !ballLeftOf(ball, brick);
105 | }
106 |
107 | function speedUp(ball) {
108 | ball.yV += goingDown(ball) ? 0.3 : -0.3;
109 | ball.xV += goingRight(ball) ? 0.3 : -0.3;
110 | }
111 |
112 |
113 | ball = transposeBallFromFrameToWallCoordinates(ball, wall);
114 |
115 | return GameComponent.extend({
116 | initialize: function (options) {
117 | const brick = options.brick;
118 | this.index = 'brick-' + brick.row + '-' + brick.col;
119 | this.initDimensions(brick);
120 | brick.width = function () {
121 | return wall.width / wall.cols;
122 | };
123 | brick.height = function () {
124 | return wall.height / wall.rows;
125 | };
126 | brick.left = function () {
127 | return brick.col * brick.width;
128 | };
129 | brick.top = function () {
130 | return brick.row * brick.height;
131 | };
132 | this._super();
133 | },
134 | run: function () {
135 | const brick = this.brick;
136 |
137 | if (hitFromBelow(ball, brick) || hitFromAbove(ball, brick)) {
138 | this.verticalHit(brick);
139 | }
140 | else if (hitFromLeft(ball, brick) || hitFromRight(ball, brick)) {
141 | this.horizontalHit(brick);
142 | }
143 | this._super();
144 | },
145 | verticalHit: function (brick) {
146 | ball.top = goingDown(ball) ? brick.top - ball.height - 1 : bottomSide(brick) + 1;
147 | ball.yV *= -1;
148 | this.hit();
149 | },
150 | horizontalHit: function (brick) {
151 | ball.left = goingRight(ball) ? brick.left - ball.width - 1 : rightSide(brick) + 1;
152 | ball.xV *= -1;
153 | this.hit();
154 | },
155 | hit: function () {
156 | scoreBoard.score++;
157 | wall.brickCount--;
158 | speedUp(ball);
159 | this.destroy();
160 | }
161 | });
162 | });
163 |
--------------------------------------------------------------------------------
/view/frontend/web/js/game/frame.js:
--------------------------------------------------------------------------------
1 | define(['gameComponent', 'ballState', 'frameState', 'gameState'], function (GameComponent, ball, frame, game) {
2 | 'use strict';
3 |
4 | return GameComponent.extend({
5 | initialize: function () {
6 | this._super();
7 | this.initDimensions(frame);
8 | frame.width = function () {
9 | return game.width * .98;
10 | };
11 | frame.height = function () {
12 | return game.height * .80;
13 | };
14 | frame.left = function () {
15 | return game.width * .01;
16 | };
17 | frame.top = function () {
18 | return game.height * .19;
19 | };
20 | },
21 | run: function () {
22 | this._super();
23 | this.checkBallCollision();
24 | },
25 | checkBallCollision: function () {
26 | if (ball.left < 0) {
27 | ball.xV *= -1;
28 | ball.left = 0;
29 | } else if (ball.left + ball.width > frame.width) {
30 | ball.xV *= -1;
31 | ball.left = frame.width - ball.width;
32 | }
33 | if (ball.top < 0) {
34 | ball.yV *= -1;
35 | ball.top = 0;
36 | } else if (ball.top + ball.height > frame.height) {
37 | game.status = 'fail';
38 | ball.yV *= -1;
39 | ball.top = frame.height - ball.height;
40 | }
41 | }
42 | });
43 | });
44 |
--------------------------------------------------------------------------------
/view/frontend/web/js/game/game-component.js:
--------------------------------------------------------------------------------
1 | define(['uiComponent', 'ko', 'VinaiKopp_Breakout/js/dom-node-binding', 'es6-collections'], function (Component, ko) {
2 | 'use strict';
3 |
4 | return Component.extend({
5 | defaults: {
6 | template: 'VinaiKopp_Breakout/game-component',
7 | border: '1px solid black',
8 | backgroundColor: 'white',
9 | bindDomNode: false
10 | },
11 | initDimensions: function (state) {
12 | ['width', 'height', 'top', 'left'].forEach(function (property) {
13 | ko.defineProperty(this, property, function () {
14 | return state[property] && state[property] + 'px';
15 | });
16 | }.bind(this));
17 | },
18 | run: function () {
19 | this.elems.each(function (elem) {
20 | elem.run();
21 | });
22 | },
23 | setStartState: function () {
24 |
25 | },
26 | reset: function () {
27 | this.setStartState();
28 | this.elems.each(function (elem) {
29 | elem.reset();
30 | });
31 | }
32 | });
33 | });
34 |
--------------------------------------------------------------------------------
/view/frontend/web/js/game/paddle.js:
--------------------------------------------------------------------------------
1 | define(
2 | ['gameComponent', 'ko', 'ballState', 'frameState', 'paddleState'],
3 | function (GameComponent, ko, ball, frame, paddle) {
4 | 'use strict';
5 |
6 | var targetPageX = ko.observable();
7 |
8 | var inFrameXPos;
9 |
10 | const trackMouse = function (e) {
11 | targetPageX(e.pageX);
12 | };
13 |
14 | function calculatePaddlePosition (paddleDomNode) {
15 | const elementPageX = paddleDomNode ? paddleDomNode.getBoundingClientRect().left : 0;
16 | const delta = targetPageX() - elementPageX;
17 | const targetFrameX = inFrameXPos + delta - Math.floor(paddle.width / 2);
18 | return inFrameXPos = Math.min(Math.max(0, targetFrameX), frame.width - paddle.width);
19 | }
20 |
21 | return GameComponent.extend({
22 | defaults: {
23 | bindDomNode: true,
24 | pauseWithGame: true
25 | },
26 | initialize: function () {
27 | this._super();
28 | this.initDimensions(paddle);
29 | paddle.width = function () {
30 | return frame.width * .21;
31 | };
32 | paddle.height = function () {
33 | return frame.height * .05;
34 | };
35 | inFrameXPos = (frame.width / 2) - (paddle.width / 2);
36 | this.setStartState();
37 | if (! this.pauseWithGame) {
38 | paddle.left = function () {
39 | return calculatePaddlePosition(this.domNode);
40 | }.bind(this);
41 | }
42 | document.addEventListener('mousemove', trackMouse);
43 | },
44 | destroy: function () {
45 | document.removeEventListener('mousemove', trackMouse);
46 | this._super();
47 | },
48 | setStartState: function () {
49 | paddle.top = frame.height - paddle.height - 2;
50 | const middlePosition = (frame.width / 2) - (paddle.width / 2);
51 | targetPageX(middlePosition);
52 | if (this.pauseWithGame) {
53 | paddle.left = middlePosition;
54 | }
55 | },
56 | run: function () {
57 | if (this.pauseWithGame) {
58 | paddle.left = calculatePaddlePosition(this.domNode);
59 | }
60 | this.checkBallCollision();
61 | this._super();
62 | },
63 | checkBallCollision: function () {
64 | const ballRight = ball.left + ball.width;
65 | const ballBottom = ball.top + ball.height;
66 | const paddleRight = paddle.left + paddle.width;
67 |
68 | if (ball.left < paddleRight &&
69 | ballRight > paddle.left &&
70 | ballBottom > paddle.top &&
71 | ball.top < paddle.top
72 | ) {
73 | ball.yV *= -1;
74 | }
75 | }
76 | });
77 | });
78 |
--------------------------------------------------------------------------------
/view/frontend/web/js/game/score-board.js:
--------------------------------------------------------------------------------
1 | define(['gameComponent', 'ko', 'scoreBoardState', 'gameState'], function (GameComponent, ko, scoreBoard, game) {
2 | 'use strict';
3 |
4 | const statusLabels = {
5 | pending: 'Click to start Game',
6 | started: 'Running',
7 | paused: 'Paused',
8 | win: 'Congrats, you win! Click to reset.',
9 | fail: 'Game Over! Click to reset.'
10 | };
11 |
12 | return GameComponent.extend({
13 | defaults: {
14 | template: "VinaiKopp_Breakout/score-board"
15 | },
16 | initialize: function () {
17 | this._super();
18 | this.initDimensions(scoreBoard);
19 | scoreBoard.width = function () {
20 | return game.width * .97;
21 | };
22 | scoreBoard.height = function () {
23 | return game.height * .15;
24 | };
25 | scoreBoard.left = function () {
26 | return game.width * .01;
27 | };
28 | scoreBoard.top = function () {
29 | return game.height * .01;
30 | };
31 | ko.defineProperty(this, 'score', function () {
32 | return scoreBoard.score;
33 | });
34 | this.setStartState();
35 | },
36 | statusLabel: ko.pureComputed(function () {
37 | if (game.status === 'started') {
38 | return game.running ? statusLabels['started'] : statusLabels['paused'];
39 | }
40 | return statusLabels[game.status] || 'Error: Unknown Status "' + game.status + '"';
41 | }),
42 | setStartState: function () {
43 | scoreBoard.score = 0;
44 | this._super();
45 | }
46 | });
47 | });
48 |
--------------------------------------------------------------------------------
/view/frontend/web/js/game/state/ball-state.js:
--------------------------------------------------------------------------------
1 | define(['ko', 'writableComputed'], function (ko, writableComputed) {
2 | 'use strict';
3 |
4 | return ko.track({
5 | width: writableComputed(),
6 | height: writableComputed(),
7 | left: writableComputed(),
8 | top: writableComputed(),
9 | xV: 0,
10 | yV: 0
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/view/frontend/web/js/game/state/brick-state-factory.js:
--------------------------------------------------------------------------------
1 | define(['ko', 'writableComputed'], function (ko, writableComputed) {
2 | 'use strict';
3 |
4 | return function (row, col) {
5 | return ko.track({
6 | width: writableComputed(),
7 | height: writableComputed(),
8 | top: writableComputed(),
9 | left: writableComputed(),
10 | row: row,
11 | col: col
12 | });
13 | }
14 | });
15 |
--------------------------------------------------------------------------------
/view/frontend/web/js/game/state/frame-state.js:
--------------------------------------------------------------------------------
1 | define(['ko', 'writableComputed'], function (ko, writableComputed) {
2 | 'use strict';
3 |
4 | return ko.track({
5 | width: writableComputed(),
6 | height: writableComputed(),
7 | top: writableComputed(),
8 | left: writableComputed()
9 | });
10 | });
11 |
--------------------------------------------------------------------------------
/view/frontend/web/js/game/state/game-state.js:
--------------------------------------------------------------------------------
1 | define(['ko', 'writableComputed'], function (ko, writableComputed) {
2 | 'use strict';
3 |
4 | return ko.track({
5 | width: writableComputed(),
6 | height: writableComputed(),
7 | left: writableComputed(),
8 | top: writableComputed(),
9 | running: false,
10 | status: null
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/view/frontend/web/js/game/state/paddle-state.js:
--------------------------------------------------------------------------------
1 | define(['ko', 'writableComputed'], function (ko, writableComputed) {
2 | 'use strict';
3 |
4 | return ko.track({
5 | width: writableComputed(),
6 | height: writableComputed(),
7 | left: writableComputed(),
8 | top: writableComputed()
9 | });
10 | });
11 |
--------------------------------------------------------------------------------
/view/frontend/web/js/game/state/score-board-state.js:
--------------------------------------------------------------------------------
1 | define(['ko', 'writableComputed'], function (ko, writableComputed) {
2 | 'use strict';
3 |
4 | return ko.track({
5 | width: writableComputed(),
6 | height: writableComputed(),
7 | top: writableComputed(),
8 | left: writableComputed(),
9 | score: 0
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/view/frontend/web/js/game/state/wall-state.js:
--------------------------------------------------------------------------------
1 | define(['ko', 'writableComputed'], function (ko, writableComputed) {
2 | 'use strict';
3 |
4 | return ko.track({
5 | width: writableComputed(),
6 | height: writableComputed(),
7 | top: writableComputed(),
8 | left: writableComputed(),
9 | rows: 0,
10 | cols: 0,
11 | brickCount: 0
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/view/frontend/web/js/game/wall.js:
--------------------------------------------------------------------------------
1 | define(
2 | ['gameComponent', 'ko', 'frameState', 'wallState', 'brickComponent', 'brickFactory', 'gameState'],
3 | function (GameComponent, ko, frame, wall, BrickComponent, createBrick, game) {
4 | 'use strict';
5 |
6 | return GameComponent.extend({
7 | defaults: {
8 | border: 'none',
9 | backgroundColor: 'transparent'
10 | },
11 | initialize: function () {
12 | this._super();
13 | this.initDimensions(wall);
14 | wall.cols = 5;
15 | wall.rows = 4;
16 | wall.left = 0;
17 | wall.top = function () {
18 | return frame.height * .1;
19 | };
20 | wall.height = function () {
21 | return frame.height * .4;
22 | };
23 | wall.width = function () {
24 | return frame.width;
25 | };
26 | this.setStartState();
27 | },
28 | setStartState: function () {
29 | this.destroyChildren();
30 | for (var row = 0; row < wall.rows; row++) {
31 | for (var col = 0; col < wall.cols; col++) {
32 | this.insertChild(new BrickComponent({brick: createBrick(row, col)}));
33 | }
34 | }
35 | wall.brickCount = wall.rows * wall.cols;
36 | },
37 | run: function () {
38 | this._super();
39 | if (wall.brickCount === 0) {
40 | game.status = 'win';
41 | }
42 | }
43 | });
44 | });
45 |
--------------------------------------------------------------------------------
/view/frontend/web/js/writable-computed.js:
--------------------------------------------------------------------------------
1 | define(['ko'], function (ko) {
2 | 'use strict';
3 |
4 | return function writableObservable(initialValue) {
5 |
6 | const observable = ko.observable(initialValue);
7 |
8 | return ko.pureComputed({
9 | read: function () {
10 | const v = observable();
11 | return v instanceof Function ? v() : v;
12 | },
13 | write: function (newValue) {
14 | observable(newValue);
15 | }
16 | })
17 | }
18 | });
19 |
--------------------------------------------------------------------------------
/view/frontend/web/template/game-component.html:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/view/frontend/web/template/score-board.html:
--------------------------------------------------------------------------------
1 |
9 |
--------------------------------------------------------------------------------