Sunday 13 March 2016

Getting stuck in with AngularJS

In response to my LAST POST regarding using Angular with what could be considered the ASP.NET 2016 toolchain (Typescript, Bower, NodeJS and Gulp), I got some response saying that it was a little hard to follow. It was a pretty targeted poast aimed at ASP.NET developers looking at using a more modern approach to their JavaScript, but I thought I’d try my hand at my own getting stuck in post with AngularJS. This is aimed at people who are beginners or intermediate with web development.

This post contains rough getting started code, shortcuts, borderline hacking and alcoholic cocktails. Consider yourself warned.

Introduction

My last post saw me take a first dive into AngularJS myself, but I was viewing it in the context of working out how best to get and work with JavaScript tooling as an ASP.NET developer. Angular was really just a use case. Here though, I want to dig into using Angular further, and aim at a larger group of people.

This post is really for people who have played with HTML and JavaScript before, maybe even JQuery and other frameworks, but who haven’t written an SPA (single-page application). I’m aiming it at beginner to intermediate level. If you’re a beginner, you’re welcome to just download the code and have a play with it to see how it works and/or change it. If you’re intermediate, you might want to follow along at each step and see what works and what doesn’t. Do whatever works for you.

What you’ll need

I hope I don’t put anyone off with this list of tools you’ll need to do this.

  • A text editor (such as notepad)
  • A web browser (such as Internet Explorer, Firefox, Chrome or Safari)

Yep, that’s right, that’s all you’ll need! If you haven’t got those, well, this post probably isn’t for you.

This post should work with Windows, Mac or Unix systems, but if not, please let me know in the comments.

Very basic app

So, let’s walk through making a basic app to learn this Angular stuff. I’ve decided to veer away from beer. Instead, we’re going to go with cocktails! It’s okay, I’ve got alcoholic and non-alcoholic covered. If cocktails aren’t your thing, well, you can use whatever you like, really. But cocktails are fun, so we’ll go with those.

First thing’s first, let’s make a folder (or directory) somewhere for us to work in.

In your folder, let’s make an index.html file first. This file’s going to be an easy one.

index.html

<!doctype html>
<html>
<head>
    <title>My cocktail app</title>
</head>
<body>
    <h1>My cocktail list</h1>
</body>
</html>

Hopefully nothing too alarming here. You can open this page in your web browser and you’ll see, as you’d expect, just your heading saying “My cocktail app”.

Getting hold of the libraries

Okay, so we’re going to want to get hold of some libraries, such as Angular, to be able to use it.

So, a nice complicated downloading process, or installing of tools to pull down the libraries, right? Well no, I promised shortcuts, and we’re going to use one here. We’re just going to get them from CDNs (online repositories of static content like javascript libraries). If you’re interested and want to find out more about CDNs, read this article on Why CDN from GTMetrix.

So, to get hold of the libraries, we’re just going to put the address of each CDN version of each thing we want to import in the src attribute of the link or script tag. Don’t worry, you don’t have to go get all those. Here are the link and script tags. Insert these after the title element in your head tag.

<!-- script and css references -->
<!-- references to libraries online that we'll use through CDNs -->
<!-- JQuery (you'll find bootstrap needs this) -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.0/jquery.min.js"></script>
<!-- bootstrap -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js" integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" crossorigin="anonymous"></script>
<!-- angular -->
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.5/angular.js"></script>
<!-- angular route -->
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.5/angular-route.js"></script>

Copy and paste. Easy, right? Now, you’ll find when you browse to your index page, it’ll pull in all the libraries and CSS (from bootstrap) when you load the page.

Note: I don’t actually use the bootstrap CSS in this post, but if you did want to pretty this up, Bootstrap is great for that.

Setting up the main list of the app

When we first go to our app, we want to display a list of the cocktails we know about. Optionally, we can add in an option to switch between alcoholic and non-alcoholic cocktails.

We’re going to be using a few new techniques that if you were following on from the last post, we never got into. Routing and templates.

A couple of quick explanations. Routing is allowing your app to cope with you going to different URLs (or addresses) on your app. On a server side app, you might show a different page, but in this single-page application, we get the app to react appropriately to you going to a different page, as you’ll see. Templates are simply separate .html files which hold your markup for different parts of your app. You could, of course, bung it all in your index.html file and in the spirit of shortcuts I could’ve done that, but it gets untidy real quick.

Template

So starting with easiest first, let’s do the template for the list of cocktails. Create a new file called list.html with the following content.

<h2>Drinks list</h2>

<!-- let people choose alcoholic or non-alcoholic -->
<label>
    <input type="radio" ng-model="alcoholic" ng-value="true" />
    Alcoholic
</label>
<label>
    <input type="radio" ng-model="alcoholic" ng-value="false" />
    Non-alcoholic
</label>

<div ng-repeat="drink in drinks">
    <a ng-href="#/drink/{{drink.id}}">{{drink.name}}</a>
</div>

This is nice and simple, with a couple of small bits of angular trickery. If you’re not interested on the specifics of these Angular directives we used, you can jump to the next section.

The first is on those radio buttons. ng-model sets what we’re changing when someone changes which radio button is selected, on what’s called the scope. The scope is basically the state of the app, or more specifically that particular part of the app. So in this instance, the scope is the state of the app around listing the cocktails. The ng-value is an angular thing that allows you to have the value be more than just a string (letters). It essentially allows us to use true and false as a Boolean on the scope. If you don’t quite get what that’s about, don’t worry, it helps make writing our app easier, but getting it’s not essential.

The next bit of trickery is the ng-repeat attribute. It basically allows us to loop through all the drinks we know about. If you want to access the current drink, it will be reachable with the word drink. The braces {{}} basically output what’s in the middle of them. So knowing that drink will be the current drink, you can start to see what this repeater does. It loops through the drinks, that are on the scope (or in the app in other words), outputs a link that goes to #/drink/(the drink ID), and the link text will be the drink’s name.

Congrats, you’ve just made your first template!

The controller

Firstly, before we write a controller, we need to let Angular know we’re making an app. Let’s make a nice and easy app.js in our directory with this code.

angular.module("cocktailApp", []);

This code just registers a new “module” (or app if you prefer) with angular, called “cocktailApp”. We’ll use this again soon. And yeah, you need those square brackets at the end. I’ll spare you the details for now though.

Now we write the Javascript behind the list of cocktails. Here we go. Create a listCtrl.js file in your directory. Here’s the content.

angular.module("cocktailApp")
    // we now add our list code (or controller) to the cocktail app
    .controller("ListCtrl", function ($scope) {
        // set the default for whether we're filtering for alcoholic cocktails or not
        $scope.alcoholic = true;
        // hardcode some drinks to get this working
        $scope.drinks = [{
            name: "Taquila Sunrise",
            id: 1
        },
        {
            name: "Cheeky Vinto",
            id: 2
        },
        {
            name: "Dirty Margarita",
            id: 3
        }];
    });

Nothing too complex here. You’ll notice we use the angular.module function again. This time, we only pass in the name of our app, and this must match what we put in when we set up the app in app.js.

We then call the controller function on it to add our ListCtrl (list controller). The second argument is where we add our controller logic. We pass in the $scope to this function, and that is where we set all the variables setting the state of this portion of the app (remember I mentioned scopes earlier)? So, we set alcoholic to true, which will set the alcoholic radio button to default to be selected (the radio button itself isn’t alcoholic… er, never mind). We also hardcode some drinks to display, giving them both an ID and a name. If you remember the repeater we wrote from the last section where we wrote the template, this will create a link and display the name for them.

Hopefully you can see how the controller we just wrote and the template play together. Soon, you’ll see them in action. Not yet, but soon.

An aside. Anyone know who “Dirty Margarita” was?

Showing the details of a drink

So we’ve got our fake list of cocktails as the main intro to the app, but we’d like users to be able to click on a cocktail to find out more.

No problem, we undergo a very similar process to make the drink details part of the app.

Template

Firstly, let’s make a really simple template for the drink display. Make a new file drink.html, with this HTML.

<a href="#/">Back to list</a>

<h2>{{name}}</h2>

Notice the back to list link uses a hash (#) at the beginning, just like the links to go to the drink details in list.html. This is because typically in single-page apps, the bits after the # in a URL are what the SPA uses to navigate. There is a way to use HTML5 compliant URLs that don’t use this hash in them, but I won’t cover that in this post. See this Scotch.io post on this subject for more info if you want to do that.

Also, notice we’re using the {{}} syntax again to put in what’s called an expression. In this case, just putting out the drink’s name in that heading tag.

Controller

Again, we’ll want some code behind the drink details to help us display it. Make a drinkCtrl.js file in your directory, with this code.

angular.module("cocktailApp")
    .controller("DrinkCtrl", function ($scope, $routeParams) {
        // depending on the ID, we output its name to the scope. a switch statement will do nicely
        switch ($routeParams.drinkid) {
            case "1":
                $scope.name = "Taquila Sunrise";
                break;
            case "2":
                $scope.name = "Cheeky Vinto";
                break;
            case "3":
                $scope.name = "Dirty Margarita";
                break;
                // in case it's an ID we don't know about
            default:
                $scope.name = "Unknown";
                break;
        }
    });

Once again, we get the app we’re building using angular.module(“cocktailApp”), and attach a controller, this time a DrinkCtrl. This time, we pass 2 arguments into the function, the scope again, which we know about (the state of the app), and a new one, $routeParams. $routeParams, in short, is any information that are attached as part of the URL. Query strings, or in this case an ID. So when you browse to #/drink/2, the idea is that we’ll know you’re after the drink with ID 2. That bit is yet to be set up.

Once we have the ID, we do a simple switch on it to decide which drink name to show. Nothing too fancy there!

Routing

Now, it’s time to sort out how our app handles different addresses. This is where we will be able to pass in that drink ID property to the drink controller that you just wrote.

All this is going to happen in app.js. Change it so that it looks like this.

// here we set up our app with its name, and also add ngRoute as something we need    
angular.module("cocktailApp", ["ngRoute"])
    // now we use the $routeProvider of ngRoute to configure our app
    // here, we're making sure going to certain addresses have the response we want
    .config(function ($routeProvider) {
        $routeProvider.when("/", {
            // the controller we will use for this URL. in this case, we're using our list functionality
            controller: "ListCtrl",
            // templateUrl is the html page that has the template, or how this feature should look
            templateUrl: "list.html"
        }).when("/drink/:drinkid", {
            // notice the colon above? that means we're going to grab that variable from the address and it will be called "drinkid"
            controller: "DrinkCtrl",
            templateUrl: "drink.html"
        }).otherwise({
            redirectTo: "/"
        });
    });

Yeah, it just got a heap more complicated than that one line file that it used to be. But it’ll make sense I hope, and hopefully the comments in there will elp explain it a bit.

Firstly, we put “ngRoute” between those square brackets where we create the cocktailApp module. ngRoute, or angular-route, is AngularJSs specific solution to helping you take control of the routing of your app. Put simply, it helps you direct traffic around your app. By putting something between those square brackets after declaring a module, we tell angular that we depend on that. We used to depend on nothing, but now we depend on ngRoute.

We now use a .config function to, well, configure the app. We pass in the $routeProvider and this is what we use to set up those routes.

$routeProvider.when basically says when someone (a user or the app) goes to the given address, use this javascript code (controller) and this template (templateUrl) to display it. We use “/” as our default URL, both as it’s a URL with nothing after it, and through the .otherwise redirection, which basically redirects any unrecognized addresses at “/”, which in our case is our list of cocktails.

Our second route, /drink/:drinkid, uses a parameter. Notice the “:”. This takes whatever’s in this place in the URL and assigns it to the $routeParams under drinkid, or whatever you put in after the colon up until the next part of the route. Remember we were looking for $routeParams.drinkid in the drink controller? Here’s where it gets set.

Plug it in, plug it in

Well, that was a heap of work. Now, save all the files (just in case) and open up your index.html page again in a browser.

Ah, rats, looks like nothing happened. There’s a couple more things we need to do first.

First up, and old JS hats might have already done this, you need to reference those js files you just made in your index.html file. Put this in the head section of your index.html file, under the references to angular and the rest.

<!-- our own JS code -->
<script src="app.js"></script>
<script src="listCtrl.js"></script>
<script src="drinkCtrl.js"></script>

Note, it is very important that this goes after your calls to bring in Angular and angular-route. Best just put it at the bottom of your head tag. If it loads all this up in the wrong order, it’ll put itself into a tizzy. No one’s got time for that.

Now, your JS code is in there too, but still nothing’s happening. That’s because we haven’t put the app we made in those js files into this index.html yet. Let’s do this now.

<body ng-app="cocktailApp">
    <h1>My cocktail list</h1>
    <!-- the active part of the app displays in this div tag -->
    <div ng-view></div>
</body>

Once again, the name in that ng-app tag has to match the name we’ve used everywhere else in the code “cocktailApp”. You’ll soon hit problems if it doesn’t. The only other thing we’ve done here is added an ng-view div. ng-view basically says that the combined output from the controller and the template when we go to a route will go in here. So anything around the ng-view element will display regardless of where we are in the app, so you can use this for titles and side menus and what have you.

Now, save and refresh. You should see a jaw-dropping list of a whole 3 cocktails! Nice. Now, try clicking one of those links. It should take you to a new view, which is just that back link and the drink’s name in the heading. But it did it! And if you click that back link, you go back to the list. No refreshes either, this is a single-page app. Snazzy, right?

You can download a zip of the solution here.

Congrats, you’ve just made (what might be) your first AngularJS app! Yeah, it’s a bit basic, a bit ugly and doesn’t actually do much, but it’s yours.

An actually useful app

I admit that basic app doesn’t actually give us much for all that effort. However, here’s where things an get interesting. If you’ve not been put off so far (and I hope you haven’t), let’s carry on and actually make this app a real working cocktails list.

We’re going to be using a cocktails API I found on the internet from the author of cocktailDB. They’ve kindly made a JSON API that people can use.

We need to make a few changes to our app to both start using their API to get our results, and to deal with the output, as it’s formatted a bit differently to how I expected in that last section.

Changing the list

First, we want to change the list so that rather than displaying our hardcoded list of cocktails, we go and get the list of cocktails from the cocktail DB, taking into account the user’s choice of alcoholic or non-alcoholic. It sounds tough, but it’s not so bad.

Here’s what’s in my listCtrl.js now.

angular.module("cocktailApp")
    // we now add our list code to the cocktail app
    .controller("ListCtrl", function ($scope, $http) {
        $scope.alcoholic = true;

        // this $watch trick allows us to refresh the drinks every time the user goes from alcoholic to non-alcoholic
        // whenever it changes, we call this function
        $scope.$watch("alcoholic", function () {
            refreshDrinks($scope, $http);
        });

        // let's put this logic in a nice reusable function here
        // since it uses the $scope and the $http, we need to pass them in
        function refreshDrinks($scope, $http) {
            var query; // whether to search for alcoholic or non-alcoholic cocktails
            // $scope.alcoholic will change whenever the radio buttons are changed
            if ($scope.alcoholic) {
                query = "Alcoholic";
            } else {
                query = "Non_Alcoholic";
            }
            // if you want an explanation of the http://crossorigin.me, either see that website or my blog
            var url = "http://crossorigin.me/http://www.thecocktaildb.com/api/json/v1/1/filter.php";
            // now, we use the $http object to go fetch the drinks from the API out there
            $http.get(url, {
                params: {
                    a: query
                }
            })
                // this .then syntax says that the function there waits until the call to the cocktails API finishes
                .then(function (response) {
                    var data = response.data; // the actual drinks returned by the api
                    $scope.drinks = data.drinks;
                });
        }
    });

I decided to annotate the code there as much as I can, to make it more understandable. It does mean that there’s not much I can say here necessarily, as hopefully I’ve explained it all in the code.

The bit where I mention about calling crossorigin.me, see the problem with SOP section for more info if you’re interested. If it wets your appitite, that’s the bit that may be borderline hacking, or it’s at the very least working around a problem that was a real pain for me when trying to make this app. That’s really only if you’re curious though, you probably won’t deal with this sort of thing when making your own apps, or at least I hope not.

We have to make some changes to the list.html template too, to deal with the differences in names of things coming back from the API. We don’t need to change anything but the names of properties though. Here’s list.html.

<h2>Drinks list</h2>

<!-- let people choose alcoholic or non-alcoholic -->
<label>
    <input type="radio" ng-model="alcoholic" ng-value="true" />
    Alcoholic
</label>
<label>
    <input type="radio" ng-model="alcoholic" ng-value="false" />
    Non-alcoholic
</label>

<div ng-repeat="drink in drinks">
    <a ng-href="#/drink/{{drink.idDrink}}">{{drink.strDrink}}</a>
</div>

Save everything and refresh. And check out that list of drinks there! Not only that, try switching between alcoholic and non-alcoholic, and watch the list live-update to suit your preference. Tasty!

Changing the drink display

So, you might notice that the drinks display now doesn’t work. But let’s make it do more than display the name anyway. Using the API, we can get back an image (depending on if there’s one available), list of ingredients, instructions on how to make it and even the recommended type of glass!

We have to make similar changes to the drinks controller as we did for the list to make it use $http to call the API. Here’s drinkCtrl.js.

angular.module("cocktailApp")
    .controller("DrinkCtrl", function ($scope, $routeParams, $http) {
        $scope.id = $routeParams.drinkid;
        getDrink($scope.id, $scope, $http);

        function getDrink(id, $scope, $http) {
            // if you want an explanation of the http://crossorigin.me, either see that website or my blog
            var url = "http://crossorigin.me/http://www.thecocktaildb.com/api/json/v1/1/lookup.php?i=" + id;
            // now use $http.get again to fetch us our details
            $http.get(url)
                .then(function (response) {
                    var drink = response.data.drinks[0];
                    // get the ingredients for the drink out of the response
                    drink.ingredients = getIngredients(drink);
                    $scope.drink = drink;
                });
        }

        // this function is kind of working around the cocktail DB API and turning it into something we can use and display
        function getIngredients(drink) {
            var ingredients = [];
            var counter = 1;
            // keep looping through the ingredients in drink until the next one's blank
            while (true) {
                var ingredient = drink["strIngredient" + counter];
                // if the ingredient is either not there or is empty, we've got all the ingredients
                if (!ingredient || ingredient == '') {
                    break;
                }

                var measurement = drink["strMeasure" + counter];
                // we've found an ingredient, so put it on the ingredients array to display
                ingredients.push({
                    ingredient: ingredient,
                    measure: measurement
                });

                counter++;
            }
            return ingredients;
        }
    });

We do the URL slightly differently in this controller. We append i=id to the end of it. There was no particular reason behind this, I just wanted to show you could do it either by sticking it on the end of the url, such as here, or using a params object, like in the list controller. $http.get is fine with either.

We also have to handle some foolishness with the ingredients to get them out, as the API has a bit of a strange way of sending ingredients and their measurements back. It sends them back like this.

{
    "strIngredient1": "taquila",
    "strIngredient2": "largar",
    "strIngredient3": "",
    ...
    "strIngredient15": ""
}

As you can see, it will always send back 15 ingredients, and any that aren’t used in a cocktail are set to empty quotes. Same with measurements of these ingredients. So what we do, is we keep going through the ingredients (with that while loop) until we find an ingredient that’s blank or we don’t find one at all. And we pull them out into a nice usable collection, ready for use in our template. Using it as this array we send back in the template will be a hell of a lot easier.

Speaking of templates, you probably guessed there’d be some changes we need to make to display not only the ingredients, but all the other stuff we said we were going to display. So let’s get to it. Here’s my drink.html.

<a href="#/">Back to list</a>

<h2>{{drink.strDrink}} <small>{{drink.strAlcoholic}}</small></h2>
<div>
    <div ng-if="drink.strDrinkThumb && drink.strDrinkThumb != ''" class="drink-thumb">
        <img ng-src="{{drink.strDrinkThumb}}" alt="Image of the cocktail" />
    </div>
    <div>
        <section class="ingredients">
            <strong>Ingredients: </strong>
            <div ng-repeat="ingredient in drink.ingredients">
                <span>{{ingredient.ingredient}}</span>
                <span>{{ingredient.measure}}</span>
            </div>
        </section>
        <section class="glass">
            <strong>Recommended glass: </strong>
            {{drink.strGlass}}
        </section>
        <section class="instructions">
            <strong>Instructions: </strong>
            {{drink.strInstructions}}
        </section>
    </div>
</div>

You’ll hopefully understand some of the newer bits in here now. Nothing too out of the ordinary of outputting bits from the scope to the screen (such as the drink’s name and whether it’s alcoholic or not). But we do use a repeater on the drinks ingredients to go over each of them and put them each out to the screen. Hopefully they make sense to you now.

Save and refresh, and try going into a drink from the list. There you should (hopefully) see the cocktail, what’s in it and how to make it!

You can download a zip of this solution that uses the API here.

Congrats, our work is done here! It works, brings back real data and displays it. Sure, it’s not a UI masterpiece, but that’s something you can work on if you feel like getting your CSS on. Feel free to try find a cocktail you can make at home, you probably deserve it.

The problem with SOP

This section is a little on the heavy side. If that’s not your bag, that’s cool, just jump to the next section.

I mentioned that this bit is possibly a bit of a hack around a problem that might have derailed using a separate API for this post. There is a way to do this “properly”, but I won’t go into too much detail on it, since it involves you having or setting up a webserver, which may or may not be in your capabilities. And for a sample app, it feels like overkill. For a real app, though, you probably would.

SOP, or Same-Origin Policy as it’s affectionately known, is a policy built into all modern browsers. It’s aim is to protect us from malicious websites. It allows websites to make requests to other addresses, as long as they’re both part of the same “origin”, or domain. If you’re wondering about why, see this question on StackOverflow, Why is the same-origin policy so important?

So, great, this thing is there for our protection, so how would anyone reach anyone else’s domain? There’s a couple of ways to do it.

The first and recommended is that you would implement CORS, or Cross-Origin Resource Sharing (the web loves an acronym). This is something you implement server side, and tells clients what domains it would expect to receive requests from.

As a contrived example, say that Google wanted to make javascript XHR GET requests from your browser to a resource on Microsoft.com. Normally, this would fall foul of SOP, as Google and Microsoft are not part of the same domain, and your browser has no reason to trust you (or Google) to call resources from Microsoft, so your browser would block the use of that request. However, Google and Microsoft may warm to each other (stranger things have happened), and there may be a legitimate reason why Google might request resources from Microsoft using JavaScript. Microsoft would then implement CORS on their server at their end, and Google will now be able to make these requests without upsetting the browser’s same-origin policy.

Great, so how does that apply to us? Well, not very well. It requires having either control of the webserver yourself (I do not have control over the API’s webserver that I’m calling), or else ask the owner of the webserver to do it for you. This is something that wouldn’t work for this, both because we’re not running this from an origin (I’m just launching index.html in my browser), and because this is a blog post, every person that wanted to play with the useful version of our app would all have to contact the author of the Cocktail DB and ask them to grant them access, or else open up access to everyone, which may or may not be something they’re willing to do.

I mentioned there was another way, and that is to use a server to contact the other server. Because SOP is built into browsers, not servers, using your server to fetch data from someone else’s API is completely fine. This is often a simpler solution if you don’t control the webserver you’re calling.

Problem here is, it involves having a webserver in the first place. We’re just running this file on our computer, and we have no back end code to call off to the remote API. So we use what’s called a CORS proxy to access the other resource, without having to own it.

In this article, I use CrossOrigin.me. Click through to see how it works. Big props to TechnoBoy10 for doing this. If this part of this blog post helped you, feel free to drop him a small donation in gratitude. This part of the post wouldn’t have happened without a CORS proxy like his to help.

Bit of a sidestep around CORS, granted, but it’s for development use. You probably shouldn’t do that for anything serious you write.

Conclusion

In this post, we covered the creation of an AngularJS app using as simple a toolset as we could. We built a sample app that displayed hard coded fake data, then built on that to bring back real data from an API, all in a nice little SPA.

This post wound up being a bit longer than I anticipated, my apologies! I hope the extra detail came in useful.

My thanks for this post go out to Zag, the creator of the cocktailDB, which we used to make our app a real app! Also thanks again to TechnoBoy10, for his work on CrossOrigin.me.

As always, I hope you found the post here useful. Feel free to comment with any tips for improvement or general feedback, including (hopefully) some success stories!

Written with StackEdit.

No comments:

Post a Comment

As always, feel free to leave a comment.