Creating a simple predicate builder with AngularJS

This is an old post!

This post is over 2 years old. Solutions referenced in this article may no longer be valid. Please consider this when utilizing any information referenced here.

So I’ve been working on a project recently where I needed a simple predicate builder. Basically I needed a way to allow users to build a somewhat complex search using a GUI. And since we are using AngularJS on this project, here’s a quick article about how I did it.

Start with a simple AngularJS setup.

angular
    .module('predicate', []);

angular
    .module('predicate')
    .factory('Condition', function() {
        function Condition(data) {
            if (data) {
                this.setData(data);
            }
        }

        Condition.prototype.setData = function(data) {
            angular.extend(this, data);
        };

        Condition.prototype.searchKey = "";
        Condition.prototype.searchValue = "";

        return Condition;
    });

angular
    .module('predicate')
    .controller('predicate', function ($scope, Condition) {
        var defaultSearch = {"searchKey": "id", "searchValue": ""};
        $scope.conditions = [new Condition(defaultSearch)];
    });

Okay, so what we’ve done so far is stub out a simple AngularJS app, with a factory that builds a Condition object. This will be the model object that we use to hold data from our predicates. We also add a starting predicate so that there’s at least one condition to search by when the user opens the page.

Now, let’s add some HTML:

<body ng-app="predicate">
    <div ng-controller="predicate">
        <form ng-submit="doSearch()">
            <div ng-repeat="condition in conditions">
                <select ng-model="condition.searchKey">
                    <option value="id">ID</option>
                    <option value="name">Name</option>
                    <option value="email">Email</option>
                </select>
                <input type="text" ng-model="condition.searchValue">
                <button type="button" ng-show="$last" ng-click="addCondition()">+</button>
                <button type="button" ng-show="conditions.length > 1" ng-click="deleteCondition(condition)">-</button>
            </div>
            <button type="button" ng-click="doSearch()">Search</button>
        </form>
    </div>
</body>

So we’ve created a simple HTML stub for an AngularJS app. A couple of notes about what we’re doing here. We only show the delete button if we have more than one condition because we don’t want to get into a situation where we have no conditions. We also only show the add button on the last condition, using the magic variable $last, which is available inside the ng-repeat loop.

So if you fire this up, you should see a simple predicate already in place, because you already have one Condition object in the conditions array.

So now we need to make it do something.

angular
    .module('predicate')
    .controller('predicate', function ($scope, Condition) {
        var defaultSearch = {"searchKey": "id", "searchValue": ""};
        $scope.conditions = [new Condition(defaultSearch)];

        $scope.addCondition = function() {
            $scope.conditions.push(new Condition(defaultSearch));
        }

        $scope.deleteCondition = function(condition) {
            $scope.conditions.splice($scope.conditions.indexOf(condition), 1);
        }
    });

Now, you should be able to add and remove conditions. Pretty neat! But how do we digest the objects we’re creating? Well, let’s do that now.

angular
    .module('predicate')
    .controller('predicate', function ($scope, Condition) {
        var defaultSearch = {"searchKey": "id", "searchValue": ""};
        $scope.conditions = [new Condition(defaultSearch)];

        $scope.addCondition = function() {
            $scope.conditions.push(new Condition(defaultSearch));
        }

        $scope.deleteCondition = function(condition) {
            $scope.conditions.splice($scope.conditions.indexOf(condition), 1);
        }

        $scope.doSearch = function() {
            var searches = {};
            $scope.conditions.forEach(function(val) {
                if (val.searchValue.length) {
                    searches[val.searchKey] = val.searchValue;
                }
            });

            if (Object.keys(searches).length == 0) {
                return;
            }

            console.log(searches);
        }
    });

So here, the new doSearch() method we added to this scope loops through the objects and builds a searches object. You can now use this object to query whatever backend resource you would like. I feed it into $httpParamSerializer to turn it into a query string for querying an API resource. Right now, we’re just printing that to the log using console.log() so you can see what it’s doing.

So there you have it! Here’s a CodePen that demonstrates the whole thing. Enjoy!

Comments (0)

Interested in why you can't leave comments on my blog? Read the article about why comments are uniquely terrible and need to die. If you are still interested in commenting on this article, feel free to reach out to me directly and/or share it on social media.

Contact Me
Share It
Javascript
AngularJS’s built-in ngResource is a great tool for natively supporting REST APIs in your Angular application. But what happens when you need to support something besides a simple call that retrieves a list of JSON objects? You quickly run into the limits of ngResource. Here’s a great case where you might need to do something more complex: paging. Say you want to get a list of objects, and there’s 10,000 or so of them. You don’t want to send 10,000 objects to your frontend app. You want to send a portion of them, but you still need to indicate to the app that there are more. Surprisingly, considering how widespread this pattern is in web development, there does not seem to be a native way to accomplish this. But you can extend ngResource. Here’s how I did it.
Read More
Release Announcements
petfeedd users, I am proud to announce the beta release of petfeedd 1.0.1. This release has no major changes in it and is solely about addressing security issues in many of the underlying libraries used by petfeedd. To install it or upgrade from previous versions, you can simply run: docker pull peckrob/petfeedd:latest
Read More
Release Announcements
After five beta releases and months of testing, I am happy to announce petfeedd Version 1.0 is now available. All changes from the beta branch have been merged in and the release is now available on Docker Hub. To install it or upgrade from Version 0.2, you can simply run: docker pull peckrob/petfeedd:latest And restart. It should perform all the upgrades needed for version 1.0.
Read More