onEmpty: A Cool Filter to Display Messages when Data Fields are Empty
The Problem:
We know that sometimes there is not going to be data recorded in a field in the database. We want the DOM to put a message in that spot, at least, so it doesn't look broken. Just having it be blank would be confusing.
The Solution:
We know that blank fields can happen with most columns in the database - name, profile photo, attachments, etc. We shouldn't write specific code for every scenario... Let's write a generic filter instead that can handle various "no data" present scenarios.
Our Approach:
We wrote a function called "onEmpty". We named it onEmpty because it fills the role of what to do when presented with an "empty" database field. When the Angular in our HTML runs onEmpty, we see a "Yeah, there is nothing here" message where we need to see a message.
Writing a filtering function wasn't that tricky because filters are sort of built in in Angular. You can check out the documentation for Angular filters <a href="https://docs.angularjs.org/api/ng/filter/filter">here</a>. Here's a slightly modified example from the Angular api.
{{ scope_expression | filter : value }}
Inside the curly braces, the first part, before the pipe, represents the info that is on scope. The part between the pipe and the colon represents the name of our function. The part after the colon represents the value (in this case, a string) that we want to feed to our function.
Now, here's the documentation for our onEmpty function:
/**
* Empty State e.g. no results, nothing found, etc
* Usage: {{someValue | onEmpty: 'some empty message' }}
* If no argument is passed, returns n/a
*/
OnEmpty is basically a function with 2 parameters divided by a pipe. The first parameter is what we are looking for in the database. The second parameter is the function that returns the message we want to show if the database comes up empty handed.
The JavaScript:
.filter('onEmpty', function() {
return function(value, str) {
if(!value) {
if(!str) {
return 'n/a';
}
return str;
}
return value;
};
})
If you read through it, it says:
- Here is a function that takes 2 arguments, value and string.
- Check for no value first
- If there is no value, check for no string...
- If there is no string, return "n/a"
- If there is a string, return that string
- However, if there is a value, return that value
Example 1:
Show "unknown" when there is no name recorded in the database
The HTML:
<span class="associate-name">{{ journalEntry.user.name | onEmpty: 'unknown associate / automatic message' }}</span>
What the code is saying here is, "Try to insert 'user.name'. Or, if no 'user.name' is available, then insert, 'unknown associate'. In the example below, you can see "Andrew Elliot" as the user in the first jpg and see "unknown associate" in place of Andrew's name in the second jpg.
Here's what you see in the rendered HTML when you inspect a div where there is a name present in the database:
<span class="associate-name ng-binding">Andrew Elliott</span>
Here's what you see in the rendered HTML when you inspect a div where there is no value in the database:
<span class="associate-name ng-binding">unknown associate / automatic message</span>
See, it works :)
Example 2:
Show "no file selected" next to "choose file" button before a file is uploaded
The HTML:
<div>
<label class="btn btn-default" ng-file-select ng-model="files">Choose File</label><span title="{{ fileName }}"> {{fileName | onEmpty: 'no file selected' | truncate: 35 }}</span>
</div>
What the code is saying here is, "Try to insert 'fileName'. If no fileName is available, insert 'no file selected' and truncate that file name to 35 characters so it will fit well.
You can read more about truncation in the pro-tip, "Simple Truncation with Vanilla JS and Angular".
Unit Tests:
We wrote tests to account for 4 scenarios that might come up while using our onEmpty filter.
The first part of the test is just built it stuff, just how you do it. Don't worry too much about it.
describe('ruFilters', function(){
beforeEach(module('ruFilters'));
describe('Filter: onEmpty', function() {
var onEmpty;
beforeEach(inject(function(_onEmptyFilter_) {
onEmpty = _onEmptyFilter_;
}));
Then here are the It's...
it('should return n/a if value is empty', function() {
expect(onEmpty('')).to.equal('n/a');
});
it('should return n/a if value and str are empty', function() {
expect(onEmpty('', '')).to.equal('n/a');
});
These two It's say, "If we don't feed the function any info, just return 'n/a'. Basically, the only reasons I could see for us not declaring the string message that we want used are 1) because we forgot, 2) we're lazy, or 3) the designer hasn't given us a message yet and we're not creative.
it('should return a value if the value exists', function() {
expect(onEmpty('hello', '')).to.equal('hello');
});
This It says, "Whatever value you get, spit that value out. If you've got some data on the scope, give us that."
it('should return the argument if the variable is empty', function() {
expect(onEmpty('', 'nothing found')).to.equal('nothing found');
});
This It says, "If you've got nothing in the database, but you have a string message, give us that string message." This scenario is most important scenario, duh. It's the one we wrote the function for :)
So, there you have it - a super generic, reusable filter where you can display strings based on myriad visual expectations to your heart's content!