Implement Checkers in Salesforce with Visualforce and LWC


I wanted to explore a little bit the LWC with uncommon process for learning and also to know how complicate it will be to transform visualforce page with javascript/jqeury into Lightning.

So I decided to implement Checkers game. Was pretty confident about implementing with visualforce but had some doubts with LWC.


I used single custom object - Game that represent game between 2 users. Main flow is that first user create new game and wait for opponent to join the table, once the opponent join and both confirm the game starts. 

Obviously to show checkers board I needed to write custom code and might want to override the standard actions of the Game, so when user go to specific game record he will view the board (either as 1 of the players or as viewer).

At first I did it with visualforce and apex controller.

Main sections of the class is initialize the relevant structures - player 1/2 tools, board and its cells, movement log.

It have also 1 public function - moveTool that being called from the the page when player want to move tool. 

Rest of the functions are private to check valid movements, king options, etc... I didn't add here the full code as it too long. The full code can be viewed here https://github.com/liron50/SFCheckers.



 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
public with sharing class PlayGameController{

    public String gameRecordId;
    public Game__c gameRecord {get; set;}
    public list<RowBoard> l_rows {get; set;}
    
    public Boolean isCurrentUser1 {get; set;}
    public Boolean isCurrentUser2 {get; set;}
    
    public map<String, PlayerTool> p1Tools {get; set;}
    public map<String, PlayerTool> p2Tools {get; set;}
    
    public map<String, CellBoard> allCellsMap {get; set;}
    public String allCellsJson {get; set;}

    public list<MovementItem> l_movementItems {get; set;}

    public PlayGameController(ApexPages.StandardController sc){
        gameRecordId = sc.getRecord().Id;
        
        initGameData();
    }
    
    private void initGameData(){
    
		//Init all the class variables
		...
    }
    
	//call from the page when player wants to move tool
    public PageReference moveTool(){
        
		...
    }
    
    //move player tool 1 move
    private void movePlayerTool(
        Integer playerNum, 
        String fromCell, 
        String toCell, 
        Integer toRow, 
        Integer toCol){
		...
    }
    
    //Check if there were any tools that had option to eat but did not eat
    private set<String> checkForBurnTools(Integer playerNum){
		...
    }
    
    //Check if player have any valid moves , if not the game end in draw
    private boolean anyValidMovement(Integer playerNum){
        ...
    }
    
    //Special logic to check if king tool had option to move
    private Boolean isKingCanMove(
        Integer playerNum, Integer opponent, Integer row, Integer col, String dirVert, String dirHorz){
		...
    }
    
    //Special logic to check if king tool had option to eat in specific direction
    private Boolean isKingCanEat(
        Integer playerNum, Integer opponent, Integer row, Integer col, String dirVert, String dirHorz){
		...
    }
}



Next is the visualforce page (again omitted here the JS/CSS code to save space). The main section of the page is simply HTML table that display the board cells and each cell content depend on the current user (is it player1, player2 or viewer) and on the tool in the cell (can be white/black checker/king or empty).


  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
<apex:page standardController="Game__c" extensions="PlayGameController" sidebar="false" lightningStylesheets="true">

<apex:includeScript value="/soap/ajax/34.0/connection.js"/>
<apex:includeScript value="/soap/ajax/34.0/apex.js"/>
<apex:includeScript value="{!URLFOR($Resource.jquery_1_11_1_min)}" />

<apex:outputPanel id="globalVars">
    <script type="text/javascript">
        
        var allCellsMap = JSON.parse('{!allCellsJson}');
        
        var namespace = '';
        var turn = '{!gameRecord.Player_Turn__c}';
        var isUser1 = '{!isCurrentUser1}';
        var isUser2 = '{!isCurrentUser2}';
        var gameId = '{!gameRecord.Id}';
		
        if((turn === 'P2' && isUser1 === 'true')
            || (turn === 'P1' && isUser2 === 'true')
            || (isUser1 === 'false' && isUser2 === 'false')) {
            console.log('STARTING TIMER');
            //start timer
            var interval = setInterval(function(){
                sforce.connection.sessionId = '{!$Api.Session_ID}'; 
                
                var expectingNextTurn = turn == 'P2' ? 'P1' : 'P2';
                var result = sforce.connection.query('select ' + namespace + 'Player_Turn__c from ' + namespace + 'Game__c where Id=\'' + gameId +  '\' and ' + namespace + 'Player_Turn__c = \'' + expectingNextTurn + '\'');
                var records = result.getArray("records");
            
                if(records[0] != undefined){
                    clearInterval(interval);
                    refreshBoardJS();
                }
            }, 3000);
        }
    </script>
</apex:outputPanel>


<style>
...
</style>

<script>
...
</script>


<apex:form id="formId">
<table>
    <tr>
        <td style="width:75%">
            <div class="boardstyle">
                
                    
                <div class="boardTitle">
                    {!gameRecord.Player_1__r.Name} VS {!gameRecord.Player_2__r.Name}
                </div>
                
                <div class="playerImg">
                    <img class="{!IF(gameRecord.Player_Turn__c == 'P2', 'playerImgBorder', 'playerWaitImgBorder')}" src="{!IF(isCurrentUser2, gameRecord.Player_1__r.SmallPhotoUrl, gameRecord.Player_2__r.SmallPhotoUrl)}" title="{!IF(isCurrentUser2, gameRecord.Player_1__r.Name, gameRecord.Player_2__r.Name)}"/>
                </div>
                <table>
                    <apex:repeat value="{!l_rows}" var="row">
                        <tr>
                            <apex:repeat value="{!row.l_cells}" var="cell">
                                <td class="{!IF(cell.isEven, 'evenbg', 'oddbg')}">
                                    <apex:outputPanel rendered="{!isCurrentUser1}">
                                        <div onclick="cellClicked('1', {!cell.rowNumber}, {!cell.colNumber}); return false;" class="player1Tool cl_a1_{!cell.cellIndexId}" style="{!IF(cell.status == 'P1' && gameRecord.Player_Turn__c == 'P1', 'display:block;', 'display:none;')}">
                                            <div class="kingStyle" style="{!IF(cell.isKing, 'display;block;', 'display:none;')}">
                                                &#9813;
                                            </div>
                                        </div>
                                        <div class="player1Tool cl_d1_{!cell.cellIndexId}" style="{!IF(cell.status == 'P1' && gameRecord.Player_Turn__c == 'P2', 'display:block;', 'display:none;')}">
                                            <div class="kingStyle" style="{!IF(cell.isKing, 'display;block;', 'display:none;')}">
                                                &#9813;
                                            </div>
                                        <div>
                                        <div class="player2Tool cl_d2_{!cell.cellIndexId}" style="{!IF(cell.status == 'P2', 'display:block;', 'display:none;')}">
                                            <div class="kingStyle" style="{!IF(cell.isKing, 'display;block;', 'display:none;')}">
                                                &#9819;
                                            </div>
                                        </div>
                                    </apex:outputPanel>
                                    
                                    <apex:outputPanel rendered="{!isCurrentUser2}">
                                        <div onclick="cellClicked('2', {!cell.rowNumber}, {!cell.colNumber}); return false;" class="player2Tool cl_a2_{!cell.cellIndexId}" style="{!IF(cell.status == 'P2' && gameRecord.Player_Turn__c == 'P2', 'display:block;', 'display:none;')}">
                                            <div class="kingStyle" style="{!IF(cell.isKing, 'display;block;', 'display:none;')}">
                                                &#9819;
                                            </div>
                                        </div>
                                        <div class="player2Tool cl_d2_{!cell.cellIndexId}" style="{!IF(cell.status == 'P2' && gameRecord.Player_Turn__c == 'P1', 'display:block;', 'display:none;')}">
                                            <div class="kingStyle" style="{!IF(cell.isKing, 'display;block;', 'display:none;')}">
                                                &#9819;
                                            </div>
                                        </div>
                                        <div class="player1Tool cl_d1_{!cell.cellIndexId}" style="{!IF(cell.status == 'P1', 'display:block;', 'display:none;')}">
                                            <div class="kingStyle" style="{!IF(cell.isKing, 'display;block;', 'display:none;')}">
                                                &#9813;
                                            </div>
                                        </div>
                                    </apex:outputPanel>
                                    
                                    
                                    <apex:outputPanel rendered="{!NOT isCurrentUser1 && NOT isCurrentUser2}">
                                        <div class="player1Tool cl_d1_{!cell.cellIndexId}" style="{!IF(cell.status == 'P1', 'display:block;', 'display:none;')}">
                                            <div class="kingStyle" style="{!IF(cell.isKing, 'display;block;', 'display:none;')}">
                                                &#9813;
                                            </div>
                                        </div>
                                        <div class="player2Tool cl_d2_{!cell.cellIndexId}" style="{!IF(cell.status == 'P2', 'display:block;', 'display:none;')}">
                                            <div class="kingStyle" style="{!IF(cell.isKing, 'display;block;', 'display:none;')}">
                                                &#9819;
                                            </div>
                                        </div>
                                    </apex:outputPanel>
                                    
                                    <div onclick="optionalCellClicked('1', {!cell.rowNumber}, {!cell.colNumber}); return false;" class="optionalMoveP1 cl_p1_{!cell.cellIndexId}" style="display:none;" />
                                    <div onclick="optionalCellClicked('2', {!cell.rowNumber}, {!cell.colNumber}); return false;" class="optionalMoveP2 cl_p2_{!cell.cellIndexId}" style="display:none;" />
                                </td>
                            </apex:repeat>
                        </tr>
                    </apex:repeat>
                </table>
                
                <div class="playerImg">
                    <img class="{!IF(gameRecord.Player_Turn__c == 'P1', 'playerImgBorder', 'playerWaitImgBorder')}" src="{!IF(NOT isCurrentUser2, gameRecord.Player_1__r.SmallPhotoUrl, gameRecord.Player_2__r.SmallPhotoUrl)}" title="{!IF(NOT isCurrentUser2, gameRecord.Player_1__r.Name, gameRecord.Player_2__r.Name)}"/>
                </div>
            </div>
            
        </td>
        <td style="width:25%; vertical-align: top;">
            <div class="movementsPanel">
                <table cellspacing="0" cellpadding="0" style="min-width: 200px;">
                    <tr>
                        <th>No.</th>
                        <th>Player 1</th>
                        <th>Player 2</th>
                    </tr>
                    <apex:repeat value="{!l_movementItems}" var="moveItem">
                        <tr>
                            <td>{!moveItem.index}</td>
                            <td><apex:outputText value="{!moveItem.player1Move}" escape="false"/></td>
                            <td><apex:outputText value="{!moveItem.player2Move}" escape="false"/></td>
                        </tr>
                    </apex:repeat>
                </table>
            </div>
        </td>
    </tr>
</table>

<apex:actionFunction name="moveToolJS" action="{!moveTool}" reRender="formId,globalVars">
    <apex:param name="pnum" value=""/>
    <apex:param name="fcell" value=""/>
    <apex:param name="tcell" value=""/>
    <apex:param name="iseatmove" value=""/>
</apex:actionFunction>

<apex:actionFunction name="refreshBoardJS" action="{!refreshBoard}" reRender="formId,globalVars"/>

</apex:form>

</apex:page>


This is how the page looks while playing. 

Each player, on its turn can click his tools, clicking on tool will highlight the tools options, then clicking on highlight option will invoke the moveTool server method that will update the board, the game and switch the turn.



In the LWC version, most of the apex server logic remain similar, but I moved most of the init logic and the class variable to the JS controller. When the component is loading it invoke server method to get the Game data and then create the relevant structures.


  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
import { LightningElement, api, track } from 'lwc';
import { subscribe, unsubscribe, onError, setDebugFlag, isEmpEnabled} from 'lightning/empApi';

import currentUserId from '@salesforce/user/Id';
import getGameData from '@salesforce/apex/PlayGameControllerLWC.getGameData';
import moveTool from '@salesforce/apex/PlayGameControllerLWC.moveTool';

export default class PlayCheckerGame extends LightningElement {

    @api recordId;
    @track gameRecord;
    @track isTurnUser1;
    @track isCurrentUserIsP1;
    @track isCurrentUserIsP2;
    @track isCurrentUserViewer;
    @track boardRows = [];
    @track allCellsMap;
    @track p1Tools;
    @track p2Tools;
    @track movementsLogs = [];

    @track gameTitle;

    @track mainUserCss;
    @track mainUserPhotoURL;
    @track mainUserName;

    @track opponentUserCss;
    @track opponentUserPhotoURL;
    @track opponentUserName;

    @track clickedToolRow;
    @track clickedToolCol;
    @track clickedOptions;

    connectedCallback(){
        getGameData({gameId: this.recordId}).then(
            result =>{
                this.gameRecord = result;

                this.loadGameData();
            }
        ).catch(error => {
                console.log('error' + error);
        });
    }

    loadGameData(){
        console.log('this.gameRecord :' + JSON.stringify(this.gameRecord));
        this.gameTitle = (this.gameRecord.Player_1__c == undefined ? '' : this.gameRecord.Player_1__r.Name) + ' VS ' + (this.gameRecord.Player_2__c == undefined ? '' : this.gameRecord.Player_2__r.Name);

        this.isTurnUser1 = this.gameRecord.Player_Turn__c == 'P1';
        this.isCurrentUserIsP1 = this.gameRecord.Player_1__c == currentUserId;
        this.isCurrentUserIsP2 = this.gameRecord.Player_2__c == currentUserId;

        this.isCurrentUserViewer = this.isCurrentUserIsP1 == false && this.isCurrentUserIsP2 == false;

        this.p1Tools = this.gameRecord.Player_1_Tools__c == undefined ? [] : JSON.parse(this.gameRecord.Player_1_Tools__c);
        this.p2Tools = this.gameRecord.Player_2_Tools__c == undefined ? [] : JSON.parse(this.gameRecord.Player_2_Tools__c);

        this.allCellsMap = {};
        this.boardRows = [];
        this.movementsLogs = [];

        if(this.gameRecord.Movement_Log__c != undefined){
            var allMovements = JSON.parse(this.gameRecord.Movement_Log__c);

            for(var moveIndex in allMovements){
                this.movementsLogs.push({
                    'index' : allMovements[moveIndex].index,
                    'player1Move' : allMovements[moveIndex].player1Move,
                    'player2Move': allMovements[moveIndex].player2Move
                });
            }
        }

        if(this.isCurrentUserIsP2 == false){
            
            this.mainUserName = this.gameRecord.Player_1__c == undefined ? '' : this.gameRecord.Player_1__r.Name;
            this.mainUserPhotoURL = this.gameRecord.Player_1__c == undefined ? '' : this.gameRecord.Player_1__r.SmallPhotoUrl;
            this.mainUserCss = (this.gameRecord.Player_Turn__c == 'P1') ? 'playerImgBorder' : '';
            this.opponentUserName = this.gameRecord.Player_2__c == undefined ? '' : this.gameRecord.Player_2__r.Name;
            this.opponentUserPhotoURL = this.gameRecord.Player_2__c == undefined ? '' : this.gameRecord.Player_2__r.SmallPhotoUrl;
            this.opponentUserCss = (this.gameRecord.Player_Turn__c == 'P2') ? 'playerImgBorder' : '';
        
            for(var rowIndex = 1; rowIndex <= 8; rowIndex++){
                var boardRow = {
                    'rowNumber' : rowIndex
                };
                var boardRowCells = [];

                for(var colIndex = 1; colIndex <=8; colIndex++){
                    var keyCell = rowIndex + '_' + colIndex;

                    var newCell = {
                        'keyCell' : keyCell,
                        'rowNumber' : rowIndex,
                        'colNumber' : colIndex,
                        'status' : (this.p1Tools[keyCell] != undefined ? 'P1' : (this.p2Tools[keyCell] != undefined ? 'P2' : 'E')),
                        'isPlayer1Status' : this.p1Tools[keyCell] != undefined,
                        'isPlayer2Status' : this.p2Tools[keyCell] != undefined,
                        'showPlayer1Option' : false,
                        'showPlayer2Option' : false,
                        'isEatOptionCell' : false,
                        'isEatPrev' : false,
                        'ignoreDirection' : '',
                        'classStlye' : ((colIndex + rowIndex) % 2 != 0 ? 'evenbg' : 'oddbg')
                    };
                    
                    newCell.isKing = (newCell.status == 'P1' && this.p1Tools[keyCell].toolType === 'K')
                        || (newCell.status == 'P2' && this.p2Tools[keyCell].toolType === 'K');

                    boardRowCells.push(newCell);
                    this.allCellsMap[keyCell] = newCell;
                }

                boardRow['rowCells'] = boardRowCells;
                this.boardRows.push(boardRow);
            }
        }
        else{ //user 2 
            this.mainUserName = this.gameRecord.Player_2__c == undefined ? '' : this.gameRecord.Player_2__r.Name;
            this.mainUserPhotoURL = this.gameRecord.Player_2__c == undefined ? '' : this.gameRecord.Player_2__r.SmallPhotoUrl;
            this.mainUserCss = (this.gameRecord.Player_Turn__c == 'P2') ? 'playerImgBorder' : '';
            this.opponentUserName = this.gameRecord.Player_1__c == undefined ? '' : this.gameRecord.Player_1__r.Name;
            this.opponentUserPhotoURL = this.gameRecord.Player_1__c == undefined ? '' : this.gameRecord.Player_1__r.SmallPhotoUrl;
            this.opponentUserCss = (this.gameRecord.Player_Turn__c == 'P1') ? 'playerImgBorder' : '';


            for(var rowIndex = 8; rowIndex >= 1; rowIndex--){
                var boardRow = {
                    'rowIndex' : rowIndex
                };
                var boardRowCells = [];

                for(var colIndex = 8; colIndex >=1; colIndex--){
                    var keyCell = rowIndex + '_' + colIndex;

                    var newCell = {
                        'keyCell' : keyCell,
                        'rowNumber' : rowIndex,
                        'colNumber' : colIndex,
                        'status' : (this.p1Tools[keyCell] != undefined ? 'P1' : (this.p2Tools[keyCell] != undefined ? 'P2' : 'E')),
                        'isPlayer1Status' : this.p1Tools[keyCell] != undefined,
                        'isPlayer2Status' : this.p2Tools[keyCell] != undefined,
                        'showPlayer1Option' : false,
                        'showPlayer2Option' : false,
                        'isEatOptionCell' : false,
                        'isEatPrev' : false,
                        'ignoreDirection' : '',
                        'classStlye' : ((colIndex + rowIndex) % 2 != 0 ? 'evenbg' : 'oddbg')
                    };
                    
                    newCell.isKing = (newCell.status == 'P1' && this.p1Tools[keyCell].toolType === 'K')
                        || (newCell.status == 'P2' && this.p2Tools[keyCell].toolType === 'K');

                    boardRowCells.push(newCell);
                    this.allCellsMap[keyCell] = newCell;
                }

                boardRow['rowCells'] = boardRowCells;
                this.boardRows.push(boardRow);
            }
        }

        
        //If it is not the player turn, listen to event
        if((this.isTurnUser1 === false && this.isCurrentUserIsP1 === true)
            || (this.isTurnUser1 === true && this.isCurrentUserIsP2 === true)
            || (this.isCurrentUserViewer)) {

            
			//Subscribe to event
			const gameEventHandler = (response) => {
                if(response.data.payload.GameId__c == this.recordId){
                    getGameData({gameId: this.recordId}).then(
                        result =>{
                            this.gameRecord = result;
                            this.loadGameData();
                        }
                    ).catch(error => {
                            console.log('error' + error);
                    });
                }
            }

            subscribe('/event/Game_Played__e', -1, gameEventHandler).then(response => {
                console.log('Successfully subscribed to : ', JSON.stringify(response.channel));
            });
            
                /*
            console.log('STARTING TIMER');
            //start timer
            var interval = setInterval(function(){
                
                var expectingNextTurn = this.isTurnUser1 ? 'P2' : 'P1';
                
                getGameData({gameId: this.recordId}).then(
                    result =>{
                        console.log('result TIMER: ' + result);

                        var updatedGame = result;

                        console.log('updatedGame.Player_Turn__c TIMER: ' + updatedGame.Player_Turn__c);

                        if(updatedGame.Player_Turn__c === expectingNextTurn) {
                            clearInterval(interval);

                            this.gameRecord = result;
                            this.loadGameData();
                        }
                    }
                ).catch(error => {
                        console.log('error' + error);
                });

            }.bind(this), 3000);*/
        }

    }
	
	cellClicked(event){    
        ...
    }

    optionalCellClicked(event){
		...
    }


    canEatMore(player, row, col){
        ...
    }
    
    canEatCell(player, otherPlayer, checkRow, checkCol, cellBeyondRow, cellBeyondCol){
        ...
    }

    isKingCanEat(playerNum, opponent, row, col, dirVert, dirHorz){
        ...
    }
	
    findKingOptions(player, row, col){
        ...
    }
}


In addition as can be seen in the code snippet, I commented the setInterval call and instead use platform event- - when user making a move it publish event and the component subscribe to this event. The event mechanism seems better and was quite easy. Technically could use it also in the visulaforce solution. After setup the event and create it with simple process builder, all is left to do is to subscribe from the component:

subscribe('/event/Game_Played__e', -1, gameEventHandler)


Next is the html code which in concept very similar to the visualforce page only with different syntax/elements.


  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
<template>
    <template if:true={gameRecord}></template>
        <table style="width: auto;">
            <tr>
                <td style="width:75%">
                    <div class="boardstyle">
                        <div class="boardTitle">
                            {gameTitle}
                        </div>
                        
                        <div class="playerImg">
                            <img class={opponentUserCss} src={opponentUserPhotoURL} title={opponentUserName}/>    
                        </div>
                        <table>
                            <template for:each={boardRows} for:item='rowItem'>
                                <tr key={rowItem.rowNumber}>
                                    <template for:each={rowItem.rowCells} for:item='cellItem'>
                                        <td class={cellItem.classStlye} key={cellItem.keyCell}>
                                            <template if:true={isCurrentUserIsP1}>
                                                <template if:true={cellItem.isPlayer1Status}>
                                                    <template if:true={isTurnUser1}>
                                                        <div onclick={cellClicked} data-row={cellItem.rowNumber} data-col={cellItem.colNumber} data-player='1' class="player1Tool" style="cursor: pointer;">
                                                            <template if:true={cellItem.isKing}>
                                                                <div class="kingStyle" onclick={cellClicked} data-row={cellItem.rowNumber} data-col={cellItem.colNumber} data-player='1'>
                                                                    &#9813;
                                                                </div>
                                                            </template>
                                                        </div>
                                                    </template>

                                                    <template if:false={isTurnUser1}>
                                                        <div class="player1Tool">
                                                            <template if:true={cellItem.isKing}>
                                                                <div class="kingStyle">
                                                                    &#9813;
                                                                </div>
                                                            </template>
                                                        </div>
                                                    </template>
                                                </template>

                                                <template if:true={cellItem.isPlayer2Status}>
                                                    <div class="player2Tool">
                                                        <template if:true={cellItem.isKing}>
                                                            <div class="kingStyle">
                                                                &#9819;
                                                            </div>
                                                        </template>
                                                    </div>
                                                </template>
                                            </template>
                                            
                                            <template if:true={isCurrentUserIsP2}>

                                                <template if:true={cellItem.isPlayer2Status}>
                                                    <template if:false={isTurnUser1}>
                                                        <div onclick={cellClicked} data-row={cellItem.rowNumber} data-col={cellItem.colNumber} data-player='2' class="player2Tool" style="cursor: pointer;">
                                                            <template if:true={cellItem.isKing}>
                                                                <div class="kingStyle" onclick={cellClicked} data-row={cellItem.rowNumber} data-col={cellItem.colNumber} data-player='2'>
                                                                    &#9819;
                                                                </div>
                                                            </template>
                                                        </div>
                                                    </template>

                                                    <template if:true={isTurnUser1}>
                                                        <div class="player2Tool">
                                                            <template if:true={cellItem.isKing}>
                                                                <div class="kingStyle">
                                                                    &#9819;
                                                                </div>
                                                            </template>
                                                        </div>
                                                    </template>
                                                </template>

                                                <template if:true={cellItem.isPlayer1Status}>
                                                    <div class="player1Tool">
                                                        <template if:true={cellItem.isKing}>
                                                            <div class="kingStyle">
                                                                &#9813;
                                                            </div>
                                                        </template>
                                                    </div>
                                                </template>
                                            </template>
                                            
                                            <template if:true={isCurrentUserViewer}>
                                                <template if:true={cellItem.isPlayer1Status}>
                                                    <div class="player1Tool">
                                                        <template if:true={cellItem.isKing}>
                                                            <div class="kingStyle">
                                                                &#9813;
                                                            </div>
                                                        </template>
                                                    </div>
                                                </template>

                                                <template if:true={cellItem.isPlayer2Status}>
                                                    <div class="player2Tool">
                                                        <template if:true={cellItem.isKing}>
                                                            <div class="kingStyle">
                                                                &#9819;
                                                            </div>
                                                        </template>
                                                    </div>
                                                </template>
                                            </template>

                                            <template: if:true={cellItem.showPlayer1Option}>
                                                <div onclick={optionalCellClicked} data-row={cellItem.rowNumber} data-col={cellItem.colNumber} data-player='1' class='optionalMoveP1' style="cursor: pointer;"></div>
                                            </template:>
                                            <template: if:true={cellItem.showPlayer2Option}>
                                                <div onclick={optionalCellClicked} data-row={cellItem.rowNumber} data-col={cellItem.colNumber} data-player='2' class='optionalMoveP2' style="cursor: pointer;"></div>
                                            </template:>
                                       </td>
                                    </template>
                                </tr>
                            </template>
                        </table>
                        
                        <div class="playerImg">
                            <img class={mainUserCss} src={mainUserPhotoURL} title={mainUserName}/>    
                        </div>
                    </div>
                </td>
                
                <td style="width:25%; vertical-align: top;">
                    <div class="movementsPanel">
                        <table cellspacing="0" cellpadding="0" style="min-width: 200px;">
                            <tr>
                                <th>No.</th>
                                <th>Player 1</th>
                                <th>Player 2</th>
                            </tr>
                            <template for:each={movementsLogs} for:item='moveItem'>
                                <tr key={moveItem.index}>
                                    <td>{moveItem.index}</td>
                                    <td style="min-width: 100px;"><lightning-formatted-rich-text value={moveItem.player1Move}></lightning-formatted-rich-text></td>
                                    <td style="min-width: 100px;"><lightning-formatted-rich-text value={moveItem.player2Move}></lightning-formatted-rich-text></td>
                                </tr>
                            </template>
                        </table>
                    </div>
                </td>
            </tr>
        </table>
    </template>
</template>


From technical perspective I can highlight few remarks about the differences/complexity related to LWC vs visualforce.

1.Developing with the LWC structures lead you to divide the code correctly. Sometimes it seems annoying at first, but after further thinking and when viewing the final code you can see the benefits. For example, in LWC you cannot use operators in to render elements, only if:true/if:false this force you to write the condition logic inside the controller, where it should be.

In visualforce this is not being enforced. I can write complex conditions + CSS + JS in the page.

2.The development for Visualforce is still easier, as it have more suitable tools. In some cases we can also develop without any additional tools expect the browser. For LWC we must use external tool and push the changes any time we want to test our changes.

3.Lightning Components still have (and probably will have) caching issues. In some cases need to clear caching to get the process using with the latest changes. 





Salesforce Rollup Summary Limitations and Alternatives

 Rollup Summary field is a great tool in Salesforce, but as other things it have limitation.
The total rollup fields per objects is currently 25 (can be increased by support to 40 in some cases) and it can be used only in master-detail relationship, which might make it part of the design while deciding the object model.
There are some great apps that extends the standard capabilities and allow to overcome the limitations. 2 of the most commons are DLRS (Declarative Lookup Rollup Summary) and Rollup Helper. 


Recently I came across similar case while developing app. In my case I provided tool for building custom process for update/create records, and while setting the record values user can use formula with functions. I wanted to provide complex function - rollupCalculation - where user will input the relevant parameters and the tool will do the rest.

It appear that such calculation is not really complicated as it seems at first. You might need to invest some time in the user setup- can either ask from users to input directly field/object API names or you can build special component that show the labels. 

But eventually for the calculation you basically only need few inputs:

1.Child object to aggregate.

2.Lookup field from the child to the parent.

3.Aggregation function. Can be either Count/Sum/AVG/MAX/MIN

4.Field for summary, if the function is not Count.

5.Optional: where criteria for filtering the child records.

6.Parent object/field to store the result.


Your logic for calculation might look something like this:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public static void calcualteRollup(
        String childObjectName,
        String aggregateFunction, 
        String aggregateField,
        String parentFieldLookup,
        String additionalCriteria,
        String parentFieldForResult,
        list<sObject> parentRecordList){
    
        //Set function + agg field for query
        String funcWithField = aggregateFunction + '(' +
                                (aggregateFunction == 'Count' ? 'Id' : aggregateField) + ')';
        
        //get parent Ids
        String inValues = '';
                        
        for(sObject rec : parentRecordList){
            inValues += '\'' + rec.Id + '\',';
        }
                
        
        String groupSoql = 'select ' + parentFieldLookup + ',' + funcWithField + ' from ' + childObjectName 
                                            + ' where ' + parentFieldLookup + ' IN (' + inValues.removeEnd(',') + ')' 
                                            + (String.isBlank(additionalCriteria) ? '' : ' and (' + additionalCriteria +')')
                                            + ' group by ' + parentFieldLookup;
    
    
        
        System.debug('groupSoql: ' + groupSoql);
        
        //Store the records input in map to easy the access
        map<Id, sObject> recordsMap = new map<Id, sObject>();
        
        for(sObject rec : parentRecordList){
            recordsMap.put(rec.Id, rec);
        }
        
        for(AggregateResult ag :database.query(groupSoql)){
            recordsMap.get(String.valueOf(ag.get(parentFieldLookup))).put(parentFieldForResult, ag.get('expr0'));
        }
        
        
        update parentRecordList;
    }


Some issues require more attention and special treatment, like records without any aggregation result (in some cases you will want to set the parent field as zero and maybe in other set it to null), or support large volume of records (both the records to be updated and the child records to count which might require batch process), but as you can see from above code the general logic is not that complicated. Sometimes when you need only 1-2 custom rollups you can do it by yourself 

When/if it become too complicate you can always use 1 of the external apps shown below or try the following tool which have much other capabilities expect the rollups.







How to Use Custom Permission in Lightning Web Component (Summer 20 Feature)

It seems obvious needed for many developers but only recently Salesforce added the ability to access custom permissions in Lightning Web Component.

Previously we needed to use some workaround like formula fields or server functions. 

With this new feature in summer 20, the workarounds are no longer required.


Simply create the custom permission (if you don't have it yet) and assign it to the relevant profiles.


In your LWC JS code import the custom permission from '@salesforce/customPermission'


1
2
3
4
5
6
7
8
import { LightningElement } from 'lwc';
import viewLWCPermission from '@salesforce/customPermission/View_LWC_Content';
 
export default class LWCCustomPermission extends LightningElement {
    get isUserCanAccessLWC() {
        return viewLWCPermission;
    }
}


Finally in the html code use the permission


1
2
3
4
5
<template>
    <template if:true={isUserCanAccessLWC}>
        Great! You can access this content
    </template>
</template>



😏

Retire of Permission on Profiles

If you are working as a Salesforce admin/developer you've probably heard somewhere that Salesforce is planning to make a significant cha...