Angular Powered Text Transformations - Part 1
Tell me if this sounds familiar?
You have some uniform text you just copied from somewhere, like a set of column or property names. Maybe you got it from a spreadsheet. Maybe you copied it from the results of a query you ran... or maybe it was buried in the requirements somewhere.
Regardless you've got something that looks like this:
Name FaceShape LegacyProperty NeverGetsUsed NobodyKnowsWhatThisMeans KillMeNow
Or maybe even this:
Name
FaceShape
LegacyProperty
NeverGetsUsed
NobodyKnowsWhatThisMeans
KillMeNow
And now you need to use those same names in your code, and probably in more than one place. It might be you need to turn those into JavaScript class properties in one place, and a C# interface somewhere else.
Now What?
You brute force it that's what you do!
The Problem
You copy and paste and check and double check to make sure you didn't accidentally screw up in the process. At some point you will probably say to yourself:
Oh my goodness if I have to copy and paste this one more time I am going to slit my wrists!
But then you realize hey, I'm a programmer, and why do anything more than once when you can have a computer do it for you.
Unfortunately being the analytical type, you will also probably formulate a graph in your head that looks something like this:
Realizing that the time it would take to automate this, for a one-off solution, wouldn't be any faster than just doing it manually, you decide to bite the bullet and get back to it. So you mumble some obscenities under your breath and go back to copy-pasting your way to hating life.
Angular To The Rescue! ...and javascript
Wouldn't it be nice if there were like a mini-ETL application that let you put together little text transformations and run them whenever you wanted to? Wouldn't it be nice if it were online so you could get to it from anywhere?
Angular.js gives us all the tools we need to build such a tool, so why don't we dive in and get started.
We'll keep this basic for part 1 and slowly evolve it to be more useful. Visually we only need three things:
- An input for our raw text
- An output for the result
- A button to initiate the transform
Using Bootstrap the markup might look something like this:
<div>
<form role="form">
<div class="form-group">
<label for="tbInput">Input</label>
<textarea id="tbInput" class="form-control" rows="6"></textarea>
</div>
<div class="form-group">
<label for="tbOutput">Output</label>
<textarea id="tbOutput" class="form-control" rows="6"></textarea>
</div>
<div class="form-group">
<button type="button" class="btn btn-default">Submit</button>
</div>
</form>
</div>
The goal is to compose our transformation into a series of small steps, where the output from the last step becomes the input to the next.
Now let's create a simple Angular module and controller we can use to bind to that markup. Besides having some properties that we can bind our controls to, we need something to model our transformation pipeline. To start, we can just use a simple array of functions.
Here is the skeleton:
var TransformCtrl = function () {
this.input = "";
this.output = "";
//This will eventually hold functions
this.transformations = [];
};
TransformCtrl.prototype.transform = function () {
//Handle the transformation pipeline here
};
var module = angular.module('ng-transform', []);
module.controller('transformCtrl', Controllers.TransformCtrl);
Transformation Pipeline
Now let's build out that transformation pipeline. We want to keep it as simple as possible for right now, but the idea is to pass the initial input into the begining of the pipeline, and then pass the output of that into the next function. Rinse. Repeat.
TransformCtrl.prototype.transform = function () {
//Make copies for posterity
var input = angular.copy(this.input),
transformFunctions = this.transformations.slice(0);
//Kick off the pipeline and keep pushing the output
// from the last step into the next
for (var i = 0; i < transformFunctions.length; i++) {
input = transformFunctions[i].call(this, angular.copy(input));
}
this.output = input;
};
So that's it. Pretty simple to start.
You'll notice that I am making a copy of the input initially to pass into the first function, and then capturing the result of each step of the transformation, which I then copy and pass into the next step. This gives us a nice linear pipeline, and the foundation to build off of.
Adding Transformations
Without actually adding some transformations to the mix, this thing is going to be pretty useless, so let's put together a few examples to demonstrate the process.
Suppose we wanted to take our tab separated list of names up there, and turn it into a JavaScript object. We might want to break that down into roughly 4 steps.
- Split the single line into an array of strings
- Alphabetize the names
- Lower case the first letter
- Format the remaining output as a simple JS Object
Each of those steps can be modelled as a function within our pipeline. So let's go ahead and build them out now.
TransformCtrl.prototype.splitWhiteSpace = function (input) {
return input.split(/\s+/);
};
TransformCtrl.prototype.alphabetize = function (input) {
input.sort(function (a, b) {
return a.localeCompare(b);
});
return input;
};
TransformCtrl.prototype.lowercaseFirst = function (input) {
var output = [],
i = 0;
for (i = 0; i < input.length; i++) {
output.push(this.lowercaseFirstSingle(input[i]));
}
return output;
};
TransformCtrl.prototype.lowercaseFirstSingle = function (str) {
return str.replace(/^([A-Z]{1})/g, function (match) {
return match.toLowerCase();
});
};
TransformCtrl.prototype.propertiesToJSObject = function (input) {
var objLines = ["var NewObj = function(config){"];
//Format each property as "this.name = config.name;"
angular.forEach(input, function (prop) {
objLines.push(" this." + prop + " = config." + prop + ";");
});
objLines.push("};");
return objLines.join("\n");
};
Once we have all of those function added to our controller, we can simply build up our transformation pipeline by adding them to the array of functions. Sprinkle in a little angular binding into our markup and we will have a very basic text transformation utility.
var TransformCtrl = function () {
this.input = "";
this.output = "";
this.transformations = [
this.splitWhiteSpace,
this.alphabetize,
this.lowercaseFirst,
this.propertiesToJSObject
];
};
Results
The end result is that you end up transforming this line of text:
Name FaceShape LegacyProperty NeverGetsUsed NobodyKnowsWhatThisMeans KillMeNow
Into this little code snippet:
var NewObj = function(config){
this.faceShape = config.faceShape;
this.killMeNow = config.killMeNow;
this.legacyProperty = config.legacyProperty;
this.name = config.name;
this.neverGetsUsed = config.neverGetsUsed;
this.nobodyKnowsWhatThisMeans = config.nobodyKnowsWhatThisMeans;
};
In part 2 we will look at using a service to DRY up our code, add some error handling, and support for multiple transformations.