Angular.js: $submitted is not set on sub ng-form

Created on 15 Nov 2014  路  20Comments  路  Source: angular/angular.js

Hi,

I have <form> with inner <ng-form>
when form is submitted inner <ng-form>.$submitted is not set to true only parent

forms low broken expected use bug

Most helpful comment

:+1: I would like to see that too

My rl scenario is that I have a form where I have directive that uses ng-form and inside it I also have another directive with some input fields. That second (most inner) directive requires form controller in link function and it is the form controller from the ng-form directive. I have validation messages showing on submitted form but the only form that gets $submitted flag is the most outer form. The ng-form controller has $submitted set to false and the validation messages are not shown.

All 20 comments

I don't think we should expect the inner ngForm to behave exactly like the <form> thing. I guess we could fix this one but would be cool to know what is your real-life use-case?

Hi

I create wizard interface,
there are two directives, which I use to show errors after form is submitted

        .directive('bkErrorText', function () {
            return {
                require: '^form',
                template: '<span></span>',
                restrict: 'E',
                link: function ($scope, $element, $attrs, ctrl) {
                    var inputCtrl = ctrl[$attrs.bkInputName];
                    if (!inputCtrl) {
                        throw 'Can\'t find input ' + $attrs.bkInputName + ' into form.';
                    }

                    function showErrorText(newValue, oldValue) {
                        if (inputCtrl.$invalid && (ctrl.$submitted || ctrl.$$parentForm.$submitted)) {
                            var i, key, e, etext = [], error = inputCtrl.$error;
                            for (i in error) {
                                if (error[i]) {
                                    key = 'bkMsg' + angular.uppercase(i.charAt(0)) + i.substr(1);
                                    e = $attrs[key] || (key + ' no message text!');
                                    etext.push(e);
                                    etext.push('&nbsp;');
                                }
                            }
                            $element.html(etext.join(''));
                            $element.removeClass('hidden');
                        }
                        else {
                            if (!$element.hasClass('hidden')) {
                                $element.addClass('hidden');
                                $element.html('');
                            }
                        }
                    };

                    $scope.$watchCollection(function () { return inputCtrl.$error; }, showErrorText);
                    $scope.$watch(function () {
                        return inputCtrl.$invalid && (ctrl.$submitted || ctrl.$$parentForm.$submitted);
                    }, showErrorText);
                }
            };
        })
        .directive('bkHasError', function () {
            return {
                require: '^form',
                restrict: 'A',
                scope: {
                    bkHasError: '@',
                    bkHasErrorClass: '@'
                },
                link: function ($scope, $element, $attrs, ctrl) {
                    var cssClass = $scope.bkHasErrorClass || 'has-error',
                        inputCtrl = ctrl[$scope.bkHasError];

                    $scope.$watch(function () {
                        return inputCtrl.$invalid && (ctrl.$submitted || ctrl.$$parentForm.$submitted);
                    }, function (newValue, oldValue) {
                        if (newValue) {
                            $element.addClass(cssClass);
                        }
                        else {
                            $element.removeClass(cssClass);
                        }
                    });
                }
            };
        })
<form class="form-horizontal" role="form" novalidate="novalidate" ng-submit="submitForm()">
    <tabset>
        <tab select="tabs['common']=true" deselect="tabs['common']=false" active="tabs['common']">
            <tab-heading ng-class="{'text-danger':tabsValid['common']===false}">
                袨斜褖懈
            </tab-heading>
            <ng-form name="common" bk-submit-valid="tabsValid['common']">
                <div class="form-group" bk-has-error="amountBuy">
                    <label class="col-md-2 control-label" for="amountBuy">amountBuy</label>
                    <div class="col-md-3">
                        <input class="form-control bk-number-lg" id="amountBuy" name="amountBuy" type="text"
                               ng-model="model.amountBuy"
                               bk-number="15,2"
                               bk-number-range="0.01,999999.99"
                               ng-model-options="{ debounce: 300 }" />
                        <bk-error-text class="help-block"
                                       bk-input-name="amountBuy"
                                       bk-msg-required="Data is required ..."
                                       bk-msg-number="Number(15,2) ..."
                                       bk-msg-number-range="Number range from 0.01 to 999 999.99 ..." />
                    </div>
                </div>
                <!--
                    others input fields
                -->
            </ng-form>
        </tab>
        <!--
            others tabs
        -->
    </tabset>
    <div class="col-md-offset-2 col-md-6">
        <button class="btn btn-default" type="button" ng-click="prevTab()">Prev</button>
        <button class="btn btn-default" type="button" ng-click="nextTab()">Next</button>
        <button class="btn btn-primary" type="submit" bk-disable-pristine>Save</button>
    </div>
</form>

Another approach is using css:

.ng-submitted .ng-invalid {
.... styles styles styles ...
}

or looping through all parent forms:

while (form) form = form.$$parentForm;

:+1: I would like to see that too

My rl scenario is that I have a form where I have directive that uses ng-form and inside it I also have another directive with some input fields. That second (most inner) directive requires form controller in link function and it is the form controller from the ng-form directive. I have validation messages showing on submitted form but the only form that gets $submitted flag is the most outer form. The ng-form controller has $submitted set to false and the validation messages are not shown.

@kwypchlo does the css I posted above solve your issue?

real life use-case:

some form:

<form ng-form="form1" ng-submit="save(form1)">
  <custom-fieldset model="model.firstname" required></custom-fieldset>
  <custom-fieldset model="model.lastname" required></custom-fieldset>
  <button>save</button>
</form>

custom-fieldset.html:

<fieldset ng-form="form">
  <input name="name" required ng-model="ctrl.model">
  <ng-messages role="alert"
    for="(form.$submitted || form.name.$touched) &amp;&amp; form.name.$error">
    <ng-message when="required">please enter a name</ng-message>
  </ng-messages>
</fieldset>

@stryju This is similar to the use case I had.

@awerlang nope, I need this functionality in js, inside my directive. The workaround with $$parentForm may work but sure doesn't look elegant :)

:+1: for including this.

I have a setup similar to @stryju 's above. I added a watch to the child forms for now to watch the parent form's $submitted property, but It does feel hacky.

I ran into this issue as well. I have a subform that I reuse in many forms, there it is excluded to refer to parent form name to know if it was submitted or not.

For example, when I submit my user form, obviouly my subform for user address is submitted as well. However, userForm.$submitted becomes true, but userForm.addressForm.$submitted stays to false.

user-form.html:

<form name="userForm" novalidate>
    <div ng-class="{ 'has-error' : userForm.$submitted &amp;&amp; userForm.firstName.$invalid}">
        <label>First name</label>
        <input type="text" ng-model="user.firstName" name="firstName" required>
    </div>
    <div ng-class="{ 'has-error' : userForm.$submitted &amp;&amp;  userForm.lastName.$invalid}">
        <label>Last name</label>
        <input type="text" ng-model="user.lastName" name="lastName" required>
    </div>
    <fieldset ng-form="addressForm" class="address" ng-include="'address/address-fields.html'"></fieldset>
</form>

address-fields.html:

<div ng-class="{ 'has-error' : addressForm.$submitted &amp;&amp;  addressForm.postcode.$invalid}">
    <label>Postcode</label>
    <input type="text" ng-model="address.postcode" ng-required="true" name="postcode">
</div>
<div ng-class="{ 'has-error' : addressForm.$submitted &amp;&amp;  addressForm.city.$invalid}">
    <label>City</label>
    <input type="text" ng-model="address.city" ng-required="true" name="city">
</div>
<div ng-class="{ 'has-error' : addressForm.$submitted &amp;&amp;  addressForm.country.$invalid}">
    <label>Country</label>
    <input type="text" ng-model="address.country" ng-required="true" name="country">
</div>

+1

Yes please!

quick workaround I've found somewhere and modified:

// sets all children ng-forms submitted (no such default functionality)
function setSubmitted(form) {
    form.$setSubmitted();
    angular.forEach(form, function(item) {
        if(item && item.$$parentForm === form && item.$setSubmitted) {
            setSubmitted(item);
        }
    });
}

so ie. instead of scope.form.$setSubmitted(); use:

setSubmitted(scope.form);

A workaround: http://stackoverflow.com/a/30138961/2075423

Note this only works if formCtrl.$submitted is changed. So it might not work with dynamic nested ng-form.

I have thesame issue. I made the following workaround based on awerlang's suggestion...

In my directive.

ctrl.isSubmitted = function() {
    var form = ctrl.form;
    while (!!form) {
        if (form.$submitted) return true;
        form = form.$$parentForm;
    }
    return false;
};

In the view...

<div class="has-feedback" ng-class="{'has-error': ctrl.isSubmitted() && ctrl.form[ctrl.name].$invalid}">
    ...
</div>

I do agree that this feels a bit hacky though, but it works...
The css rules would work as well I think, but the has-error class is defined in bootstrap, and I don't really want to duplicate the css from bootstrap for a custom css rule. That can get messy when migrating to newer bootstrap versions.

will this issue be resolved before Trump takes office?

nope...

"_bump_"

while (form) form = form.$$parentForm;

FYI: This doesn't actually work -- the root FormController has a "noop form" set as its parent; different looping and/or conditional are required.
(e.g. while (form.$$parentForm.hasOwnProperty('$submitted')) form = form.$$parentForm or such).

Note, Angular doesn't expose a static binding for FormController (or any of the types it claims to in the documentation; NgModelController included), otherwise one could use types to check the $$parentForm for real-ness.

Another workaround I used was a watch:

$scope.$watch('form.$$parentForm.$submitted', function (submitted) {
    if (submitted) {
        $scope.form.$setSubmitted(true);
    }
});

My team got into a situation where we really needed the change from https://github.com/angular/angular.js/pull/15778, so what we did was to create a copy of our installed angular.js (say, angular-mod.js), applied the change from the PR and used npm's _postinstall_ to overwrite the original angular.js file in node_modules (a simple cp angular-mod.js node_modules/angular/angular.js).

Worked like a charm.

Was this page helpful?
0 / 5 - 0 ratings