Building Complex Process with Flow

I remember 1 of my first tasks in Salesforce before decade - provide the user screen to set some input and then run some calculations based on the input. The Salesforce person who worked with our team reference me to the visual flow, noting that it was build exactly for such process. 

I read few basic guides and start implementing. 

Very soon, we found out that the flow cannot display input search lookup and the only alternative was to provide dropdown selection. But since in our case there were potentially hundreds of options it made the flow not suitable for the case. Ok, next time I thought.

But it seems that this was repeating itself - every time I was trying to implement task with flow, very soon I found limitation that prevented me from using it.


New Flow

This was true until Salesforce added some huge improvements to the flow (started somewhere 2 years ago). Irrelevant elements were removed from the toolbox, bulkifying all DML/SOQL elements, support also setup objects, complex logical operations and much much more. In addition, combining the flow with Process Builder increased its usage significantly. 

Today, it seems the flow can be used almost for any common task instead of apex code. Furthermore, for complex logic we can still use the flow partially and invoke from the flow apex code that we will handle the complex part.

It raise some questions regarding the Apex code usage in future. 

I could write list of things that still cannot be done with flow and maybe estimate which will be too complex to be supported by flow in future, but the truth is that building application without code is not new concept (not only in Salesforce, of course). There are lots of tools in Salesforce that were build for that purpose - page layout, custom object/field, email alert, workflow (RIP), validation rules, reports/dashboards, process builderHowever, it turns out that customization requests become more and more complex and in many cases those tools were not able to provide proper solution. The new flow catch up the race and can cover wider area of the requests.


Flow Sample: Merge Account with Flow

I decided to test the flow usability with sample that can point out few notes regarding its ability and flexibility.

Take for example the following request - user want to merge account records but needs option to include/exclude related lists. This cannot be done with the standard merge as the related data from the merged record always being added to the master record. 

We need customization. 

Can it be done with flow? (Of course it can. otherwise I wouldn't pick this sample... But when started I wasn't really sure about the complexity).

For simplicity assume the process start from the master account record page. 

In the flow will have the following steps:

    1.Get master account data.

    2.Screen for selecting second account.

    3.Get the selected second account data.

    4.Show data from both master and second accounts, options to set the output for each field and checkbox option 'Merge Related Lists'

    5.Update the master account with the selected values

    6.If the option 'Merge Related Lists' was selected reparent the related records.


Additional decisions for simplicity: 

1.I used only 1 checkbox which determine if all related lists should be merged or none. Could use several checkboxes for each entity (e.g 'Merge Contact', 'Merge Opportunities'....)

2.If user select to merge related lists I will reparent only contacts and opportunities records.

3.Merged only 3 fields: Account Rating, Type and Account Source. Could set all the other fields but for the demo it is enough.


This is how the flow will look like:






Lets focus at element 'Set Merge Output Fields'.

I added Choice option for each field that need to be merged and per each 2 choices variable: first option from the master account field and second from the second account field.



After setting the fields and clicking next the flow will assign the user selection to the master record, update it and reparent the related records only if the checkbox was checked.

Seems that's it. After activation, I can embed the flow in the record page (added as tab in this sample) and launch the flow.





Few notes worth mentioning when comparing the flow to the apex code:

  • The list of fields cannot be dynamic. In future if new fields will be added to the account, it will need to be added also in the flow.
  • The needs to set choice + choice options per each input might make this process exhausting if you have lots of fields on the account.  
  • Dynamic is missing in additional areas. Assume that we will want to allow merging more than 2 accounts in single run. We can add button 'Add Another Account' and store the accounts in collection, but seems hard to show per each field dynamic choice options that will be populated at run time. Optional we can allow up to X accounts in the merge and define in advanced choices for each.
  • If you are building application you can take the dynamic concept further. What if you want to use similar functionality for merging leads or contact or any other object? Will need to build flow with similar logic per each. This won't be very efficient.
  • Lack of styling options in this example was noticeable, It would be better to show the data in table where you can compare clearly compare the data from the merged accounts.


Some Conclusions

Some of the missing parts above might be supported in future in the flow. However, looking at those notes with the main discussion, we can conclude that the flow is very powerful and useful tool. It can cover lots of requests, but it reduces the flexibility. 

Usually it will be good enough when the development is for internal users. When it designed for end customers you might need this flexibility and advanced options. Same goes when building applications for distribution, you want to build flexible solution and impressive UI. With the estimation that requests will keep getting more complex apex will still be needed. Most likely the require skill level from developers will increased, since simple-medium tasks can be done without development.







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. 





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