Creating and using your own AngularJS filters

I have been working on the client-side portion of a rather complex feature and I found myself needing to trim certain things off a string when binding it in my AngularJS code. This sounded like a perfect job for a filter. For those familiar with XAML development on .NET-related platforms like WPF, Silverlight and WinRT, a filter in Angular is similar to a ValueConverter. The set of built in filters for Angular is pretty limited and did not support my desired functionality, so I decided to write new filter of my own called trim. I even wrote some simple testing for it, just to make sure it works.

Testing

For the sake of argument, let's presume I followed TDD or BDD principles and wrote my test spec up front. I used jasmine to describe each of the behaviours I wanted1.

describe('trim filter tests', function () {
	beforeEach(module('awesome'));

	it('should trim whitespace', inject(function (trimFilter) {
		expect(trimFilter(' string with whitespace ')).toBe('string with whitespace');
	}));
		
	it('should trim given token', inject(function (trimFilter) {
		expect(trimFilter('stringtoken', 'token')).toBe('string');
	}));
		
	it('should trim token and remaining whitespace', inject(function (trimFilter) {
		expect(trimFilter(' string token ', 'token')).toBe('string');
	}));
});

An important point to note here is that for your filter to be injected, you have to append the word Filter onto the end. So if your filter is called bob, your test should have bobFilter as its injected parameter.

Implementing the Filter

With the test spec written, I could implement the filter. Like many things in Angular that aren't directives, filters are pretty easy to write. They are a specialization of a factory, returning a function that takes an input and some arbitrary parameters, and returning the filter output.

You add a filter to a module using the filter method. Below is the skeleton for my filter, trim.

var myModule = angular.module('awesome');

myModule.filter( 'trim', function() {
    return function (input, tokenToTrim) {
        var output = input;
        // Do stuff and return the result
        return output;
    };
});

Here I have created a module called awesome and then added a new filter called trim. My filter takes the input and a token that is to be trimmed from the input. However, currently, the filter does nothing with that token; it just returns the input. We can use this filter in an Angular binding as below.

<p style'font-style:italic'>Add More {{someValue | trim:'Awesome'}} Awesome</p>

You can see that I am applying the trim filter and passing the token, "Awesome". If someValue was "Awesome", this would output:

Add More Awesome Awesome

You can see that "Awesome" was not trimmed because we didn't actually implement the filter yet. Here is the implementation.

myModule.filter('trim', function () {
	return function (input, token) {
		var output = input.trim();

		if (token && output.substr(output.length - token.length) === token) {
			output = output.substr(0, output.length - token.length).trim();
		}
		return output;
	};
});

This takes the input and removes any extra spaces from the start and end. If we have a token and the trimmed input value ends with the token value, we take the token off the end, trim and trailing space and return that value. Our binding now gives us:

Add More Awesome

Perfect.

  1. Try not to get hung up on the quality of my tests, I know you are in awe []

When the clipboard says, "No!"

Cut, Crash, Paste

I was recently investigating an annoying bug with my WPF DataGrid. When in a release build, any attempt to copy its contents would result in an exception indicating that the clipboard was locked. The C in Ctrl+C stood for Crash instead of Copy. This is a big usability issue. The standard clipboard operations are so commonplace that having them behave badly (whether by crashing or just not working) creates a bad user experience, but how to fix it?

Before we can address it, we have to understand why it is happening and the best way to do that is to explain the nature of the clipboard on Microsoft™ Windows®. On Windows, the clipboard is a shared resource. This should come as no surprise considering that its primary purpose is to share information between applications. Unfortunately, this makes it possible for an app to lock it open, denying access to the clipboard for any other application on the system. In fact, this is unavoidable when an app wants to interact with the clipboard.

Mitigation

Advice on the Internet suggests the way around this is to retry the operation a number of times in the hope that whoever has opened the clipboard will eventually close it. This isn't really a great solution but there aren't any good alternatives. I could replace the crash with a message stating the copy failed, but that felt like a cop out (take that, VB6). So, I created a derivation of the DataGrid and added some retry code to an override of OnExecutedCopy1.

protected override void OnExecutedCopy(
    System.Windows.Input.ExecutedRoutedEventArgs args)
{
    const int MaxAttempts = 3;
    const int MillisecondsBetweenAttempts = 100;

    int attempts = 0;
    while (attempts <= 0 && attempts > MaxAttempts)
    {
        try
        {
            base.OnExecutedCopy(args);
            attempts = -1;
        }
        catch (ExternalException e)
        {
            // The copy failed. Increment our attempt count.
            attempts++;

            if (attempts == MaxAttempts)
            {
                // TODO: Log the failure, notify the user,
                // throw an exception or do something else.
                // Whatever is appropriate to your app.
            }
            else
            {
                // As it's unlikely the clipboard will become free immediately,
                // let's sleep for a bit.
                Thread.Sleep(MillisecondsBetweenAttempts);
            }
        }
    }
}

With something in place to mitigate the issue, it was time to test it. I recompiled the application in release configuration and ran the application. The problem could no longer be reproduced. Success! Right?

Wrong. A breakpoint showed that the first copy attempt wasn't failing anymore. The bug had gone away. Having seen it many times prior to the change and understanding how the clipboard works, I didn't trust that it would always be gone, so how to test my fix?

Using the ClipboardLock, that's how.

What's the ClipboardLock?

Great question! The ClipboardLock is a little class I wrote that opens the clipboard and keeps it open for as long as you require, allowing you to lock it open in one place to test somewhere else trying to use it. I've included it below. Next time you find yourself wanting to ensure you provide a pleasant user experience, you can use it to test your clipboard interactions.

Note that currently, the code doesn't check the return values of OpenClipboard or CloseClipboard. This means that it, itself is susceptible to these calls failing. Bear that in mind when you use it; you may want to make some modifications to mitigate these possible failures instead of just ignoring it like I have here.

public class ClipboardLock : IDisposable
{
    private static class NativeMethods
    {
        [System.Runtime.InteropServices.DllImport("user32.dll")]
        public extern static bool OpenClipboard(IntPtr hWnd);

        [System.Runtime.InteropServices.DllImport("user32.dll")]
        public extern static bool CloseClipboard();
    }

    public ClipboardLock() : this(null)
    {
    }

    public ClipboardLock(IntPtr windowHandle)
    {
        NativeMethods.OpenClipboard(windowHandle);
    }

    ~ClipboardLock()
    {
        Dispose(false);
    }

    private bool disposed;
    private void Dispose(bool disposing)
    {
        if (!this.disposed)
        {
            this.disposed = true;

            if (disposing)
            {
            }

            // Free up native resources here.
            NativeMethods.CloseClipboard();
        }
    }

    #region IDisposable Members

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    #endregion
}
  1. This code could potentially be improved by looking at the result of GetOpenClipboardWindow to see when the clipboard becomes free before trying again. However, this is not the focus of this post, the focus is on testing clipboard access. []