Tuesday 16 May 2017

Web Tutorial: AngularJS BMI Calculator

A couple years back, while beefing up on my front-end repertoire, I came across this nifty little front-end framework known as AngularJS. And had a fair bit of fun with it.

Angular 2 has come out since then, but for old times' sake, let's revisit one of the little projects I tinkered with in AngularJS. It's a BMI calculator I made for shits and giggles.

What's BMI?

It's an acronym for Body-Mass Index. Basically, you take your height and weight, then apply a formula that gives you the BMI. The BMI chart tells you whether you're dangerously underweight, overweight, or just right, or any range in between.

Let's get started!

For this, you'll need a HTML file, index.html, and a js file, main.js

The main.js file should be stored in the js folder. This is not absolutely necessary, but it's a good habit which we should follow.



Your HTML begins like this...
index.html
<!DOCTYPE html>
<html>
    <head>
        <title>BMI</title>
        <script src="js/main.js"></script>
    </head>

    <body>

    </body>
</html>


Now we're going to add a little something that makes this an Angular app - the reference to the external AngularJS library.
index.html
<!DOCTYPE html>
<html>
    <head>
        <title>BMI</title>
        <script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
        <script src="js/main.js"></script>
    </head>

    <body>

    </body>
</html>


Next, we add two range inputs with ids of rngHeight and rngWeight respectively, as well as labels for those inputs.
index.html
<!DOCTYPE html>
<html>
    <head>
        <title>BMI</title>
        <script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
        <script src="js/main.js"></script>
    </head>

    <body>
        <div>
            <label for="rngHeight">Height </label>
            <input type="range" id="rngHeight"> 
            <br />
            <label for="rngWeight">Weight </label>
            <input type="range" id="rngWeight">
        </div>
    </body>
</html>


Let's set rngHeight's input range between 1 to 300 cm, and rngWeight's range between 1 to 300000 g. Which is probably a reasonable estimate.
index.html
<!DOCTYPE html>
<html>
    <head>
        <title>BMI</title>
        <script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
        <script src="js/main.js"></script>
    </head>

    <body>
        <div>
            <label for="rngHeight">Height </label>
            <input type="range" id="rngHeight" min="1" max="300"
            <br />
            <label for="rngWeight">Weight </label>
            <input type="range" id="rngWeight" min="1" max="30000">
        </div>
    </body>
</html>


So this is what your current output looks like...


The front-end will do us, for now. Let's focus on adding functionality. To do this the Angular way, we need to modify the HTML this way.
index.html
<body ng-app="bmiApp">


And then we need to add this code into main.js. This declares a variable, app, and sets it to a module created by AngularJS's module() method, using the the element we just tagged with ng-app="bmiApp" above. That means anything within the body tag is now considered part of the app module.
main.js
var app = angular.module("bmiApp", []);


Now make these changes to your HTML. The div element that houses the range inputs rngHeight and rngWeight now has a new attribute ng-controller with the value "bmiCtrl". This tags it as the controller bmiCtrl. The rngHeight input has ng-model added as an attribute with the value "height_cm". Something similar has been done for rngWeight. Why will become clear momentarily.
index.html
        <div ng-controller="bmiCtrl">
            <label for="rngHeight">Height </label>
            <input type="range" id="rngHeight" min="1" max="300" ng-model="height_cm">
            <br />
            <label for="rngWeight">Weight </label>
            <input type="range" id="rngWeight" min="1" max="300000" ng-model="weight_g">
        </div>


Now, let's add this code in. This creates a controller in the app module, and passes in the string "bmiCtrl" as the first argument, and a function (with $scope as the argument) as the second argument.
main.js
var app = angular.module("bmiApp", []);

app.controller("bmiCtrl",
function($scope)
{

}
);


Now add this code into the function. This ensures that the default values for the local scope variables height_cm and weight_g are 100 and 10000 respectively.
main.js
var app = angular.module("bmiApp", []);

app.controller("bmiCtrl",
function($scope)
{
    $scope.height_cm=100;
    $scope.weight_g=10000;
}
);


Refresh. Since we've bound rngHeight to height_cm and rngWeight to weight_g, it should look like this now.


OK, great! Now that data looks correct, let's add a little visual indicator to the range inputs. Having the values in centimeters and grams is necessary for functionality, but a little too granular for human consumption. So we're going to display values in meters and kilograms.
index.html
        <div ng-controller="bmiCtrl">
            <label for="rngHeight">Height </label>
            <input type="range" id="rngHeight" min="1" max="300" ng-model="height_cm">{{height_m}} m
            <br />
            <label for="rngWeight">Weight </label>
            <input type="range" id="rngWeight" min="1" max="300000" ng-model="weight_g">{{weight_kg}} kg
        </div>


Your display now has "m" and "kg" next to the range inputs. You'll notice that the double curly bracers and stuff don't show up - that's because the curlies are AngularJS's placeholders for values. And since the values height_m and weight_kg are currently undefined, they simply don't show up.


One more thing we need to add to your HTML. ng-init ensures that the calc_bmi() function is run when the page is initialized.
index.html
        <div ng-controller="bmiCtrl" ng-init="calc_bmi()">
            <label for="rngHeight">Height </label>
            <input type="range" id="rngHeight" min="1" max="300" ng-model="height_cm">{{height_m}} m
            <br />
            <label for="rngWeight">Weight </label>
            <input type="range" id="rngWeight" min="1" max="300000" ng-model="weight_g">{{weight_kg}} kg
        </div>


Now let's do this. Here, within the scope defined by the controller bmiCtrl, we have the calc_cmi() function. First, it takes the values for height_cm and weight_g, and converts them using the functions convert_height() and convert_weight() respectively.
main.js
app.controller("bmiCtrl", function($scope)
{   
    $scope.calc_bmi=
    function()
    {
        $scope.height_m=convert_height($scope.height_cm);
        $scope.weight_kg=convert_weight($scope.weight_g);
    };

    $scope.height_cm=100;
    $scope.weight_g=10000;
});


And here we define the functions convert_height() and convert_weight(). These functions are not accessible by the app, and run only within the scope of calc_bmi. convert_height() converts the value of its argument to meters and returns the new value to two decimal places. convert weight() does something similar, except this time it converts grams to kilograms.
main.js
app.controller("bmiCtrl", function($scope)
{   
    $scope.calc_bmi=
    function()
    {
        function convert_height(ht)
        {
            return (ht/100).toFixed(2);
        }

        function convert_weight(wt)
        {
            return (wt/1000).toFixed(2);
        }

        $scope.height_m=convert_height($scope.height_cm);
        $scope.weight_kg=convert_weight($scope.weight_g);
    };

    $scope.height_cm=100;
    $scope.weight_g=10000;
});


Now the values for height_m and weight_kg should show up!


OK, now we're going to make this little app react to your input. This ensures that calc_bmi() is run not just on page initialization, but also whenever the values in the range inputs change!
index.html
        <div ng-controller="bmiCtrl" ng-init="calc_bmi()">
            <label for="rngHeight">Height </label>
            <input type="range" id="rngHeight" min="1" max="300" ng-change="calc_bmi()" ng-model="height_cm">{{height_m}} m
            <br />
            <label for="rngWeight">Weight </label>
            <input type="range" id="rngWeight" min="1" max="300000" ng-change="calc_bmi()" ng-model="weight_g">{{weight_kg}} kg
        </div>


Refresh. Do the values change when you move the sliders?


Right. From here on, it's just a matter of further applying what we've already done! Modify your HTML.
index.html
        <div ng-controller="bmiCtrl" ng-init="calc_bmi()">
            <label for="rngHeight">Height </label>
            <input type="range" id="rngHeight" min="1" max="300" ng-change="calc_bmi()" ng-model="height_cm">{{height_m}} m
            <br />
            <label for="rngWeight">Weight </label>
            <input type="range" id="rngWeight" min="1" max="300000" ng-change="calc_bmi()" ng-model="weight_g">{{weight_kg}} kg
            <h1>Your BMI is {{bmi}}</h1>
        </div>


Again, you should not be seeing anything within the curly bracers, because those values are undefined.


Modify the JavaScript. This new line ensures that height_m and weight_kg are converted to BMI after being defined by converting height_cm and weight_g.
main.js
app.controller("bmiCtrl", function($scope)
{   
    $scope.calc_bmi=
    function()
    {
        function convert_height(ht)
        {
            return (ht/100).toFixed(2);
        }

        function convert_weight(wt)
        {
            return (wt/1000).toFixed(2);
        }

        $scope.height_m=convert_height($scope.height_cm);
        $scope.weight_kg=convert_weight($scope.weight_g);

        $scope.bmi=convert_bmi($scope.height_m,$scope.weight_kg);
    };

    $scope.height_cm=100;
    $scope.weight_g=10000;
});


And of course, here we define the convert_bmi() function. It basically takes the height and weight in meters and kilograms respectively, and applies the formula, then returns the result to two decimal places.
main.js
app.controller("bmiCtrl", function($scope)
{   
    $scope.calc_bmi=
    function()
    {
        function convert_height(ht)
        {
            return (ht/100).toFixed(2);
        }

        function convert_weight(wt)
        {
            return (wt/1000).toFixed(2);
        }

        function convert_bmi(ht,wt)
        {
            return (wt/(ht*ht)).toFixed(2);
        }

        $scope.height_m=convert_height($scope.height_cm);
        $scope.weight_kg=convert_weight($scope.weight_g);

        $scope.bmi=convert_bmi($scope.height_m,$scope.weight_kg);
    };

    $scope.height_cm=100;
    $scope.weight_g=10000;
});


Now try your code again! The BMI calculation should appear, and should adjust with your sliders!


But hold on, we're not done yet. The BMI's just a number. The typical user, of course, wants to know what it means! So add this to your HTML.
index.html
        <div ng-controller="bmiCtrl" ng-init="calc_bmi()">
            <label for="rngHeight">Height </label>
            <input type="range" id="rngHeight" min="1" max="300" ng-change="calc_bmi()" ng-model="height_cm">{{height_m}} m
            <br />
            <label for="rngWeight">Weight </label>
            <input type="range" id="rngWeight" min="1" max="300000" ng-change="calc_bmi()" ng-model="weight_g">{{weight_kg}} kg
            <h1>Your BMI is {{bmi}}</h1>
            <p>{{remarks}}</p>
        </div>


This creates a new scope variable, remarks, which is derived from running the function convert_remarks() with bmi as an argument.
main.js
app.controller("bmiCtrl", function($scope)
{
    function convert_height(ht)
    {
        return (ht/100).toFixed(2);
    }

    function convert_weight(wt)
    {
        return (wt/1000).toFixed(2);
    }

    function convert_bmi(ht,wt)
    {
        return (wt/(ht*ht)).toFixed(2);
    }   

    $scope.calc_bmi=
    function()
    {
        $scope.height_m=convert_height($scope.height_cm);
        $scope.weight_kg=convert_weight($scope.weight_g);

        $scope.bmi=convert_bmi($scope.height_m,$scope.weight_kg);

        $scope.remarks=convert_remarks($scope.bmi);
    };

    $scope.height_cm=100;
    $scope.weight_g=10000;
});


And of course, convert_remarks() takes bmi and returns a text message based on its range!
main.js
app.controller("bmiCtrl", function($scope)
{
    function convert_height(ht)
    {
        return (ht/100).toFixed(2);
    }

    function convert_weight(wt)
    {
        return (wt/1000).toFixed(2);
    }

    function convert_bmi(ht,wt)
    {
        return (wt/(ht*ht)).toFixed(2);
    }

    function convert_remarks(bmi)
    {
        if (bmi<=20) return "You're malnourished.";
        if (bmi>20&&bmi<=22) return "Looking a tad lightweight here.";
        if (bmi>22&&bmi<=24) return "Looking good.";
        if (bmi>24&&bmi<=27) return "Time to shed a few pounds.";
        if (bmi>27) return "Dude, you have a problem. A HEAVY problem.";
    }   

    $scope.calc_bmi=
    function()
    {
        $scope.height_m=convert_height($scope.height_cm);
        $scope.weight_kg=convert_weight($scope.weight_g);

        $scope.bmi=convert_bmi($scope.height_m,$scope.weight_kg);

        $scope.remarks=convert_remarks($scope.bmi);
    };

    $scope.height_cm=100;
    $scope.weight_g=10000;
});


Re-run your code. The comments should appear now!


That's it for our little AngularJS project...

There's a whole lot more we can accomplish with this framework, of course. This wasn't anything I couldn't have done in plain old vanilla JavaScript, but it did feature two-way data binding and scoped functions and variables. Which makes this little app far more maintainable if I ever wanted to expand it. This little appetizer might actually motivate me to get off my arse and learn Angular2.

Is there more? Weight and see!
T___T

No comments:

Post a Comment