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;')}"> ♕ </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;')}"> ♕ </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;')}"> ♛ </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;')}"> ♛ </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;')}"> ♛ </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;')}"> ♕ </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;')}"> ♕ </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;')}"> ♛ </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'> ♕ </div> </template> </div> </template> <template if:false={isTurnUser1}> <div class="player1Tool"> <template if:true={cellItem.isKing}> <div class="kingStyle"> ♕ </div> </template> </div> </template> </template> <template if:true={cellItem.isPlayer2Status}> <div class="player2Tool"> <template if:true={cellItem.isKing}> <div class="kingStyle"> ♛ </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'> ♛ </div> </template> </div> </template> <template if:true={isTurnUser1}> <div class="player2Tool"> <template if:true={cellItem.isKing}> <div class="kingStyle"> ♛ </div> </template> </div> </template> </template> <template if:true={cellItem.isPlayer1Status}> <div class="player1Tool"> <template if:true={cellItem.isKing}> <div class="kingStyle"> ♕ </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"> ♕ </div> </template> </div> </template> <template if:true={cellItem.isPlayer2Status}> <div class="player2Tool"> <template if:true={cellItem.isKing}> <div class="kingStyle"> ♛ </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.
No comments:
Post a Comment