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>



😏

Salesforce Report, KPI Dashboard and Chart.JS




In previous post I showed how we can use Plotly in Salesofrce Lightning component.
Another powerful library for graphs is Chart.Js.

I created recently package that allow to setup KPI and view them in custom dashboard using either Plotly or Chart.JS.
The latest question I was facing is how I can include Salesforce standard reports in the KPI dashboard. 
We do know that Salesforce reports have some limitations, but still they are commonly and if there are logic that can be achieve with the standard tool we should use it.

The good thing is that Salesforce have almost

As a demo, I implemented the same example from the
The Lightning component updated to use the chart.js static resource



1
2
3
4
5
6
<aura:component implements="flexipage:availableForRecordHome,force:hasRecordId" access="global" controller="PlotlyDemoController">
    <ltng:require scripts="/resource/chartJS/Chart.bundle.js" afterScriptsLoaded="{!c.scriptsLoaded}"/>
    <div style="width:600px;height:400px;">
     <canvas id="myDiv" ></canvas>
    </div>
</aura:component>

The Lightning JS controller was updated to match the chart js input



 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
({
    scriptsLoaded : function(component, event, helper) {
        var getAccountDataAction = component.get("c.getAccountData");
        
        getAccountDataAction.setParams({
            'accountId' : component.get("v.recordId")
        });
        
        getAccountDataAction.setCallback(this, function(response){
            
            var resData = JSON.parse(response.getReturnValue());
            
            
            var ctx = document.getElementById('myDiv').getContext('2d');
            var myChart = new Chart(ctx, {
                type: 'bar',
                data: {
                    labels: resData.labels,
                    datasets: [{
                        data: resData.values,
                        backgroundColor: '#1f77b4'
                    }]
                },
                options: {
                    responsive:true,
                    maintainAspectRatio: false
                }
            });
        });
        
        $A.enqueueAction(getAccountDataAction);
    }
})

And last the the apex controller was changed to read its input from standard report (+building Salesforce standard report that show the opportunity grouped by  type).



 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
public with sharing class PlotlyDemoController{  
    
    @AuraEnabled
    public static String getAccountData(String accountId){
    
        map<String, object> m_retData = new map<String, object>();
        
        map<String, decimal> m_oppType_Amt = new map<String, decimal>();
        
        Report rep = [select Id from Report where DeveloperName = 'Account_Opp_Report1'];
        
        //Run the report
        Reports.ReportResults repResult = Reports.ReportManager.runReport(rep.Id, false);
        
        //Read result and grouping property
        list<Reports.GroupingValue> l_groupingValue = repResult.getGroupingsDown().getGroupings();
        map<String,Reports.ReportFact> m_reportFact = repResult.getFactMap();

        map<String, object> m_cell_sumValue = new map<String, object>();
        
        //Store each per each uniuque cell its summary value
        for(String keyFact : m_reportFact.keyset()){
            list<Reports.SummaryValue> l_summryValues = m_reportFact.get(keyFact).getAggregates();
            String keyToUse = m_reportFact.get(keyFact).getKey().replace('!T', '');
    
            m_cell_sumValue.put(keyToUse, l_summryValues.get(0).getValue());    //*Might have several summaries. In this assume the first summary if the sum amount that we need
        }
        
        //Per each grouping label store it in the final result
        for(Reports.GroupingValue grupingItem : l_groupingValue){
            m_oppType_Amt.put(grupingItem.getLabel(), decimal.valueOf(String.valueOf(m_cell_sumValue.get(grupingItem.getKey()))));
        }        
        
        m_retData.put('labels', m_oppType_Amt.keySet());
        m_retData.put('values', m_oppType_Amt.values());
        
        return JSON.serialize(m_retData);
    }

}


Finally, this is a quick demo how I used all those items together in the KPI dashboard app.







Using Plotly in Lightning Component

Plotly has Javascript open source library that can be used to easily create powerful graphs.
But can we use it in Salesforce Lightning Component? We can. 
Under some consideration.

First, in Lightning we cannot reference external javascript files, therefore the plotly library must be downloaded and saved as static resource. 
Second, Lightning component with API higher than 39 uses locker service which make the component more secure but also more isolate, therefore you will need to use component with API version 39 (or less).

With those settings it is possible and quite easy to use Plotly.
Here is very simple example of Lightning component embedded in account Lightning page that display the total opportunities amount grouped by opportunities type.



1.Apex controller - use to collect opportunities data


 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
public with sharing class PlotlyDemoController{


    @AuraEnabled
    public static String getAccountData(String accountId){
    
        map<String, object> m_retData = new map<String, object>();
        
        
        map<String, decimal> m_oppType_Amt = new map<String, decimal>();
        
        for(Opportunity opp : [select Id,Type,Amount from Opportunity where AccountId = :accountId]){
            if(! m_oppType_Amt.containsKey(opp.Type)){
                m_oppType_Amt.put(opp.Type, 0);
            }
            
            m_oppType_Amt.put(opp.Type, opp.Amount + m_oppType_Amt.get(opp.Type));
        }
        
        m_retData.put('labels', m_oppType_Amt.keySet());
        m_retData.put('values', m_oppType_Amt.values());
        
        return JSON.serialize(m_retData);
    }

}


2.Lightning component


1
2
3
4
<aura:component implements="flexipage:availableForRecordHome,force:hasRecordId" access="global" controller="PlotlyDemoController">
    <ltng:require scripts="{!$Resource.plotlyCode}" afterScriptsLoaded="{!c.scriptsLoaded}"/>
    <div id="myDiv" style="width:600px;height:400px;"></div>
</aura:component>



3.Lighting Controller


 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
({
    scriptsLoaded : function(component, event, helper) {
        var getAccountDataAction = component.get("c.getAccountData");
        
        getAccountDataAction.setParams({
            'accountId' : component.get("v.recordId")
        });
        
        getAccountDataAction.setCallback(this, function(response){
            
            var resData = JSON.parse(response.getReturnValue());
            
            var data = [
              {
                x: resData.labels,
                y: resData.values,
                type: 'bar'
              }
            ];
            
            Plotly.newPlot('myDiv', data);
        });
        
        $A.enqueueAction(getAccountDataAction);
    }
})



😏

KPI Dashboard with Dynamic Parameters

Recently I introduce app for manage business activities in Salesforce. 
One of the module in the application is KPI Dashboards. 
It provide the ability to set up KPIs for any data type in the organization, build dashboards based on those KPIs, schedule it and distribute the results to users. 
Some questions that I was facing is if/how it is possible to run dashboard with dynamic parameters. For example - build dashboard with account parameter and run it every time for different account. 
Indeed such option was missing, so I added it.

For this example I'm using 4 KPIs related to opportunity and opportunity product, each result is split to quarters.

  1. Counting closed opportunities
  2. Sum the opportunities amount
  3. Calculate average days it took to close
  4. Counting the products solds

In the app I created those 4 KPI Setup.
Notice that each have filter with Account Id equal <empty value>, as I want to send it as parameter later.







Next, I created dashboard with those 4 KPIs, added audience and set my user as recipient.




Finally, clicked on button Schedule to run the calculations and here I added the new checkbox - Use Dynamic Parameters. Checking it will show list of available filters according to the KPIs Setup filters.


I can set different values for each KPI, or use the same value for all (that are using the same field filter). This is my case - I want to filter all the KPIs according to the same account.






After few minutes the process completed and I got email with link to view the dashboard result.









Test Class Builder App

I created simple and free app that can be useful for Salesforce development. It mainly used to simplify the process of writing test classes. 
It does not generate test classes automatically as I saw several other apps attend to do, but instead the app provide simple UI for generate test data and the test processes and finally generate 2 output files:
1.Static resource file that contains the test meta data.
2.Apex test class that run the test.

Using the app require permission: Customize Application.
In addition you should have at least Salesforce Developer basic knowledge.


Following are the main steps for creating new apex test class with the app:

1.Go to tab Test Class Builder
2.Either provide name for new test or select test file that was created before.



3.In the Test Data section you can create data that will be created before the test (before Test.StartTest()). Can either create new data using the  icon or import data from the current environment using the  icon.
  • Can add connections between the records by clicking the  icon next to the record and set the relevant lookup field.
  • Known limitation: rich text fields being displayed as regular text area field.

4.In the Test Parameters section you can define parameters that will be used during the test. 
For example, if you added few accounts in the test data and you need to test function that receive list of accounts you can create list parameter with those accounts. 

If you need to use specific field from the test record as parameter you can also create parameter for that purpose



5.Create Test Process by clicking the icon at top. 

In each test process you can have 3 steps:
5.1 Data manipulation. Add by the icon.
Usually used to test triggers functionality. For example if you have logic that invoked from trigger when opportunity is closed, you should update the opportunity to close.
  • Click the icon to set fields to update.


5.2 Methods invocation. Add by theicon.
Used to call directly to apex function inside classes.
First type the class you want to test and select it, the page will show all the functions in this class. Select the relevant functions and click button Add Selected.

  • For this demo I created simple apex class with 2 functions: first receive list of account records and print their names, second receive account Id parameter and close its opportunities
  • If the function receive parameters you can set them by selecting parameters that were defined under Test Parameters.


  • Use the  icon to reorder the function order. The order in the page is the order that the functions will be executed.

5.3 Assert. Add by the icon.
Can be used to verify your test complete as expected.
Select the Assert Type (for now only 1 option exists Field Value) and click the icon to choose the record, field, expected value and error message to show if the assert failed.


6.Click the icon to generate your test files.
You can receive email with the 2 output files, or if it is sandbox you can directly deploy the files to this org.



If you used the email, process the instruction in the email - 1 file should be added as static resource, second as new apex class.
If you deployed the files, you should find your apex test class under the name {input name at start}Test, in our demo it will be demo01Test.

  • If using in sandbox and you want to use the deploy option you should first add remote site setting with your current org. If you not sure what is the URL, just try the deployment and you will receive appropriate message indicating which URL should be added.
  • You can modify the apex classes that was generated by the app. For example if you have complex logic that is hard to implement with the app, you can use the app only to create the test data and then write the test logic by yourself in the output file.


Finally, run the test class and after verifying the coverage deploy to production both the static resource and the apex text class.

Installation link

Sample video:

😏


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...