Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

AngularJS - Modular forms with directives

I originally asked this question here, but I think I got ahead of myself and made it more complicated than it really is, so I'm re-asking it here with a bit clearer wording.

How do you create re-usable form widgets with directives and re-useable parameters? Like this:

<form>
<special-input label-text="A Special Input" bind-to="data.special"></special-input>
<special-input label-text="Specialer" bind-to="data.moreSpecial"></special-input>
</form>

Directive templates don't seem to allow interpolation on ng-model.

Furthermore, can you modularize and parameterize form behavior so that you can have standard POST actions, for example?

I've answer the question below based on my experimentation, but I'll hold off from accepting it for a while, since I am very new to Angular and would like to hear from others.

like image 523
hgcrpd Avatar asked Jul 08 '13 14:07

hgcrpd


Video Answer


1 Answers

Angular comes out of the box with an improve tag that is documented here. Basically it creates a scope in the form of a controller around the form and all of the tags within it. So you do this:

<body ng-app="TestApp">
<form ng-controller="FormCtrl" name="testForm">
    <input name="firstInput" ng-model="data.first">
    <input name="secondInput" ng-model="data.second">
    <button ng-click="submit()">Submit</button>
</form>
</body>

JS:

var app = angular.app('TestApp', []);
app.controller('FormCtrl', function($scope) {
    $scope.submit = function() {
        // Form submit logic here
        console.log("Submitting the form");
        console.log($scope);
    }
})

This creates a scope for the form, since the form tag contains the ng-controller tag. Within the scope, testForm is the javascript object for the form, and testForm.firstInput is the javascript object for the first input. It looks like these objects also have some validation functionality available, see docs here.

The data on the form will be available as an object data in the FormCtrl scope, with keys "first" and "second", and you can define methods in the controller that work on that.

You can also put multiple forms using the same FormCtrl, and it seems like Angular will create new instances for each form, so you don't have to worry about forms polluting each other's data.

Using directives

Now lets suppose that we have some sort of complex input or widget that is implemented in a directive. This example uses two select boxes to display all cities in a state. You have to first select a state, then it'll query for the cities in that state and populate the second select box.

app.directive('citySelect', function() {
    return {
        replace: true,
        template: '<div><select ng-change="getCities()" ng-options="s.name for s in states"></select>' +
                  '<select ng-model="data.selectedCity" ng-options="c.name for c in cities"></select>',
        controller: function($scope) {
            // Omitting the logic for getCities(), but it'd go here
        }
    };
})

Then you can just stick it into the form tag, and it'll work. Because the directive doesn't define a scope, it'll just attach to the scope of the FormCtrl.

<body ng-app="TestApp">
<form ng-controller="FormCtrl" name="testForm">
    <input name="firstInput" ng-model="data.first">
    <input name="secondInput" ng-model="data.second">
    <div city-select></div>
    <button ng-click="submit()">Submit</button>
</form>
</body>

Paramaterizing the directives

EDIT: So apparently this does work:

scope: {someParameter: "="},
template: '<div><select ng-model="someParameter"></select></div>'

You simply do it without the curlies, and it'll bind. My guess is that the parent scope is binding to someParameter in the child scope, and the select is then binding to somParameter in the child scope.

So all of this below about manually compiling in the link function is not necessary.

=====

But the problem with this is that my citySelect directive has a hard coded ng-model binding, so if I created some sort of generic widget, I couldn't use more than one of it in a form. Unfortunately this does not seem to work:

scope: {someParameter: "="},
template: '<div><select ng-model="{{ someParameter }}"></select></div>'

The only way that I have gotten this to work is to build the DOM element manually in a linking function, but I'm not sure if this is advisable. I would appreciate comments from anyone about this implementation:

<body ng-app="TestApp">
<form ng-controller="FormCtrl" name="testForm">
    <input name="firstInput" ng-model="data.first">
    <input name="secondInput" ng-model="data.second">
    <div city-select bind-to="data.homeCity"></div>
    <div city-select bind-to="data.workCity"></div>
    <button ng-click="submit()">Submit</button>
</form>
</body>

app.directive('citySelect', function($compile) {
    return {
        replace: true,
        template: '<div></div>',
        controller: function($scope) {
            // Omitting the logic for getCities(), but it'd go here
        }
        link: function(scope, iElem, iAttrs) {
            var html = '<div><select ng-bind="' + iAttrs['bindTo'] + '"></div>';
            iElem.replaceWith($compile(html)(scope));
        }
    };
})

Mixing in parameters on the form

Since separate instances of FormCtrl are created for each form, you can reuse a lot of the functionality in FormCtrl. But you can also use additional directives on a form tag to add parameters or break apart functionality. For example:

<form ng-controller="FormCtrl" name="testForm" post-form post-path="/path/to/resource/">

app.directive('postForm', function() {
    return {
        controller: function($scope) {
            $scope.post = function() {
                // Some generic POST behavior
            };
        },
        link: function(scope, iElem, iAttr) {
            scope.postPath = iAttr['postPath'];
        },
    };
});

The form's scope will then combine the scope from FormCtrl and postForm, so that everything is accessible. In my experimentation, it seems like the FormCtrl takes precedence, so if something like $scope.submit() is defined in both FormCtrl and postForm, FormCtrl's will take precedence (I think), maybe this is a race condition from asynchronous loading, I don't know.

Instead of using ng-controller, I think you can also use scope:true on the mixin directive (postForm), or perhaps more safely, scope: {}.

like image 186
hgcrpd Avatar answered Sep 21 '22 07:09

hgcrpd