Leap Tutorial
Leap is a Model Oriented Design (MOD) framework for JavaScript.
This introductory tutorial demonstrates using Leap to create a simple MOD single page game. It is strongly suggested that you first view the Hello World example for a quick, basic understanding of Leap & MOD.
In this tutorial, we will build a simple Tic-Tac-Toe game, adding features step-by-step to incrementally demonstrate using Leap & applying MOD to a project.
Model Oriented Design (MOD) Development
Model Oriented Design (MOD - pronounced mode) uses task-specific data models to store & manipulate information related to performing a task. Operators on the data (such as UI's or back-end services) manipulate the shared data models, thus maintaining a coordinated, single view of the state of the task. For example, if we have a UI to enter or edit a person's name, the model could contain a person object with a name property.
When UI components receive user input, the model is changed (not just the UI widget) thus reflecting the change throughout the entire application. This means that changes are automatically reflected on all application components related to the data, including both UI components and back-end services, such as REST calls.
Using models to hold task-related data has several compelling advantages:
- Data is no longer 'trapped' in UI components, such as input fields
- A single copy of data is shared throughout the entire application by all actors
- The DOM simply reflects the state of the world, no longer containing the data.
- Actions can be taken on data changes regardless of where in the app a data model has been changed
Leap MOD
Leap implements MOD using standard HTML & JavaScript, providing an automated model data-store. Using standard HTML & JavaScript means:
- There is no need to learn a custom framework
- You can integrate into existing code as much or as little as you'd like
- HTML & JavaScript developers can be immediately productive
- You are not locked in to a single, holistic framework
Getting Started with Leap
To get started, create a new game.html file. include the leap.min.js library in your HTML file & call leap.start() to start your app:
<!DOCTYPE html> <html> <head> <script src='http://leapjs.org/latest/leap.min.js'></script> <script> leap.start(); </script> </head> </html>
leap.start() runs only when the page is fully loaded. It accepts an optional function as a parameter which can be used to execute page start-up JavaScript code that is to be run only after the page is fully loaded.
Creating a Model
Creating a model in Leap is simple - just annotate any widget with a data model path. Leap does the rest, automatically creating the model and associating the widget with it. Additionally, Leap automatically watches the widget for changes and updates the model so you don't have to do any additional work to keep things in sync.
To build the tic-tac-toe board, create an array of input elements where each represents a box on the board. The state of the board will be kept in a two-dimensional array model called game:
<body> <input type='text' data-field='game[0][0]' /> <input type='text' data-field='game[0][1]' /> <input type='text' data-field='game[0][2]' /> <input type='text' data-field='game[1][0]' style='clear: both;' /> <input type='text' data-field='game[1][1]' /> <input type='text' data-field='game[1][2]' /> <input type='text' data-field='game[2][0]' style='clear: both;' /> <input type='text' data-field='game[2][1]' /> <input type='text' data-field='game[2][2]' /> </body>
The data-field attribute links the input elements to the game model's array elements. Therefore, user changes to the <input automatically causes the model to be changed.
The game model could be explicitly created at the start of the application by adding a call in the leap.start() function: leap.set('game',[["","",""],["","",""],["","",""]]);.
game.html
<!DOCTYPE html> <html> <head> <meta charset='ISO-8859-1'> <title>Leap Tutorial</title> <script src='http://leapjs.org/latest/leap.min.js'></script> <script> leap.start(); </script> <style> html, body { font-family: Arial, Helvetica, sans-serif; font-size: .8em; } input { float: left; width: 3rem; height: 3rem; border: 1px solid black; text-align: center; line-height: 3rem; font-size: 2rem; } </style> </head> <body> <input type='text' data-field='game[0][0]' /> <input type='text' data-field='game[0][1]' /> <input type='text' data-field='game[0][2]' /> <input type='text' data-field='game[1][0]' style='clear: both;' /> <input type='text' data-field='game[1][1]' /> <input type='text' data-field='game[1][2]' /> <input type='text' data-field='game[2][0]' style='clear: both;' /> <input type='text' data-field='game[2][1]' /> <input type='text' data-field='game[2][2]' /> </body> </html>
Open the game.html file in a browser. You will see the input field on the page. You can observe the linking of the model to the UI by typing an x in the upper left-hand input box, hitting return, then opening up the web browser's console & displaying the value of the game[0][0] model property by entering:
You will see the x you have typed in the input box.
You can see the entire game model in a similar way. For a complex model (such as an Object or Array), we can view the model directly, but it is clearer to wrap the data model path in JSON calls to make it more presentable:
0: Array(3) [ "x", "", "" ] 1: Array(3) [ "", "", "" ] 2: Array(3) [ "", "", "" ] <prototype>: Array []
Referencing Model Data
The data model can be referenced throughout an HTML file, in text or in element attributes, using a data model expression that encloses a data model path inside {{ }}. A data model expression is replaced by the current data model value and is kept in sync as the model changes.
The tic-tac-toe board's input elements' value attribute references the data model using the {{game[][]}} expressions. Whenever the model changes, the expression is automatically refreshed, displaying the latest model value.
Data model expressions can appear anywhere in the HTML. For example, a div can be added to display the current value of the data model. In fact, we can create a live view of the tic-tac-toe board by adding a parallel set of div elements:
<div style="clear: both;padding:20px;"> <div class="box">{{game[0][0]}}</div> <div class="box">{{game[0][1]}}</div> <div class="box">{{game[0][2]}}</div> <div class="box" style="clear: both;">{{game[1][0]}}</div> <div class="box">{{game[1][1]}}</div> <div class="box">{{game[1][2]}}</div> <div class="box" style="clear: both;">{{game[2][0]}}</div> <div class="box">{{game[2][1]}}</div> <div class="box">{{game[2][2]}}</div> </div>
Whenever the data model is changed, the div elements' text will be changed to reflect the new values.
game.html
<!DOCTYPE html> <html> <head> <meta charset='ISO-8859-1'> <title>Leap Tutorial</title> <script src='http://leapjs.org/latest/leap.min.js'></script> <script> leap.start(); </script> <style> html, body { font-family: Arial, Helvetica, sans-serif; font-size: .8em; } input, .box { float: left; width: 3rem; height: 3rem; border: 1px solid black; text-align: center; line-height: 3rem; font-size: 2rem; } </style> </head> <body> <input type='text' data-field='game[0][0]' /> <input type='text' data-field='game[0][1]' /> <input type='text' data-field='game[0][2]' /> <input type='text' data-field='game[1][0]' style='clear: both;' /> <input type='text' data-field='game[1][1]' /> <input type='text' data-field='game[1][2]' /> <input type='text' data-field='game[2][0]' style='clear: both;' /> <input type='text' data-field='game[2][1]' /> <input type='text' data-field='game[2][2]' /> <div style='clear: both; padding: 20px;'> <div class='box'>{{game[0][0]}}</div> <div class='box'>{{game[0][1]}}</div> <div class='box'>{{game[0][2]}}</div> <div class='box' style='clear: both;'>{{game[1][0]}}</div> <div class='box'>{{game[1][1]}}</div> <div class='box'>{{game[1][2]}}</div> <div class='box' style='clear: both;'>{{game[2][0]}}</div> <div class='box'>{{game[2][1]}}</div> <div class='box'>{{game[2][2]}}</div> </div> </body> </html>
Changing Model Data
Whenever the input values change, the related game model's properties change accordingly. Likewise, if you set the model's value in the web browser's console, the model will change along with all related references, such as the input and div elements:
By inspecting the lower right-hand input, you will see the element's value has changed to reflect the change in the data model (you can inspect the element by right clicking on the input and selecting Inspect from the pop-up menu).
Validation
As of now, a tic-tac-toe player can enter any value in the board's input elements. For the game, however, we'd like to restrict the values to a fixed set of values. To do so, we create a data validator.
In MOD, data validation is performed on the model not on the widgets. Validation at the model level ensures that ALL actors on the data are validated and ensured to be correct. Data model-level validation means that modifications from the UI as well as any back-end service responses will all be validated. As such, data validators are added to a data model directly.
To add the following validator we'll modify the leap.start() call to accept a function to be run when the page is loaded, then set the validator inside that function:
<script> leap.start(function(){ leap.addValidator('game',val=>(val && val.length == 1 && ( val == 'x' || val == 'o' ) ? "" : "only 'x' or 'o' are allowed")); }); <script>
Notice that we have added a validator to the entire game model. This way the validation is applied to all data model values within that model, meaning all of the tic-tac-toe board's boxes will be checked. Validators can be added at any point in a data model, including for each individual box on the tic-tac-toe board.
Errors produced by validators are held alongside the data in the model and are passed to any error handlers that are listening for errors in the model data.
game.html
<!DOCTYPE html> <html> <head> <meta charset='ISO-8859-1'> <title>Leap Tutorial</title> <script src='http://leapjs.org/latest/leap.min.js'></script> <script> leap.start(function(){ leap.addValidator( 'game', val=>(val && val.length == 1 && ( val == 'x' || val == 'o' ) ? '' : 'only 'x' or 'o' are allowed')); }); </script> <style> html, body { font-family: Arial, Helvetica, sans-serif; font-size: .8em; } input, .box { float: left; width: 3rem; height: 3rem; border: 1px solid black; text-align: center; line-height: 3rem; font-size: 2rem; } </style> </head> <body> <input type='text' data-field='game[0][0]' /> <input type='text' data-field='game[0][1]' /> <input type='text' data-field='game[0][2]' /> <input type='text' data-field='game[1][0]' style='clear: both;' /> <input type='text' data-field='game[1][1]' /> <input type='text' data-field='game[1][2]' /> <input type='text' data-field='game[2][0]' style='clear: both;' /> <input type='text' data-field='game[2][1]' /> <input type='text' data-field='game[2][2]' /> <div style='clear: both; padding: 20px;'> <div class='box'>{{game[0][0]}}</div> <div class='box'>{{game[0][1]}}</div> <div class='box'>{{game[0][2]}}</div> <div class='box' style='clear: both;'>{{game[1][0]}}</div> <div class='box'>{{game[1][1]}}</div> <div class='box'>{{game[1][2]}}</div> <div class='box' style='clear: both;'>{{game[2][0]}}</div> <div class='box'>{{game[2][1]}}</div> <div class='box'>{{game[2][2]}}</div> </div> </body> </html>
Error Handling
When a data model encounters errors, the errors are sent to error handlers that have been attached to the data model. For example, to catch errors in the game data model, add an error listener inside the leap.start() function:
<script>
leap.start(function(){
leap.addValidator( 'game', val=>(val && val.length == 1 && ( val == 'x' || val == 'o' ) ? '' : "only 'x' or 'o' are allowed"));
leap.addErrorListener( 'game', (model,errors)=>alert(errors.reduce((p,c)=>p+c+' ','')));
});
<script>
Like a validator, an error handler can be added to any node in the model. Here a listener has been added to the game node, meaning that it will listen for any errors that occur in the game node or any of its descendants. As such, errors in the game[0][0] property will cause the error handler to be run. Note that to process only errors for the game[0][0] property, the handler could instead be added directly to the game[0][0] data path.
game.html
<!DOCTYPE html> <html> <head> <meta charset='ISO-8859-1'> <title>Leap Tutorial</title> <script src='http://leapjs.org/latest/leap.min.js'></script> <script> leap.start(function(){ leap.addValidator( 'game', val=>(val && val.length == 1 && ( val == 'x' || val == 'o' ) ? '' : "only 'x' or 'o' are allowed")); leap.addErrorListener( 'game', (model,errors)=>alert(errors.reduce((p,c)=>p+c+' ',''))); }); </script> <style> html, body { font-family: Arial, Helvetica, sans-serif; font-size: .8em; } input, .box { float: left; width: 3rem; height: 3rem; border: 1px solid black; text-align: center; line-height: 3rem; font-size: 2rem; } </style> </head> <body> <input type='text' data-field='game[0][0]' /> <input type='text' data-field='game[0][1]' /> <input type='text' data-field='game[0][2]' /> <input type='text' data-field='game[1][0]' style='clear: both;' /> <input type='text' data-field='game[1][1]' /> <input type='text' data-field='game[1][2]' /> <input type='text' data-field='game[2][0]' style='clear: both;' /> <input type='text' data-field='game[2][1]' /> <input type='text' data-field='game[2][2]' /> <div style='clear: both; padding: 20px;'> <div class='box'>{{game[0][0]}}</div> <div class='box'>{{game[0][1]}}</div> <div class='box'>{{game[0][2]}}</div> <div class='box' style='clear: both;'>{{game[1][0]}}</div> <div class='box'>{{game[1][1]}}</div> <div class='box'>{{game[1][2]}}</div> <div class='box' style='clear: both;'>{{game[2][0]}}</div> <div class='box'>{{game[2][1]}}</div> <div class='box'>{{game[2][2]}}</div> </div> </body> </html>
Data Listeners
Data listeners, like error listeners, are added to a data model to capture changes in the model. For example, in the tic-tac-toe game, we'd like to check for a winner after each play is made. We can add standard JavaScript functions to the script section of the HTML file that check for a winner:
function getWinner() { let game = leap.get('game'); let winner; for( let i=0; i < 3; i++ ) { winner = ( game[i][0] == game[i][1] && game[i][1] == game[i][2] ) ? game[i][0] : ( ( game[0][i] == game[1][i] && game[1][i] == game[2][i] ) ? game[0][i] : null ); if( winner ) return winner; } return ( game[0][0] == game[1][1] && game[1][1] == game[2][2] ) ? game[0][0] : ( ( game[0][2] == game[1][1] && game[1][1] == game[2][0] ) ? game[0][2] : null ); } function checkForWinner(obj,prop,before,after,action) { let winner = getWinner(); if( winner ) { alert( winner + " WINS"); } }
Whenever getWinner is called, it get the current state of the game model by calling leap.get('game') which returns the model object containing the current values.
Finally to check for a winner, we add a data listener to have checkForWinner called whenever there is a change in the data model:
<script>
leap.start(function(){
leap.addValidator('game',val=>(val && val.length == 1 && ( val == 'x' || val == 'o' ) ? "" : "only 'x' or 'o' are allowed"));
leap.addErrorListener( 'game', (model,errors)=>alert(errors.reduce((p,c)=>p+c+" ","")));
leap.addDataListener( 'game', 'change', checkForWinner );
});
<script>
Several standard events are fired for data model changes, including read, add, change, remove, error, success & final (as well as related before & after events for add, change & remove events).
game.html
<!DOCTYPE html> <html> <head> <meta charset='ISO-8859-1'> <title>Leap Tutorial</title> <script src='http://leapjs.org/latest/leap.min.js'></script> <script> leap.start(function(){ leap.addValidator( 'game', val=>(val && val.length == 1 && ( val == 'x' || val == 'o' ) ? '' : "only 'x' or 'o' are allowed")); leap.addErrorListener( 'game', (model,errors)=>alert(errors.reduce((p,c)=>p+c+' ',''))); leap.addDataListener( 'game', 'change', checkForWinner ); }); function getWinner() { let game = leap.get('game'); let winner; for( let i=0; i < 3; i++ ) { winner = ( game[i][0] == game[i][1] && game[i][1] == game[i][2] ) ? game[i][0] : ( ( game[0][i] == game[1][i] && game[1][i] == game[2][i] ) ? game[0][i] : null ); if( winner ) return winner; } return ( game[0][0] == game[1][1] && game[1][1] == game[2][2] ) ? game[0][0] : ( ( game[0][2] == game[1][1] && game[1][1] == game[2][0] ) ? game[0][2] : null ); } function checkForWinner(obj,prop,before,after,action) { let winner = getWinner(); if( winner ) { alert( winner + ' WINS'); } } </script> <style> html, body { font-family: Arial, Helvetica, sans-serif; font-size: .8em; } input, .box { float: left; width: 3rem; height: 3rem; border: 1px solid black; text-align: center; line-height: 3rem; font-size: 2rem; } </style> </head> <body> <input type='text' data-field='game[0][0]' /> <input type='text' data-field='game[0][1]' /> <input type='text' data-field='game[0][2]' /> <input type='text' data-field='game[1][0]' style='clear: both;' /> <input type='text' data-field='game[1][1]' /> <input type='text' data-field='game[1][2]' /> <input type='text' data-field='game[2][0]' style='clear: both;' /> <input type='text' data-field='game[2][1]' /> <input type='text' data-field='game[2][2]' /> <div style='clear: both; padding: 20px;'> <div class='box'>{{game[0][0]}}</div> <div class='box'>{{game[0][1]}}</div> <div class='box'>{{game[0][2]}}</div> <div class='box' style='clear: both;'>{{game[1][0]}}</div> <div class='box'>{{game[1][1]}}</div> <div class='box'>{{game[1][2]}}</div> <div class='box' style='clear: both;'>{{game[2][0]}}</div> <div class='box'>{{game[2][1]}}</div> <div class='box'>{{game[2][2]}}</div> </div> </body> </html>
Data Model HTML Elements
As we've seen, in MOD, UI elements on the page are linked to values in a data model. The input elements for the tic-tac-toe board are each linked to a value in the game data model.
There are times when it is useful to identify all HTML elements that are associated with a data model node. To do so, call leap.getElements() passing in the path of the data model node for which you would like to find all elements.
Changing HTML Elements on Error
For example, we may want to provide better error feedback than simply popping up an alert box. We may want to shade all the data model related HTML elements red to indicate that there is an error. If we replace our start() functions error handler to run a function named errorFunction
<script>
leap.start( ()=>{
leap.addValidator( 'game', val=>(val && ( val == 'x' || val == 'o' ) ? "" : "only 'x' or 'o' are allowed"));
leap.addDataListener( 'game', 'change', checkForWinner );
leap.addErrorListener( 'game', errorFunction );
});
</script>
The function can identify all HTML elements linked to the data model value that has the error, then color their backgrounds red:
function errorFunction(path,errors,obj,prop,before,after,action) { let elements = leap.getElements( path ); if( elements !== undefined ) { elements.forEach( e => { e.style.backgroundColor='red'; }); } }
The call to leap.getElements(path) returns all HTML elements that are associated with the path that received the error (which is passed into the error function). Then for each element, the background color is set to red.
game.html
<!DOCTYPE html> <html> <head> <meta charset='ISO-8859-1'> <title>Leap Tutorial</title> <script src='http://leapjs.org/latest/leap.min.js'></script> <script> leap.start(function(){ leap.addValidator( 'game', val=>(val && val.length == 1 && ( val == 'x' || val == 'o' ) ? '' : "only 'x' or 'o' are allowed")); leap.addDataListener( 'game', 'change', checkForWinner ); leap.addErrorListener( 'game', errorFunction ); }); function getWinner() { let game = leap.get('game'); let winner; for( let i=0; i < 3; i++ ) { winner = ( game[i][0] == game[i][1] && game[i][1] == game[i][2] ) ? game[i][0] : ( ( game[0][i] == game[1][i] && game[1][i] == game[2][i] ) ? game[0][i] : null ); if( winner ) return winner; } return ( game[0][0] == game[1][1] && game[1][1] == game[2][2] ) ? game[0][0] : ( ( game[0][2] == game[1][1] && game[1][1] == game[2][0] ) ? game[0][2] : null ); } function checkForWinner(obj,prop,before,after,action) { let winner = getWinner(); if( winner ) { alert( winner + ' WINS'); } } function errorFunction(path,errors,obj,prop,before,after,action) { let elements = leap.getElements( path ); if( elements !== undefined ) { elements.forEach( e => { e.style.backgroundColor='red'; }); } } </script> <style> html, body { font-family: Arial, Helvetica, sans-serif; font-size: .8em; } input, .box { float: left; width: 3rem; height: 3rem; border: 1px solid black; text-align: center; line-height: 3rem; font-size: 2rem; } </style> </head> <body> <input type='text' data-field='game[0][0]' /> <input type='text' data-field='game[0][1]' /> <input type='text' data-field='game[0][2]' /> <input type='text' data-field='game[1][0]' style='clear: both;' /> <input type='text' data-field='game[1][1]' /> <input type='text' data-field='game[1][2]' /> <input type='text' data-field='game[2][0]' style='clear: both;' /> <input type='text' data-field='game[2][1]' /> <input type='text' data-field='game[2][2]' /> <div style='clear: both; padding: 20px;'> <div class='box'>{{game[0][0]}}</div> <div class='box'>{{game[0][1]}}</div> <div class='box'>{{game[0][2]}}</div> <div class='box' style='clear: both;'>{{game[1][0]}}</div> <div class='box'>{{game[1][1]}}</div> <div class='box'>{{game[1][2]}}</div> <div class='box' style='clear: both;'>{{game[2][0]}}</div> <div class='box'>{{game[2][1]}}</div> <div class='box'>{{game[2][2]}}</div> </div> </body> </html>
Clearing HTML Element Errors
Once an error condition for a data model value has been corrected, we'll want to remove any indication of an error from the related HTML elements. To do so, we can use a success data listener that gets called whenever the value of a data model has been successfully modified (meaning it has been changed without causing an error):
<script>leap.start( ()=>{
leap.addValidator( 'game', val=>(val && ( val == 'x' || val == 'o' ) ? "" : "only 'x' or 'o' are allowed"));
leap.addDataListener( 'game', 'change', checkForWinner );
leap.addErrorListener( 'game', errorFunction );
leap.addDataListener( 'game', 'success', successFunction );
});
</script>
Then in the successFunction we can revert all data model related HTML elements back to their non-error state:
function successFunction(path,obj,prop,before,after,action) { let elements = leap.getModelElements( obj, prop ); if( elements !== undefined ) { elements.forEach( e=>{ e.style.backgroundColor=''; }); } }
game.html
<!DOCTYPE html> <html> <head> <meta charset='ISO-8859-1'> <title>Leap Tutorial</title> <script src='http://leapjs.org/latest/leap.min.js'></script> <script> leap.start(function(){ leap.addValidator( 'game', val=>(val && val.length == 1 && ( val == 'x' || val == 'o' ) ? '' : "only 'x' or 'o' are allowed")); leap.addDataListener( 'game', 'change', checkForWinner ); leap.addErrorListener( 'game', errorFunction ); leap.addDataListener( 'game', 'success', successFunction ); }); function getWinner() { let game = leap.get('game'); let winner; for( let i=0; i < 3; i++ ) { winner = ( game[i][0] == game[i][1] && game[i][1] == game[i][2] ) ? game[i][0] : ( ( game[0][i] == game[1][i] && game[1][i] == game[2][i] ) ? game[0][i] : null ); if( winner ) return winner; } return ( game[0][0] == game[1][1] && game[1][1] == game[2][2] ) ? game[0][0] : ( ( game[0][2] == game[1][1] && game[1][1] == game[2][0] ) ? game[0][2] : null ); } function checkForWinner(obj,prop,before,after,action) { let winner = getWinner(); if( winner ) { alert( winner + ' WINS'); } } function errorFunction(path,errors,obj,prop,before,after,action) { let elements = leap.getElements( path ); if( elements !== undefined ) { elements.forEach( e => { e.style.backgroundColor='red'; }); } } function successFunction(path,obj,prop,before,after,action) { let elements = leap.getModelElements( obj, prop ); if( elements !== undefined ) { elements.forEach( e=>{ e.style.backgroundColor=''; }); } } </script> <style> html, body { font-family: Arial, Helvetica, sans-serif; font-size: .8em; } input, .box { float: left; width: 3rem; height: 3rem; border: 1px solid black; text-align: center; line-height: 3rem; font-size: 2rem; } </style> </head> <body> <input type='text' data-field='game[0][0]' /> <input type='text' data-field='game[0][1]' /> <input type='text' data-field='game[0][2]' /> <input type='text' data-field='game[1][0]' style='clear: both;' /> <input type='text' data-field='game[1][1]' /> <input type='text' data-field='game[1][2]' /> <input type='text' data-field='game[2][0]' style='clear: both;' /> <input type='text' data-field='game[2][1]' /> <input type='text' data-field='game[2][2]' /> <div style='clear: both; padding: 20px;'> <div class='box'>{{game[0][0]}}</div> <div class='box'>{{game[0][1]}}</div> <div class='box'>{{game[0][2]}}</div> <div class='box' style='clear: both;'>{{game[1][0]}}</div> <div class='box'>{{game[1][1]}}</div> <div class='box'>{{game[1][2]}}</div> <div class='box' style='clear: both;'>{{game[2][0]}}</div> <div class='box'>{{game[2][1]}}</div> <div class='box'>{{game[2][2]}}</div> </div> </body> </html>
Lists of Values
Rather than hard-code the possible players for the game in the code, it is a better practice to maintain the list of players as a model. The model can then be referenced for validation and population of the game. To create the list of players in the model, simply declare a new data model object in the leap.start(). A simple list of values for the players can be created with just the name of each player:
leap.set( 'players', [ 'x', 'o' ] );
Or more commonly, applications want to keep an internal value for each player, along with the name to be displayed in the application. To do so, we'll create a data model object that has value-name pairs where the object's property is the internal value for the player and the property value is the label to be displayed:
<script>
leap.start(function(){
leap.set( 'players', { '0' : 'x', '1' : 'o' } );
leap.addValidator( 'game', val=>(val && ( val == 'x' || val == 'o' ) ? "" : "only 'x' or 'o' are allowed"));
leap.addDataListener( 'game', 'change', checkForWinner );
leap.addErrorListener( 'game', errorFunction );
leap.addDataListener( 'game', 'success', successFunction );
});
<script>
let players = new Map(); players.set( 0, 'x' ); players.set( 1, 'o' ); leap.set('players', players );
game.html
<!DOCTYPE html> <html> <head> <meta charset='ISO-8859-1'> <title>Leap Tutorial</title> <script src='http://leapjs.org/latest/leap.min.js'></script> <script> leap.start(function(){ leap.set( 'players', { '0' : 'x', '1' : 'o' } ); leap.addValidator( 'game', val=>(val && val.length == 1 && ( val == 'x' || val == 'o' ) ? '' : "only 'x' or 'o' are allowed")); leap.addDataListener( 'game', 'change', checkForWinner ); leap.addErrorListener( 'game', errorFunction ); leap.addDataListener( 'game', 'success', successFunction ); }); function getWinner() { let game = leap.get('game'); let winner; for( let i=0; i < 3; i++ ) { winner = ( game[i][0] == game[i][1] && game[i][1] == game[i][2] ) ? game[i][0] : ( ( game[0][i] == game[1][i] && game[1][i] == game[2][i] ) ? game[0][i] : null ); if( winner ) return winner; } return ( game[0][0] == game[1][1] && game[1][1] == game[2][2] ) ? game[0][0] : ( ( game[0][2] == game[1][1] && game[1][1] == game[2][0] ) ? game[0][2] : null ); } function checkForWinner(obj,prop,before,after,action) { let winner = getWinner(); if( winner ) { alert( winner + ' WINS'); } } function errorFunction(path,errors,obj,prop,before,after,action) { let elements = leap.getElements( path ); if( elements !== undefined ) { elements.forEach( e => { e.style.backgroundColor='red'; }); } } function successFunction(path,obj,prop,before,after,action) { let elements = leap.getModelElements( obj, prop ); if( elements !== undefined ) { elements.forEach( e=>{ e.style.backgroundColor=''; }); } } </script> <style> html, body { font-family: Arial, Helvetica, sans-serif; font-size: .8em; } input, .box { float: left; width: 3rem; height: 3rem; border: 1px solid black; text-align: center; line-height: 3rem; font-size: 2rem; } </style> </head> <body> <input type='text' data-field='game[0][0]' /> <input type='text' data-field='game[0][1]' /> <input type='text' data-field='game[0][2]' /> <input type='text' data-field='game[1][0]' style='clear: both;' /> <input type='text' data-field='game[1][1]' /> <input type='text' data-field='game[1][2]' /> <input type='text' data-field='game[2][0]' style='clear: both;' /> <input type='text' data-field='game[2][1]' /> <input type='text' data-field='game[2][2]' /> <div style='clear: both; padding: 20px;'> <div class='box'>{{game[0][0]}}</div> <div class='box'>{{game[0][1]}}</div> <div class='box'>{{game[0][2]}}</div> <div class='box' style='clear: both;'>{{game[1][0]}}</div> <div class='box'>{{game[1][1]}}</div> <div class='box'>{{game[1][2]}}</div> <div class='box' style='clear: both;'>{{game[2][0]}}</div> <div class='box'>{{game[2][1]}}</div> <div class='box'>{{game[2][2]}}</div> </div> </body> </html>
Data Model Validation
Now that we have a fixed set of players in a data model, it makes more sense to validate the players using the players data model. Note that since MOD performs validation in the data model, we create a validator that validates the values in the game model with the list of possible values in the players model:
<script>
leap.start(function(){
leap.set( 'players', { '0' : 'x', '1' : 'o' } );
leap.addDataListener( 'game', 'change', checkForWinner );
leap.addErrorListener( 'game', errorFunction );
leap.addDataListener( 'game', 'success', successFunction );
leap.addModelValidator( 'game', 'players' );
});
<script>
We have just changed the valid values for each box to a number (0 or 1) with a display label for each ('x' & 'y'). So valid a value to be entered into the box is now number 0 or 1. Entering an 'x' or a 'y' will give an error since these are labels to be displayed, not actual acceptable values. For value-label pairs, a select element is a much more appropriate UI widget for a use to select a value (as we will do directly below).
game.html
<!DOCTYPE html> <html> <head> <meta charset='ISO-8859-1'> <title>Leap Tutorial</title> <script src='http://leapjs.org/latest/leap.min.js'></script> <script> leap.start(function(){ leap.set( 'players', { '0' : 'x', '1' : 'o' } ); leap.addDataListener( 'game', 'change', checkForWinner ); leap.addErrorListener( 'game', errorFunction ); leap.addDataListener( 'game', 'success', successFunction ); leap.addModelValidator( 'game', 'players' ); }); function getWinner() { let game = leap.get('game'); let winner; for( let i=0; i < 3; i++ ) { winner = ( game[i][0] == game[i][1] && game[i][1] == game[i][2] ) ? game[i][0] : ( ( game[0][i] == game[1][i] && game[1][i] == game[2][i] ) ? game[0][i] : null ); if( winner ) return winner; } return ( game[0][0] == game[1][1] && game[1][1] == game[2][2] ) ? game[0][0] : ( ( game[0][2] == game[1][1] && game[1][1] == game[2][0] ) ? game[0][2] : null ); } function checkForWinner(obj,prop,before,after,action) { let winner = getWinner(); if( winner ) { alert( winner + ' WINS'); } } function errorFunction(path,errors,obj,prop,before,after,action) { let elements = leap.getElements( path ); if( elements !== undefined ) { elements.forEach( e => { e.style.backgroundColor='red'; }); } } function successFunction(path,obj,prop,before,after,action) { let elements = leap.getModelElements( obj, prop ); if( elements !== undefined ) { elements.forEach( e=>{ e.style.backgroundColor=''; }); } } </script> <style> html, body { font-family: Arial, Helvetica, sans-serif; font-size: .8em; } input, .box { float: left; width: 3rem; height: 3rem; border: 1px solid black; text-align: center; line-height: 3rem; font-size: 2rem; } </style> </head> <body> <input type='text' data-field='game[0][0]' /> <input type='text' data-field='game[0][1]' /> <input type='text' data-field='game[0][2]' /> <input type='text' data-field='game[1][0]' style='clear: both;' /> <input type='text' data-field='game[1][1]' /> <input type='text' data-field='game[1][2]' /> <input type='text' data-field='game[2][0]' style='clear: both;' /> <input type='text' data-field='game[2][1]' /> <input type='text' data-field='game[2][2]' /> <div style='clear: both; padding: 20px;'> <div class='box'>{{game[0][0]}}</div> <div class='box'>{{game[0][1]}}</div> <div class='box'>{{game[0][2]}}</div> <div class='box' style='clear: both;'>{{game[1][0]}}</div> <div class='box'>{{game[1][1]}}</div> <div class='box'>{{game[1][2]}}</div> <div class='box' style='clear: both;'>{{game[2][0]}}</div> <div class='box'>{{game[2][1]}}</div> <div class='box'>{{game[2][2]}}</div> </div> </body> </html>
Repeating Data Elements
Previously users enter the name of the player in an input field. However, with a fixed set of players in a model, it makes more sense to allow the user to select from a list of players. To create a list, a select element can be created for each box on the tic-tac-toe board, where each of the options come from the players data model:
<select data-field="game[0][0]"> <option value=""></option> <option data-foreach='players' value="{{property}}">{{value}}</option> </select>
The select tag is linked to the game[0][0] data model value (so any selection made automatically updates the data model). Then an empty option is added to the list. Finally, the data-foreach attribute is used to auto-generate an option for each value in the players data model. Leap will iterate through all the values in the model and produce a copy of the option for each player, substituting in the property (the object property name - the player id) and the value (the object property value - the player display name) for each player.
To build the entire tic-tac-toe board, we would repleat the select block of code for all nine boxes for the board.
game.html
<!DOCTYPE html> <html> <head> <meta charset='ISO-8859-1'> <title>Leap Tutorial</title> <script src='http://leapjs.org/latest/leap.min.js'></script> <script> leap.start(function(){ leap.set( 'players', { '0' : 'x', '1' : 'o' } ); leap.addDataListener( 'game', 'change', checkForWinner ); leap.addErrorListener( 'game', errorFunction ); leap.addDataListener( 'game', 'success', successFunction ); leap.addModelValidator( 'game', 'players' ); }); function getWinner() { let game = leap.get('game'); let winner; for( let i=0; i < 3; i++ ) { winner = ( game[i][0] == game[i][1] && game[i][1] == game[i][2] ) ? game[i][0] : ( ( game[0][i] == game[1][i] && game[1][i] == game[2][i] ) ? game[0][i] : null ); if( winner ) return winner; } return ( game[0][0] == game[1][1] && game[1][1] == game[2][2] ) ? game[0][0] : ( ( game[0][2] == game[1][1] && game[1][1] == game[2][0] ) ? game[0][2] : null ); } function checkForWinner(obj,prop,before,after,action) { let winner = getWinner(); if( winner ) { alert( winner + ' WINS'); } } function errorFunction(path,errors,obj,prop,before,after,action) { let elements = leap.getElements( path ); if( elements !== undefined ) { elements.forEach( e => { e.style.backgroundColor='red'; }); } } function successFunction(path,obj,prop,before,after,action) { let elements = leap.getModelElements( obj, prop ); if( elements !== undefined ) { elements.forEach( e=>{ e.style.backgroundColor=''; }); } } </script> <style> html, body { font-family: Arial, Helvetica, sans-serif; font-size: .8em; } select { float: left; width: 3rem; height: 3rem; border: 1px solid black; text-align: center; line-height: 3rem; font-size: 2rem; } </style> </head> <body> <select data-field='game[0][0]'> <option value=''></option> <option data-foreach='players' value='{{property}}'>{{value}}</option> </select> <select data-field='game[0][1]'> <option value=''></option> <option data-foreach='players' value='{{property}}'>{{value}}</option> </select> <select data-field='game[0][2]'> <option value=''></option> <option data-foreach='players' value='{{property}}'>{{value}}</option> </select> <select data-field='game[1][0]' style='clear: both;'> <option value=''></option> <option data-foreach='players' value='{{property}}'>{{value}}</option> </select> <select data-field='game[1][1]'> <option value=''></option> <option data-foreach='players' value='{{property}}'>{{value}}</option> </select> <select data-field='game[1][2]'> <option value=''></option> <option data-foreach='players' value='{{property}}'>{{value}}</option> </select> <select data-field='game[2][0]' style='clear: both;'> <option value=''></option> <option data-foreach='players' value='{{property}}'>{{value}}</option> </select> <select data-field='game[2][1]'> <option value=''></option> <option data-foreach='players' value='{{property}}'>{{value}}</option> </select> <select data-field='game[2][2]'> <option value=''></option> <option data-foreach='players' value='{{property}}'>{{value}}</option> </select> </body> </html>
Modifying Lists of Values
We now have data validation and the selectable list of players linked to the players data model. If we wish to modify the players list of values (such as adding a third player named z, we simply modify the related data model by calling leap.append() to add values onto the data model.
To see this in action, open the browser's console and enter the following:
Since both the select options and the validation are linked to the players data model, any changes in the data model are immediately reflected on the page.
Components
As we saw above, copying the select code and options for all nine tic-tac-toe board boxes creates a lot of duplicated code. To avoid copying code, we would prefer define a template for the select that could be reused for each box on the board.
Components do exactly that. They are reusable blocks of HTML code that can be added to an HTML file by using standard HTML tags. To define a component, create an HTML template in the HTML that defines the code for the component:
<template id='game-selector'> <style> select { float: left; width: 3em; height: 3em; margin: .1em; border: 1px solid black; text-align: center; } </style> <select data-field='#data-field#' style='#style#'> <option value=''></option> <option data-foreach='players' value='{{property}}'>{{value}}</option> </select> </template>
The template tag is standard HTML that defines a re-usable block of HTML that does not appear on the page, but can be cloned onto the page once that page has loaded (see <template>: The Content Template element for details). Templates can have their own style elements that are independent of the main page's styles.
The id of the template will be the tag name that will be used in the HTML. The id (tag name) must contain at least one hyphen ('-') character. A good practice is to name components app-component.
Once a component has been defined, it can be used in your HTML by simply adding an HTML element, using the component's name as the tag name:
<game-selector data-field="game[0][0]"></game-selector>
game.html
<!DOCTYPE html> <html> <head> <meta charset='ISO-8859-1'> <title>Leap Tutorial</title> <script src='http://leapjs.org/latest/leap.min.js'></script> <script> leap.start(function(){ leap.set( 'players', { '0' : 'x', '1' : 'o' } ); leap.addDataListener( 'game', 'change', checkForWinner ); leap.addErrorListener( 'game', errorFunction ); leap.addDataListener( 'game', 'success', successFunction ); leap.addModelValidator( 'game', 'players' ); }); function getWinner() { let game = leap.get('game'); let winner; for( let i=0; i < 3; i++ ) { winner = ( game[i][0] == game[i][1] && game[i][1] == game[i][2] ) ? game[i][0] : ( ( game[0][i] == game[1][i] && game[1][i] == game[2][i] ) ? game[0][i] : null ); if( winner ) return winner; } return ( game[0][0] == game[1][1] && game[1][1] == game[2][2] ) ? game[0][0] : ( ( game[0][2] == game[1][1] && game[1][1] == game[2][0] ) ? game[0][2] : null ); } function checkForWinner(obj,prop,before,after,action) { let winner = getWinner(); if( winner ) { alert( winner + ' WINS'); } } function errorFunction(path,errors,obj,prop,before,after,action) { let elements = leap.getElements( path ); if( elements !== undefined ) { elements.forEach( e => { e.style.backgroundColor='red'; }); } } function successFunction(path,obj,prop,before,after,action) { let elements = leap.getModelElements( obj, prop ); if( elements !== undefined ) { elements.forEach( e=>{ e.style.backgroundColor=''; }); } } </script> <style> html, body { font-family: Arial, Helvetica, sans-serif; font-size: .8em; } </style> </head> <body> <template id='game-selector'> <style> select { float: left; width: 3em; height: 3em; margin: .1em; border: 1px solid black; text-align: center; } </style> <select data-field='#data-field#' style='#style#'> <option value=''></option> <option data-foreach='players' value='{{property}}'>{{value}}</option> </select> </template> <game-selector data-field='game[0][0]'></game-selector> <game-selector data-field='game[0][1]'></game-selector> <game-selector data-field='game[0][2]'></game-selector> <game-selector data-field='game[1][0]' style='!!clear:both;!!'></game-selector> <game-selector data-field='game[1][1]'></game-selector> <game-selector data-field='game[1][2]'></game-selector> <game-selector data-field='game[2][0]' style='!!clear:both;!!'></game-selector> <game-selector data-field='game[2][1]'></game-selector> <game-selector data-field='game[2][2]'></game-selector> </body> </html>
Component Creation
When the page loads, the component elements on the HTML page (for example the game-selector elements) will have the component's template HTML appended to them. In the example above, the game-selector will have the content of the template added to it (the select element and its option elements). Note however that the component template elements are not added directly as children of the component element, but instead added as DocumentFragments attached to a ShadowRoot node under the component element (see DocumentFragments and ShadowRoot).
Attaching the component's template HTML to a ShadowRoot means that the content is independent from the page holding the main HTML document. This means that the component can operate independently of the main HTML file, using styles that will not conflict with the main page's styles.
Component Selection
Since the generated component elements live in a separate DocumentFragment, they do not always interact with the main HTML page as other elements do. One area in particular is events. Some events are propagated upwards to the main HTML element while others do not. Therefore capturing events higher in the element hierarchy is not always possible.
Additionally, the generated component elements are not visible from the main document, which means component template elements will not be visible through selectors such as document.getElementById(), document.querySelector() and document.querySelectorAll(). Leap provides equivalent component-based selector functions for locating elements within the component template elements:
- leap.getComponentElementById(id [,parent])
- leap.componentQuerySelector(selector [,parent])
- leap.componentQuerySelectorAll(selector [,parent])
Component Event Listeners
Since component template elements fall under the ShadowRoot of the component element, you cannot directly add event listeners through the HTML document. Leap provides an equivalent function to add event listeners to components: leap.addComponentEventListener(selector,event,listener). The component event listener assignment works in the exact same way as the normal JavaScript addEventListener(), it just has the ability to locate component template elements in ShadowRoots and attach event listeners to them.
Component Attribute Substitution
Attribute values that are declared in a the component element (such as the data-field='game[0][0]' in the above example) can be passed into the component's template elements by using the attribute name surrounded by hash tags ('#') in the definition of the component's template HTML, such as:
<select data-field='#data-field#'>
which will take the value from the component element's data-field attribute and substitutes it into the template HTML when the component is generated and placed on the page.
Any HTML attribute from the component can be substituted into a component template. This includes class, style or even data- custom attributes. When component template HTML has an #attribute# substitution but the component element itself does not declare the attribute, the value of the attribute is set to an empty string.
The values of component attributes can be substituted anywhere in the component's template HTML, either in attributes or in the text of the HTML itself. For example, if you'd like to show the value of the component's data-field inside a div element, you could add the following to the template:
<div>{{#data-field#}}</div>
which would substitute the value of the component's data-field attribute (the name of the data field) into the div which in turn would be evaluated when rendered since it is inside {{ }} brackets.
Since we are using a value-name data model for the list of tic-tac-toe players, we can even use the #data-field# substitution inside a call to leap.getProperty() to get the display name for the player rather than just showing the player id:
<div>{{leap.getProperty('players','#data-field#')}}</div>
Component Attribute Erasure
By default, attributes and their values declared in the component element will be substituted into the component's template HTML and remain defined in the component element. If you wish to have the attribute value substituted into the component's template HTML then removed from the component element itself, you can add erasure brackets !! !! around the attribute value:
<game-selector data-field='!!game[0][0]!!'></game-selector>
Erasure brackets will cause the attribute value to be substituted into the component's template HTML when the component is generated, then be removed from the component element. In this case the select element will be rendered with data-field='game[0][0]' then the data-field attribute will be removed from the game-selector element.
Component Files
While its useful to define a component within an HTML page, it is even more useful to have a component definition that can be simply be added to any HTML page, being reused over and over again.
To create a reusable component, create a separate JavaScript file for the component. For the game-selector we'll create a file named selector.js. The component JavaScript file will contain all of the game's JavaScript functions, the start-up code and the component's template definition. Then to use the component, we only need to include the component's JavaScript file in the HTML file, then use the component tags as desired:
<!DOCTYPE html> <html> <head> <meta charset='ISO-8859-1'> <title>Leap Tutorial</title> <script src='http://leapjs.org/latest/leap.min.js'></script> <script src='selector.js'></script> <script> leap.start(); </script> <style> html, body { font-family: Arial, Helvetica, sans-serif; font-size: .8em; } </style> </head> <body> <div style='clear: both; padding: 20px;'> <game-selector data-field='!!game[0][0]!!'></game-selector> <game-selector data-field='!!game[0][1]!!'></game-selector> <game-selector data-field='!!game[0][2]!!'></game-selector> <game-selector data-field='!!game[1][0]!!' style='!!clear: both;!!'></game-selector> <game-selector data-field='!!game[1][1]!!'></game-selector> <game-selector data-field='!!game[1][2]!!'></game-selector> <game-selector data-field='!!game[2][0]!!' style='!!clear: both;!!'></game-selector> <game-selector data-field='!!game[2][1]!!'></game-selector> <game-selector data-field='!!game[2][2]!!'></game-selector> </div> </body> </html>
After moving all of the game functions to the selector.js file, we add the initialization calls to set up the players data model as well as the validator and listeners:
leap.ready(()=>{ leap.set( 'players', { '0' : 'x', '1' : 'o' } ); leap.addDataListener( 'game', 'change', checkForWinner ); leap.addModelValidator( 'game', 'players' ); leap.addErrorListener('game', errorFunction ); leap.addDataListener('game', 'success', successFunction ); });
Since we are creating the component's template from within the selector.js file (not in a standard HTML file), we'll make a call to leap.component() to register the component's template. Since the component's template will be a multi-line string, we'll pass the component template into leap.component() using a JavaScript Template Literal (see Template literals for details), enclosing the HTML inside backticks (`):
leap.component(` <template id="game-selector"> <style> select { float: left; width: 3em; height: 3em; margin: .1em; border: 1px solid black; text-align: center; } </style> <select data-field="#data-field#" style="#style#"> <option value=""></option> <option data-foreach='players' value="{{property}}">{{value}}</option> </select> </template> `);
selector.js
leap.ready(()=>{ leap.set( 'players', { '0' : 'x', '1' : 'o' } ); leap.addDataListener( 'game', 'change', checkForWinner ); leap.addModelValidator( 'game', 'players' ); leap.addErrorListener('game', errorFunction ); leap.addDataListener('game', 'success', successFunction ); }); leap.component(` <template id='game-selector'> <style> select { float: left; width: 3em; height: 3em; margin: .1em; border: 1px solid black; text-align: center; } </style> <select data-field='#data-field#' style='#style#'> <option value=''></option> <option data-foreach='players' value='{{property}}'>{{value}}</option> </select> </template> `); function getWinner() { let game = leap.get('game'); let winner; for( let i=0; i < 3; i++ ) { winner = ( game[i][0] == game[i][1] && game[i][1] == game[i][2] ) ? game[i][0] : ( ( game[0][i] == game[1][i] && game[1][i] == game[2][i] ) ? game[0][i] : null ); if( winner ) return winner; } return ( game[0][0] == game[1][1] && game[1][1] == game[2][2] ) ? game[0][0] : ( ( game[0][2] == game[1][1] && game[1][1] == game[2][0] ) ? game[0][2] : null ); } function checkForWinner(obj,prop,before,after,action) { let winner = getWinner(); if( winner ) { alert( winner + ' WINS'); } } function successFunction(path,obj,prop,before,after,action) { let elements = leap.getModelElements( obj, prop ); if( elements !== undefined ) { elements.forEach( e=>{ e.style.backgroundColor=''; if( e.tagName == 'GAME-BUTTON' ){ [...e.shadowRoot.children].forEach( c=>c.style.backgroundColor=''); } }); } } function errorFunction(path,errors,obj,prop,before,after,action) { let elements = leap.getElements( path ); if( elements !== undefined ) { elements.forEach( e=>{ e.style.backgroundColor='red'; if( e.tagName == 'GAME-BUTTON' ){ [...e.shadowRoot.children].forEach( c=>c.style.backgroundColor='red'); } }); } }
game.html
<!DOCTYPE html> <html> <head> <meta charset='ISO-8859-1'> <title>Leap Tutorial</title> <script src='http://leapjs.org/latest/leap.min.js'></script> <script src='selector.js'></script> <script> leap.start(); </script> <style> html, body { font-family: Arial, Helvetica, sans-serif; font-size: .8em; } </style> </head> <body> <game-selector data-field='game[0][0]'></game-selector> <game-selector data-field='game[0][1]'></game-selector> <game-selector data-field='game[0][2]'></game-selector> <game-selector data-field='game[1][0]' style='!!clear:both;!!'></game-selector> <game-selector data-field='game[1][1]'></game-selector> <game-selector data-field='game[1][2]'></game-selector> <game-selector data-field='game[2][0]' style='!!clear:both;!!'></game-selector> <game-selector data-field='game[2][1]'></game-selector> <game-selector data-field='game[2][2]'></game-selector> </body> </html>
REST
Single page apps commonlly rely on making REST/AJAX calls to service providers. Implementing REST calls into MOD is extremely easy. MOD focuses all state in models, therefore REST calls need only execute the call (optionally being sent a portion of the model) then update the model according to the returned results. Since all validators, listeners, UI components, etc. are coordinated through the data models, REST calls that update the model cause the entire application to be immediate kept in sync with any changes.
As a simple example, create a JSON file game.json in the same folder you have the game.html file so that it can be served up by the web server:
[['1','',''],['','0',''],['9','','1']]
Next we'll modify the game.html file to have a button that has an event listener that will:
- Make a call using fetch to get the JSON file
- Convert the result into JSON using fetch's toJson() function
- Set the game model with the results returned from the REST call
<script> leap.start( function(){ document.getElementById('load-game').addEventListener('click',e=> fetch('/leap/scripts/game/game.json').then(res=>res.json()).then( res=>leap.set('game',res))); }); </script> <body> <div style="clear: both; padding: 20px;"> <button id="load-game">Load Saved Game</button> </div> </body>
Once the data has returned from the REST call and has been converted to JSON, leap.set() is called with the results from the call to reset the game board. You'll notice that the board is immediate updated with the results, even including showing the error for the invalid value 9 that is returned in the JSON result.
For a detailed discussion on using fetch see Using Fetch.
- add() Add a new data model
- set() Set the data model, removing any existing data model of the same path
- merge() Merge the data into an existing data model, or create if not existing
- append() Append the data onto an existing data model, or create if not existing
- appendAll() Append the data array elements or data object properties on to the existing data model
- remove() Remove the data model
- setProperty() Set the value of an individual property for a data model
game.json
[['1','',''],['','0',''],['9','','1']]
selector.js (same as above)
game.html
<!DOCTYPE html> <html> <head> <meta charset='ISO-8859-1'> <title>Leap Tutorial</title> <script src='http://leapjs.org/latest/leap.min.js'></script> <script src='selector.js'></script> <script> leap.start( function(){ document.getElementById('load-game').addEventListener('click',e=> fetch('game.json').then(res=>res.json()).then( res=>leap.set('game',res))); }); </script> <style> html, body { font-family: Arial, Helvetica, sans-serif; font-size: .8em; } </style> </head> <body> <game-selector data-field='game[0][0]'></game-selector> <game-selector data-field='game[0][1]'></game-selector> <game-selector data-field='game[0][2]'></game-selector> <game-selector data-field='game[1][0]' style='!!clear:both;!!'></game-selector> <game-selector data-field='game[1][1]'></game-selector> <game-selector data-field='game[1][2]'></game-selector> <game-selector data-field='game[2][0]' style='!!clear:both;!!'></game-selector> <game-selector data-field='game[2][1]'></game-selector> <game-selector data-field='game[2][2]'></game-selector> <div style='clear: both; padding: 20px;'> <button id='load-game'>Load Saved Game</button> </div> </body> </html>
Leap for Java
Leap for Java is a Java-based server platform supporting REST services along with a large array of other server-side capabilities (set leapj4 for further details). Leap for Java provides a complete Master View Controller (MVC) infrastructure for Java developers.
Leap for Java provides a very simple means for integrating its back-end services into a Leap for JavaScript application. By simply including a JavaScript reference to a Leap for Java controller, a JavaScript-callable library is made available for direct calls from your JavaScript app. Additionally, Leap for Java provides specific REST controller support and semantics to make the passing of data back and forth (as well as error handling) easy.
If we have the following controller being served up on the server:
package org.leapjs.game.controllers; import org.json.JSONException; import org.json.JSONObject; import com.xobaa.controller.AppPath; import com.xobaa.controller.RestController; import com.xobaa.exceptions.ControllerException; import com.xobaa.response.JsonResponse; @AppPath(value = "game") public class GameController extends RestController { private String[][] game; private static JSONObject savedGame; public String[][] getGame() { return game; } public void setGame( String[][] game ) { this.game = game; } private boolean matches( String a, String b ) { return a != null && b != null && a.equals( b ); } @AppPath("getWinner") public JsonResponse<JSONObject< getWinner() throws ControllerException { String winner = null; String[][] game = getGame(); if( game != null ) { for( int i = 0; i < 3; i++ ) { winner = ( matches( game[i][0], game[i][1] ) && matches( game[i][1], game[i][2] ) ) ? game[i][0] : ( ( matches( game[0][i], game[1][i] ) && matches( game[1][i], game[2][i] ) ) ? game[0][i] : null ); if( winner != null ) break; } if( winner == null ) { winner = ( matches( game[0][0], game[1][1] ) && matches( game[1][1], game[2][2] ) ? game[0][0] : ( ( matches( game[0][2], game[1][1] ) && matches( game[1][1], game[2][0] ) ) ? game[0][2] : null ) ); } } try { return new JsonResponse<JSONObject<( new JSONObject().put( "winner", winner ) ); } catch( JSONException e ) { throw new ControllerException( e ); } } public static JSONObject getSavedGame() { return savedGame; } public static void setSavedGame( JSONObject savedGame ) { GameController.savedGame = savedGame; } @AppPath("saveGame") public JsonResponse<JSONObject< saveGame() throws ControllerException { validate(); savedGame = getAppRequest().getJson(); return new JsonResponse<JSONObject<( new JSONObject() ); } @AppPath("retrieveGame") public JsonResponse<JSONObject< retrieveGame() throws ControllerException{ JsonResponse<JSONObject< res = new JsonResponse<JSONObject<( savedGame ); return res; } private void validate() { String[][] game = getGame(); for( int r = 0; r < 3; r++ ) { for( int c = 0; c < 3; c++ ) { if( game[r][c] != null ) { try { Integer.parseInt( game[r][c] ); } catch( NumberFormatException e ) { getErrors().addError( "game[" + r + "][" + c + "]", "invalid value " + game[r][c] ); } } } } } }
We can integrate the controller (and thus all of its REST calls) into a JavaScript application by adding a script tag that calls the generic /leap/core/js controller then appends on the name of the application and the controller we wish to access:
<script src='/leap/core/js/game/GameController'></script>
Once the script has been included, we can natively make async JavaScript calls to all of the GameController's methods. As such, for the tic-tac-toe game, we can transition the work to the back end.
To transition the tic-tac-toe game to back end services, we'll convert the check for a winner to a REST call, then integrate additional REST calls to save and retrieve a game. The HTML code will be almost identical, only adding additional buttons to rest, save and retrieve a game.
To make the back end controller methods available in the selector.js file, we'll add the script to the top of our file. Note that since we are adding the controller access script inside a JavaScript file, we'll need to call leap.script() to dynamically add the script to the file:
leap.script('/leap/core/js/game/GameController');
To swap out the local (JavaScript) check for a winner with the REST call, we need only change the implementation of our JavaScript checkForWinner() function in the selector.js file:
function checkForWinner(path,obj,prop,before,after,action) { new GameController('game').getWinner().then( res=>res.json()).then( res=>{ if( res.winner ) alert( leap.get('players')[res.winner] + ' WINS'); } ); }
To make a REST call to a Leap service, we first instantiate the GameController by passing in the data model that is going to be sent in the REST calls. Note that multiple instances can be created for a single controller, each operating on a different data model.
After the controller has been instantiated, we make the call using the JavaScript version of the controllers Java method, using the same parameters that exist in the Java method (here we have no parameters for the getWinner() method).
REST calls using a controller function exactly like a normal fetch call in JavaScript, returning a Promise that can be used to examine the results and populate the model.
To support the additional reset, save and retrieve capability for the game, the following functions will be added to the selector.js file
function resetGame() { leap.set('game', [['','',''],['','',''],['','','']] ); } function saveGame() { new GameController('game').saveGame().then(res=>res.json()).then(res=>{ document.getElementById('errors').innerHTML = ''; if( !res.ok && res.errors !== undefined ) { for (const [key, value] of Object.entries(res.errors)) { let msg = document.createElement('div'); msg.innerText = `${key}: ${value}`; document.getElementById('errors').append( msg ); } } }); } function retrieveGame() { new GameController(null).retrieveGame().then(res=>res.json()).then( res=>leap.set('game',res.game)); }
Finally, we'll update the game.html file to add the additional buttons and event handling:
<script> leap.start(function() { document.getElementById('reset').addEventListener('click', resetGame); document.getElementById('save').addEventListener('click', saveGame); document.getElementById('retrieve').addEventListener('click', retrieveGame); }); </script> <body> <div style='padding: 2rem; clear: both;'> <div id='reset' class='float:left;'> <button type='button'>RESET</button> </div> <div id='save' class='float:left;'> <button type='button'>SAVE</button> </div> <div id='retrieve' class='float:left;'> <button type='button'>RETRIEVE GAME</button> </div> </div> <div id='errors'></div> </body>
GameController.java
package org.leapjs.game.controllers; import org.json.JSONException; import org.json.JSONObject; import com.xobaa.controller.AppPath; import com.xobaa.controller.RestController; import com.xobaa.exceptions.ControllerException; import com.xobaa.response.JsonResponse; @AppPath(value = 'game') public class GameController extends RestController { private String[][] game; private static JSONObject savedGame; public String[][] getGame() { return game; } public void setGame( String[][] game ) { this.game = game; } private boolean matches( String a, String b ) { return a != null && b != null && a.equals( b ); } @AppPath('getWinner') public JsonResponse<JSONObject> getWinner() throws ControllerException { String winner = null; String[][] game = getGame(); if( game != null ) { for( int i = 0; i < 3; i++ ) { winner = ( matches( game[i][0], game[i][1] ) && matches( game[i][1], game[i][2] ) ) ? game[i][0] : ( ( matches( game[0][i], game[1][i] ) && matches( game[1][i], game[2][i] ) ) ? game[0][i] : null ); if( winner != null ) break; } if( winner == null ) { winner = ( matches( game[0][0], game[1][1] ) && matches( game[1][1], game[2][2] ) ? game[0][0] : ( ( matches( game[0][2], game[1][1] ) && matches( game[1][1], game[2][0] ) ) ? game[0][2] : null ) ); } } try { return new JsonResponse<JSONObject>( new JSONObject().put( 'winner', winner ) ); } catch( JSONException e ) { throw new ControllerException( e ); } } public static JSONObject getSavedGame() { return savedGame; } public static void setSavedGame( JSONObject savedGame ) { GameController.savedGame = savedGame; } @AppPath('saveGame') public JsonResponse<JSONObject> saveGame() throws ControllerException { validate(); savedGame = getAppRequest().getJson(); return new JsonResponse<JSONObject>( new JSONObject() ); } @AppPath('retrieveGame') public JsonResponse<JSONObject> retrieveGame() throws ControllerException { JsonResponse<JSONObject> res = new JsonResponse<JSONObject>( savedGame ); return res; } private void validate() { String[][] game = getGame(); for( int r = 0; r < 3; r++ ) { for( int c = 0; c < 3; c++ ) { if( game[r][c] != null ) { try { Integer.parseInt( game[r][c] ); } catch( NumberFormatException e ) { getErrors().addError( 'game[' + r + '][' + c + ']', 'invalid value ' + game[r][c] ); } } } } } }
selector.js
leap.script('/leap/core/js/game/GameController'); leap.ready(()=>{ leap.set( 'players', { '0' : 'x', '1' : 'o' } ); leap.addDataListener( 'game', 'change', checkForWinner ); leap.addModelValidator( 'game', 'players' ); leap.addErrorListener('game', errorFunction ); leap.addDataListener('game', 'success', successFunction ); }); leap.component(` <template id='game-selector'> <style> select { float: left; width: 3em; height: 3em; margin: .1em; border: 1px solid black; text-align: center; } </style> <select data-field='#data-field#' style='#style#'> <option value=''></option> <option data-foreach='players' value='{{property}}'>{{value}}</option> </select> </template> `); function checkForWinner(path,obj,prop,before,after,action) { new GameController('game').getWinner().then( res=>res.json()).then( res=>{ if( res.winner ) alert( leap.get('players')[res.winner] + ' WINS'); } ); } function successFunction(path,obj,prop,before,after,action) { let elements = leap.getModelElements( obj, prop ); if( elements !== undefined ) { elements.forEach( e=>{ e.style.backgroundColor=''; if( e.tagName == 'GAME-BUTTON' ){ [...e.shadowRoot.children].forEach( c=>c.style.backgroundColor=''); } }); } } function errorFunction(path,errors,obj,prop,before,after,action) { let elements = leap.getElements( path ); if( elements !== undefined ) { elements.forEach( e=>{ e.style.backgroundColor='red'; if( e.tagName == 'GAME-BUTTON' ){ [...e.shadowRoot.children].forEach( c=>c.style.backgroundColor='red'); } }); } } function resetGame() { leap.set('game', [['','',''],['','',''],['','','']] ); } function saveGame() { new GameController('game').saveGame().then(res=>res.json()).then(res=>{ document.getElementById('errors').innerHTML = ''; if( !res.ok && res.errors !== undefined ) { for (const [key, value] of Object.entries(res.errors)) { let msg = document.createElement('div'); msg.innerText = `${key}: ${value}`; document.getElementById('errors').append( msg ); } } }); } function retrieveGame() { new GameController(null).retrieveGame().then(res=>res.json()).then( res=>leap.set('game',res.game)); }
game.html
<!DOCTYPE html> <html> <head> <meta charset='ISO-8859-1'> <title>Leap Tutorial</title> <script src='http://leapjs.org/latest/leap.min.js'></script> <script src='selector.js'></script> <script> leap.start(function() { document.getElementById('reset').addEventListener('click', resetGame); document.getElementById('save').addEventListener('click', saveGame); document.getElementById('retrieve').addEventListener('click', retrieveGame); }); </script> <style> html, body { font-family: Arial, Helvetica, sans-serif; font-size: .8em; } </style> </head> <body> <game-selector data-field='game[0][0]'></game-selector> <game-selector data-field='game[0][1]'></game-selector> <game-selector data-field='game[0][2]'></game-selector> <game-selector data-field='game[1][0]' style='!!clear:both;!!'></game-selector> <game-selector data-field='game[1][1]'></game-selector> <game-selector data-field='game[1][2]'></game-selector> <game-selector data-field='game[2][0]' style='!!clear:both;!!'></game-selector> <game-selector data-field='game[2][1]'></game-selector> <game-selector data-field='game[2][2]'></game-selector> <div style='padding: 2rem; clear: both;'> <div id='reset' class='float:left;'> <button type='button'>RESET</button> </div> <div id='save' class='float:left;'> <button type='button'>SAVE</button> </div> <div id='retrieve' class='float:left;'> <button type='button'>RETRIEVE GAME</button> </div> </div> <div id='errors'></div> </body> </html>
Complex Components
It is possible to create complex components, with a component composed of other components. Additionally multiple copies of a complex component can be placed on a single HTML page, each attached to an individual data model of its own. This is particularly useful for common objects, such as addresses, that have multiple copies on a single page and are to be managed in the same fashion.
Below is a push-button version of the tic-tac-toe board, having two boards on the same page, each attached to a different data model. Note how the data model for each tic-tac-toe board is passed from the component element into the component's template, then picked up in the component functions to determine which game board to take action on.
Also note that a game board is made up of nine game buttons, each one a component itself.
<template id="game-board"> <game-button data-field="#data-field#[0][0]" style="!!clear:both;!!"></game-button> <game-button data-field="#data-field#[0][1]"></game-button> <game-button data-field="#data-field#[0][2]"></game-button> <game-button data-field="#data-field#[1][0]" style="!!clear:both;!!"></game-button> <game-button data-field="#data-field#[1][1]"></game-button> <game-button data-field="#data-field#[1][2]"></game-button> <game-button data-field="#data-field#[2][0]" style="!!clear:both;!!"></game-button> <game-button data-field="#data-field#[2][1]"></game-button> <game-button data-field="#data-field#[2][2]"></game-button> </template>
To use such a complex component, we still only need to add a simple HTML element to the web page:
<game-board data-field="game"></game-board>
Since we have multiple instances of the complex component, we need to dynamically initialize each instance when it is created. HTML templates can include a script section to do exactly this when each instances is created.
<template id="game-board"> <script> leap.addComponentEventListener( "game-button", "click", setPlayer ); leap.addDataListener('#data-field#', 'change', checkForWinner ); leap.addModelValidator('#data-field#', 'players' ); leap.addDataListener('#data-field#', 'success', successFunction ); leap.addErrorListener('#data-field#', errorFunction ); leap.set( 'plays.#data-field#', 0 ); </script> </template>
Having multiple game boards means that we have multiple data models, one for each component instance. Each data model has its own name, game and game2. However, the GameController().checkForWinner() REST service expects a model named game. To support passing the model named game2 to GameController().checkForWinner()'s game model, we can instantiate the controller using an alias to use when submitting the REST request:
function checkForWinner(path,obj,prop,before,after,action) {
new GameController(leap.getRoot(path),'game').getWinner().then( res=>res.json()).then( res=>{if( res.winner ) alert( leap.get('players')[res.winner] + " WINS"); } );
}
board.js
leap.script('/leap/core/js/game/GameController'); leap.ready( ()=>{ leap.set('players', {'0':'x', '1':'o'} ); }); leap.component(` <template id='game-button'> <style> button { float: left; width: 3em; height: 3em; margin: .1em; border: 1px solid black; border-radius: 10px; text-align: center; } </style> <button type='button' data-field='#data-field#' style='#style#'> {{leap.getProperty('players','#data-field#')}}</button> </template> `); leap.component(` <template id='game-board'> <script> leap.addComponentEventListener( 'game-button', 'click', setPlayer ); leap.addDataListener('#data-field#', 'change', checkForWinner ); leap.addModelValidator('#data-field#', 'players' ); leap.addDataListener('#data-field#', 'success', successFunction ); leap.addErrorListener('#data-field#', errorFunction ); leap.set( 'plays.#data-field#', 0 ); </script> <game-button data-field='#data-field#[0][0]' style='!!clear:both;!!'></game-button> <game-button data-field='#data-field#[0][1]'></game-button> <game-button data-field='#data-field#[0][2]'></game-button> <game-button data-field='#data-field#[1][0]' style='!!clear:both;!!'></game-button> <game-button data-field='#data-field#[1][1]'></game-button> <game-button data-field='#data-field#[1][2]'></game-button> <game-button data-field='#data-field#[2][0]' style='!!clear:both;!!'></game-button> <game-button data-field='#data-field#[2][1]'></game-button> <game-button data-field='#data-field#[2][2]'></game-button> </template> `); function setPlayer(e) { if( typeof e == 'undefined' || !e.target.value || e.target.value == '' ) { let model = leap.getRoot(leap.getElementDataField(e.target)); let data = leap.getPropertyNames('players'); let numPlays = leap.get( 'plays.' + model ); leap.setElementData( e.target, data[numPlays%data.length] ); leap.set( 'plays.' + model, numPlays+1 ); } } function checkForWinner(path,obj,prop,before,after,action) { new GameController(leap.getRoot(path),'game').getWinner().then( res=>res.json()).then( res=>{if( res.winner ) alert( leap.get('players')[res.winner] + ' WINS'); } ); } function successFunction(path,obj,prop,before,after,action) { let elements = leap.getModelElements( obj, prop ); if( elements !== undefined ) { elements.forEach( e=>{ e.style.backgroundColor=''; if( e.tagName == 'GAME-BUTTON' ){ [...e.shadowRoot.children].forEach( c=>c.style.backgroundColor=''); } }); } } function errorFunction(path,errors,obj,prop,before,after,action) { let elements = leap.getElements( path ); if( elements !== undefined ) { elements.forEach( e=>{ e.style.backgroundColor='red'; if( e.tagName == 'GAME-BUTTON' ){ [...e.shadowRoot.children].forEach( c=>c.style.backgroundColor='red'); } }); } } function resetGame() { leap.set('game', [['','',''],['','',''],['','','']] ); } function saveGame() { new GameController('game').saveGame().then(res=>res.json()).then(res=>{ document.getElementById('errors').innerHTML = ''; if( !res.ok && res.errors !== undefined ) { for (const [key, value] of Object.entries(res.errors)) { let msg = document.createElement('div'); msg.innerText = `${key}: ${value}`; document.getElementById('errors').append( msg ); } } }); } function retrieveGame() { new GameController(null).retrieveGame().then(res=>res.json()).then( res=>leap.set('game',res.game)); } function addPlayer() { let player = document.getElementById('new-player'); if( player && player.value ) { let id = ( Math.max(...leap.getPropertyNames('players')) + 1 ).toString(); let newPlayer = {}; newPlayer[id]=player.value; leap.append('players', newPlayer ); document.getElementById('new-player').value = ''; } }
game.html
<!DOCTYPE html> <html> <head> <meta charset='ISO-8859-1'> <title>Leap TicTacToe</title> <script src='/leap/scripts/leap.min.js'></script> <script src='board.js'></script> <script> leap.start(function() { document.getElementById('reset').addEventListener('click', resetGame); document.getElementById('save').addEventListener('click', saveGame); document.getElementById('retrieve').addEventListener('click', retrieveGame); document.getElementById('add-player') .addEventListener('click', addPlayer); }); </script> <style> html, body { font-family: Arial, Helvetica, sans-serif; font-size: .8em; } table { border-collapse: collapse; } th { background-color: #ccc; } td, th { padding: .2rem; border: 1px solid black; text-align: center; border: 1px solid black; } </style> </head> <body> <div style='clear: both; margin: 1em;'> <table style='font-size: 2em;'> <tr> <th>PLAYER ID</th> <th>PLAYER NAME</th> </tr> <tr data-foreach='players'> <td>{{property}}</td> <td>{{value}}</td> </tr> </table> </div> <div> <input id='new-player' type='text' /> <button id='add-player' type='button'>ADD NEW PLAYER</button> </div> <div style='padding: 2rem; clear: both;'> <game-board data-field='game'></game-board> </div> <div style='padding: 2rem; clear: both;'> <div id='reset' class='float:left;'> <button type='button'>RESET</button> </div> <div id='save' class='float:left;'> <button type='button'>SAVE</button> </div> <div id='errors'></div> <div id='retrieve' class='float:left;'> <button type='button'>RETRIEVE GAME</button> </div> </div> <div style='padding: 2rem; clear: both;'> <game-board data-field='game2'></game-board> </div> </body> </html>