/ blockchain

EOSIO Blockchain dApp Step by Step: Part 4 - Google App Maker Frontend

Preface

This is the complement of the blog: EOSIO dApp on Blockchain Step-by-Step: Part 3. In the blog, I'll show how to build the Election Demo webapp using Google App Maker instead of HTML/CSS/Javascript manually coding. The backend has no change still using EOSIO Blockchain.

Google announced on 14 Jun 2018 that App Maker had been made generally available to G-suite business and enterprise users. App Maker is a low-code development environment for developing software based on its G Suite productivity and communications suite. App Maker uses templates for building UIs, which can be assembled via drag-and-drop.

Creation in Google App Maker

I've recorded the webapp creation to below video (silent mode). The whole creation process was about 30 minutes and has been reduced to 5 minutes.

Below listed two scripts (client script and server script) which are copy&paste to App Maker:

ClientScripts:

var _account;

/////////////////////////////////////////////
// Internal Functions
/////////////////////////////////////////////

// Call server-side script unlockWalletRestApi()
function callServerUnlockWallet_(callback) {
  var status = app.pages.ElectionPage.descendants.ConnectionStatus; // the status label
  google.script.run.withFailureHandler(function(error) {
    // An error occurred, so display an error message.
    status.text = error.message;
  }).withSuccessHandler(function(result) {
    callback();
  }).unlockWalletRestApi();
}

// Call server-side script createAccountRestApi()
function callServerCreateAccount_(callback) {
  var status = app.pages.ElectionPage.descendants.SubmitStatus; // the status label
  google.script.run.withFailureHandler(function(error) {
    // An error occurred, so display an error message.
    status.text = error.message;
  }).withSuccessHandler(function(result) {
    // Store the account and show the panelVote
    _account = JSON.parse(result).account;
    callback(true); // return a true result to callback
  }).createAccountRestApi();
}

// Call server-side script voteCandidateRestApi()
function callServerVoteCandidate_(candidateKey) {
  var status = app.pages.ElectionPage.descendants.SubmitStatus; // the status label
  google.script.run.withFailureHandler(function(error) {
    // An error occurred, so display an error message.
    status.text = error.message;
  }).withSuccessHandler(function(result) {
  }).voteCandidateRestApi(_account, candidateKey);
}

/////////////////////////////////////////////
// Functions availiable for pages
/////////////////////////////////////////////

function onFormLoad() {
  app.pages.ElectionPage.descendants.panelVote.visible = false;
  app.pages.ElectionPage.descendants.panelVoteResult.visible = false;
}

function onInputNameSubmitClicked(submitButton) {
  var props = submitButton.root.properties;
  if (submitButton.root.descendants.panelInputName.validate()) {
    props.CreatingAccount = true;
    callServerUnlockWallet_(function() {
      callServerCreateAccount_(function(result) {
        props.CreatingAccount = false;
        if (result) {
          // Show next panel if success
          app.currentPage.root.descendants.panelVote.visible = true;
        }
      });
    });
  }
}

function onVoteButtonClicked(voteButton) {
  var selected = voteButton.root.descendants.dropdownCandidate.value;
  console.log('dropdownCandidate: ' + selected);
  var props = voteButton.root.properties;
  props.VotingCandidate = true;

  var candidateKey = null;
  for (var i = 0; i < app.datasources.ElectionData.items.length; ++i) {
    var candidate = app.datasources.ElectionData.items[i];
    if (candidate.Name == selected) {
      candidateKey = candidate.Key;
    }
  }
  console.log('candidateKey=' + candidateKey);
  callServerVoteCandidate_(candidateKey);
  app.datasources.ElectionData.load(function() {
    app.currentPage.root.descendants.panelVoteResult.visible = true;
    props.VotingCandidate = false;
  });
}

ServerScripts:

function unlockWalletRestApi() {
  // Make a POST request with a JSON payload.
  var options = {
    'method': 'post',
    'payload': ''
  };
  UrlFetchApp.fetch('http://demo.simonho.net:5000/api/unlock_wallet', options);
}

function createAccountRestApi() {
  // Make a POST request with a JSON payload.
  var options = {
    'method': 'post',
    'contentType': 'application/json',
    'payload': JSON.stringify({})
    //'payload': '',
  };
  var response = UrlFetchApp.fetch('http://demo.simonho.net:5000/api/create_account', options);
  console.log(response.getContentText());
  return response.getContentText();
}

function voteCandidateRestApi(account, candidateKey) {
  console.log('account: ' + account);
  console.log('candidateKey: ' + candidateKey);
  // Make a POST request with a JSON payload.
  var data = {
    account: account,
    candidate: candidateKey
  };
  var options = {
    'method': 'post',
    'payload': data
  };
  UrlFetchApp.fetch('http://demo.simonho.net:5000/api/vote_candidate', options);
}

function candidatesRestApi_() {
  var url = 'http://demo.simonho.net:5000/api/candidates';

  console.log('Election backend url: ' + url);

  var response = JSON.parse(UrlFetchApp.fetch(url));
  return response; 
}

function voteRestApi_() {
  // Make a POST request with a JSON payload.
  var data = {
    'name': 'Bob Smith',
    'age': 35,
    'pets': ['fido', 'fluffy']
  };
  var options = {
    'method' : 'post',
    'contentType': 'application/json',
    // Convert the JavaScript object to a JSON string.
    'payload' : JSON.stringify(data)
  };
  UrlFetchApp.fetch('http://demo.simonho.net:5000/api/vote_candidate', options);
}

function getCandidates_() {
  var response;
  try {
    response = candidatesRestApi_();
  } catch (error) {
    throw new Error('Unable to call RESTful API from candidatesRestApi_()');
  }

  var records = response.map(function (item) {
    var record = app.models.ElectionData.newRecord();

    record.Count = item._count;
    record.Key = item._key;
    record.Name = item._name;

    return record;
  });

  return records;
}

The Produced Webapp

Below is the video recorded (at normal speed) the webapp which produced by App Maker:

Conclusion

In my experiment, I've used about one hour to make the webap. Compares to the traditional webapp (in past 3) which used about three hours. App Maker could reduce 70% less of creation time. It is best for making intranet or internal applications. App Maker not only capable connects to external RESTful service, but also connect to Google Cloud SQL. In this case, it provides auto-synchronize the data to the database with no coding needed.