7
\$\begingroup\$

I'm wrapping ui-select in my own directives to pre-configure and style it, as I need it for my app in different select widgets. They appear on many pages, for example accountSelect, countrySelect, contactSelect and so on.

What I really dislike is, that I'm mostly copy-and-pasting the directive's code and then just changing the service. What I would normally do is to create some kind of abstract or base class and inherit from that but classes and inheritance are a "problem" in Javascript for me. By the way, I'm aware the language is prototypical. So far I couldn't find a better way to avoid the code duplication between the directives.

Any recommendation how to make this more DRY?

angular.module('crm.invoicing').directive('productsSelect', [function() {
    return {
        restrict: 'E',
        scope: {
            productId: '=',
            products: '=',
            multiple: '=?',
            onSelection: '=?',
            onRemoval: '=?',
            selectOptions: '=?',
            list: '=?'
        },
        transclude: true,
        templateUrl: '/psa/invoicing/templates/products-select.html',
        controller: function($scope, ProductsService) {
            $scope.multiple = angular.isDefined($scope.multiple) ? $scope.multiple : false;
            if (!angular.isDefined($scope.selectOptions)) {
                $scope.selectOptions = {};
            };

            var data = {
                productId: $scope.productId,
                products: $scope.products,
                items: null,
                selected: $scope.products
            };

            if ($scope.multiple === false) {
                data.items = [data.selected]
            } else {
                data.selected = $scope.products;
                data.items = []
            }

            $scope.productsList = {
                items: data.items,
                selected: data.selected,
                select: function ($item, $model) {
                    $scope.productsList.selectedItem = $item;
                    if ($scope.multiple === false) {
                        if ($item !== undefined) {
                            $scope.productId = $item.id;
                        }
                    } else {
                        for (var i = 0; i < $scope.data.products.length; i++) {
                            if ($scope.data.products[i].id == $item.id) {
                                return;
                            }
                        }
                        $scope.data.products.push($item);
                    }
                    if ($scope.onSelection) {
                        $scope.onSelection($item, $model, $scope.productsList);
                    }
                },
                remove: function($item, $model) {
                    if ($scope.multiple === false) {
                    } else {
                        for (var i = 0; i < $scope.data.products.length; i++) {
                            if ($scope.data.products[i].id == $item.id) {
                                $scope.data.products.splice(i, 1);
                            }
                        }
                    }
                    if ($scope.onRemoval) {
                        $scope.onRemoval($item, $model, $scope.productsList);
                    }
                },
                search: function(term) {
                    if (term.length > 2) {
                        ProductsService.lookup(term).then(function(result) {
                            if ($scope.multiple === false) {

                            } else {
                                for (var i = 0; i < $scope.data.products.length; i++) {
                                    for (var i2 = 0; i2 < result.data.length; i2++) {
                                        if (result.data[i2].id == $scope.data.products[i].id) {
                                            result.data.splice(i2, 1);
                                        }
                                    }
                                }
                            }
                            $scope.productsList.items = result.data;
                        });
                    }
                }
            };
            angular.extend($scope.productsList, $scope.selectOptions);
            if (angular.isDefined($scope.list)) {
                $scope.list = $scope.productsList;
            }
        }
    };
}]);
\$\endgroup\$
  • \$\begingroup\$ "What I really dislike is, that I'm mostly copy and pasting the directive's code and then just changing the service." - how are you doing this exactly? We're missing a big part of the problem. \$\endgroup\$ – Joseph Sep 3 '15 at 1:31
  • \$\begingroup\$ @JosephtheDreamer if you look at the code you'll see only one place where ProductsService.lookup(term) is called and the service is injected into the controller of the directive. This and the template is the only difference between all my select directives and I hate myself for duplicating all of that code for reach of my different select directives. Does this make it more clear what the problem is? \$\endgroup\$ – burzum Sep 3 '15 at 7:04
5
\$\begingroup\$

Anything that makes each instance of a directive different should be delegated somewhere higher up. You make a "wrapper" directive. Generic logic stays down the hierarchy, while specific logic wraps and provides for it.

Assuming each of your directive has a hierarchy of something like:

page -> your three directives -> ui-select

I suggest you actually create another directive that sits between your three different directives, and the ui-select. This directive contains your generic crud logic you wrote above but it does not have any special UI (it merely wraps ui-select) and knows nothing about services. It only knows that it is provided search results via $scope and is passed in functions that it can call when stuff happens.

            .--- calls handler when something happens ---.
            V                                            |
page -> your directives -(data via $scope)-> generic select -> ui-select

Now for the specific directives. You wrap the generic select with the custom template for each then supply the data via $scope. As for knowing when to search, the generic level should be passed a handler from the specific directives that it can call when search is done. Specific directive does lookup on service, updates data on $scope, done.

In the end, it should look like:

Generic select - Wrapping your ui-select. All your logic stays on this directive. This is also where your logic interfaces with ui-select.

<div class="generic-select">
  <ui-select ... ></ui-select>
</div>

Your 3 different selects - Each of them have different templates and services. When generic-select does a search, it should call a function that's provided by the three (in this case, each will have itemSearch). In that function, it calls the specific service, updating $scope which should then be transparent to the generic-select.

<div class="product-select>
  <generic-select onSearch="itemSearch"></generic-select>
</div>

<div class="account-select>
  <generic-select onSearch="itemSearch"></generic-select>
</div>

<div class="country-select>
  <generic-select onSearch="itemSearch"></generic-select>
</div>
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.