A JavaScript Framework for Robust UI Applications

Originally published by Nalla Senthilnathan at https://dzone.com

Framework Overview

This framework has the following core components:

  • uiParams — that define the view state.
  • <app>States — that are various possible view states.
  • <app>Events — that are various possible native and custom events.
  • stateTransitionsController — that orchestrates various state transitions.

where <app> corresponds to the app name like turnstile, order etc.

The framework suggests configuring the UI application as a series of state transitions with each transition like:

[initial state] -> [native event] -> [handler] -> [custom event] -> [final state]

The initial and final states are configured in <app>States, the native and custom events are configured in <app>Events.

When a native event is triggered, the controller dispatches it to a configured handler, and when the handler completes the execution of the business rule it, in turn, triggers a configured event based on the outcome of the execution.

When a custom event is triggered, the controller transitions the view to a configured final state.

A Sample UI Application

Consider a turnstile UI application where a user enters a coin amount to unlock the turnstile, and if there are no errors, the user can push to gain access. For server model integration, consider the coin amount validation handled by the server and the result returned as a JSON. See the UI design for this example here.

The framework suggests that we first create a state transitions table like:

Once the table is correctly constructed, the rest of the steps are straighforward.

The uiProps are configured like:

var uiProps = {
state: "defaultState",
welcomeShow: "block",
turnstileLockedShow: "block",
turnstileUnlockedShow: "none",
coinTxtDisabled: false,
coinTxtValue: "",
coinBtnDisabled: false,
pushBtnDisabled: true,
coinErrorMsgShow: "none",
thankyouShow: "none"
}

The states in the table are configured for the example like:

const turnstileStates = {
defaultState : function(e) {
renderApp(uiProps);
},
coinSuccessState : function(e) {
uiProps.state="coinSuccessState";
uiProps.turnstileLockedShow="none";
uiProps.turnstileUnlockedShow="block";
uiProps.coinTxtDisabled=true;
uiProps.coinTxtValue=e.data.coinval;
uiProps.coinBtnDisabled=true;
uiProps.pushBtnDisabled=false;
uiProps.coinErrorMsgShow="none";
renderApp(uiProps);
},
coinErrorState : function(e) {
uiProps.state="coinErrorState";
uiProps.turnstileLockedShow="block";
uiProps.turnstileUnlockedShow="none";
uiProps.coinTxtDisabled=false;
uiProps.coinTxtValue=e.data.coinval;
uiProps.coinBtnDisabled=false;
uiProps.pushBtnDisabled=true;
uiProps.coinErrorMsgShow="block";
uiProps.coinErrorMsgText=e.data.errorMessage;
renderApp(uiProps);
},
pushSuccessState : function(e) {
uiProps.state="pushSuccessState";
uiProps.welcomeShow="none";
uiProps.thankyouShow="block";
uiProps.turnstileLockedShow="none";
uiProps.turnstileUnlockedShow="none";
uiProps.coinTxtDisabled=true;
uiProps.coinBtnDisabled=true;
uiProps.pushBtnDisabled=true;
uiProps.coinErrorMsgShow="none";
renderApp(uiProps);
}
};

where the utility function renderApp is:

function renderApp(uiProps){
$("#state").text(uiProps.state);
$("#thankyou").css("display", uiProps.thankyouShow)
$("#turnstileLocked").css("display", uiProps.turnstileLockedShow);
$("#turnstileUnlocked").css("display", uiProps.turnstileUnlockedShow);
$("#coinTxt").prop("disabled", uiProps.coinTxtDisabled);
$("#coinTxt").val(uiProps.coinTxtValue);
$("#coinBtn").prop("disabled", uiProps.coinBtnDisabled);
$("#pushBtn").prop("disabled", uiProps.pushBtnDisabled);
$("#coinErrorMsg").css("display", uiProps.coinErrorMsgShow);
$("#coinErrorMsg").text(uiProps.coinErrorMsgText);
}

The events in the table are configured like:

const turnstileEvents = {
coinEvent : {
handleCoin : function(e) {
//call the server api to validate coin amount
var result = null;
isCoinAmountValid(e.data.coinval())
.done(function(serverData){
$("body").triggerHandler("turnstileEvents.coinSuccessEvent");
})
.fail( function(xhr, textStatus, errorThrown) {
if(xhr.status==0)xhr.responseText="Unknown server error, please try later.";
$("body").triggerHandler("turnstileEvents.coinErrorEvent", xhr.responseText);
    });
return result;
}
},
coinSuccessEvent : {
nextState : function(e) {
return turnstileStates.coinSuccessState(e);
}
},
coinErrorEvent : {
nextState : function(e) {
//e.data.errorMessage = 'server sent messge';
return turnstileStates.coinErrorState(e);
}
},
pushEvent : {
handlePush : function(e) {
return turnstileEvents.pushSuccessEvent;
}
},
pushSuccessEvent : {
nextState : function(e) {
return turnstileStates.pushSuccessState(e);
}
}
};

Note that the nextState() is configured in turnstileEvents instead of in the turnstileStates since we see in the state transitions table that the post-event dictates what the next state should be.

Finally, the stateTransitionsController activities are configured in the jQuery body like:

    //handle the page load event
    //initiaize uiData if necessary
    //for the current example there is no server data on page load
turnstileStates.defaultState(event);
    //handle the coin event
    $("#coinBtn").on("click",{ coinval : function(){return $("#coinTxt").val();} },function(event) {
turnstileEvents.coinEvent.handleCoin(event);
});
    //handle the push event
$("#pushBtn").on("click", function() {
return turnstileEvents.pushEvent.handlePush(event).nextState(event);
});
    //handle the custom coinSuccessEvent
    $("body").on("turnstileEvents.coinSuccessEvent", {coinval:function(){return $("#coinTxt").val();}}, function(event){
    return turnstileEvents.coinSuccessEvent.nextState(event);
    });
    //handle the custom coinErrorEvent
    $("body").on("turnstileEvents.coinErrorEvent",{errorMessage:"", coinval:function(){return $("#coinTxt").val();}}, function(event, param){
    event.data.errorMessage=param;
    return turnstileEvents.coinErrorEvent.nextState(event);
    });

Ajax call to the server API:

//use ajax post to the server api /turnstile/payment 
function isCoinAmountValid(coinAmt){
if(coinAmt=='')coinAmt=0.0;
var requestBody = "{ \"payment\":" + coinAmt +"}";
$.ajaxSetup({
   headers:{
      'Content-Type': "application/json"
   }
});
return $.post('http://localhost:8080/turnstile/payment', requestBody)
}

Consider a Spring Boot controller like:

@PostMapping("/turnstile/payment")
public TurnstileData validatePayment(@Valid @RequestBody TurnstileData turnstileData) throws Exception {
  if(turnstileData.getPayment()<=0) {
    throw new BadDataException("Payment value must be more than 0.00");
  }
  return turnstileData;
}

The full source code for the application is on my GitHub.

When the GitHub project is imported into an IDE like STS and run, the state transitions listed above in the table can be tested.

Test #1: On accessing http://localhost:8080/jqStateMachine.html we get the defaultState view like:

Default size of turnstile application

Test #2: While the defaultState is active, on entering 0 in the textbox and clicking "Drop Coins" button we get the following coinErrorState.

Coin error state as a result of entering zero coins

Test #3: While the defaultState is active, on entering 86 in the textbox and clicking "Drop Coins" button we get the following coinSuccessState:


Coin success state as a result of entering valid number coins

Test #4: While cointErrorState is active, on entering 86 and clocking the "Drop Coins" button, we get the following coinSuccessState. 

The turnstile is unlocked!

Test #5: While the coinSucessState is active, on clicking the "Push" button, we get the following pushSuccessState. 

Push success state

Conclusions

In conclusion, it can be said that the framework steps are quite straightforward once the state transitions table is correctly created. Even though a major change like server integration was added to the application, the complexity of the application increased only marginally with the clean separation of concerns remaining unaffected.

Thanks for reading

If you liked this post, please do share/like it with all of your programming buddies!

Follow us on Facebook | Twitter

Further reading

The Complete JavaScript Course 2019: Build Real Projects!

Vue JS 2 - The Complete Guide (incl. Vue Router & Vuex)

JavaScript Bootcamp - Build Real World Applications

The Web Developer Bootcamp

JavaScript Programming Tutorial - Full JavaScript Course for Beginners

New ES2019 Features Every JavaScript Developer Should Know

Best JavaScript Frameworks, Libraries and Tools to Use in 2019

JavaScript Basics Before You Learn React

Build a CMS with Laravel and Vue

Google’s Go Essentials For Node.js / JavaScript Developers


#javascript #json #web-development

A JavaScript Framework for Robust UI Applications
10.65 GEEK