5

i'm trying to figure out how to save and react on multiple forms in multiple directives.

To give you a short overview: Screenshot of the current view

I've got three tabs containing forms and a fourth containing a JsTree (Groups). Each of the three tabs contains a directive, which, in turn, contains a Formly form. The tabs are wrapped by a main directive which contains a footer directive with the save and cancel buttons in the bottom right corner.

Main directive:

/**
 * Displays the ui for editing a specific user
 */
export function UserDetailsDirective() {
 class UserDetailsDirective {

  /*@ngInject*/
  constructor(
   $stateParams,
   userService,
   formlyChangeService
  ) {
   this.currentUser = this.currentUser || {};
   this.originalUser = this.originalUser || {};

   this.userForms = {
    mainData: {},
    personalData: {},
    basicSettings: {}
   };

   this.savingAllowed = true;
              
            /* Second try: Registering a callback at the change service, which will be executed on any field change in the passed form (mainData) */
            formlyChangeService.onFormChange('mainData', () => {
    console.log('test123');
    console.log('this123', this);

    console.log('this.userForms.mainData.api.isValid()', this.userForms.mainData.api.isValid());
   });


   if ($stateParams.id > 0) {
    userService.getUser($stateParams.id).then((userData) => {
     userData.Birthday = new Date(userData.Birthday);
     this.currentUser = userData;

     this.breadcrumbData = [...];
    })
   }
  }

  onSave(controller) {
   alert('on save');
   console.log('controller', controller);
  }
 }

 return {
  restrict: 'E',
  templateUrl: 'components/usermanagement/edit/user-details/user-details.directive.html',
  controller: UserDetailsDirective,
  controllerAs: 'controller',
  bindToController: true
 }
}
<breadcrumb [...]></breadcrumb>

<ul class="nav nav-tabs">
 <li class="active"><a data-toggle="tab" data-target="#mainData">Account data</a></li>
 <li><a data-toggle="tab" data-target="#personalData">Personal data</a></li>
 <li><a data-toggle="tab" data-target="#basicSettings">Settings</a></li>
 <li><a data-toggle="tab" data-target="#userGroupAssignment">Groups</a></li>
</ul>

<div class="row">
 <div class="col-lg-6">
  <div class="tab-content">
   <div id="mainData" class="tab-pane fade in active">
    <main-data user="controller.currentUser"></main-data>
   </div>
   <div id="personalData" class="tab-pane fade">
    <personal-data user="controller.currentUser"></personal-data>
   </div>
   <div id="basicSettings" class="tab-pane fade">
    <basic-settings user="controller.currentUser"></basic-settings>
   </div>
   <div id="userGroupAssignment" class="tab-pane fade">
    <group-assignment user="controller.currentUser"></group-assignment>
   </div>
  </div>
 </div>
 <div class="col-lg-6">
  [...] <!-- Right column -->
 </div>
</div>

<!-- Footer -->
<user-details-footer
 on-save="controller.onSave(controller)"
 saving-allowed="controller.savingAllowed"
></user-details-footer>

Footer directive

/**
 * Displays the user details footer
 */
export function UserDetailsFooterDirective() {
 class UserDetailsFooterDirective {

  /*@ngInject*/
  constructor(
   $state,
   Notification,
   $translate
  ) {
   this.state = $state;
   this.notification = Notification;
   this.translate = $translate;

   this.savingAllowed = this.savingAllowed || false;
  }

  /**
   * Event that is triggered on save button click
   *
   * Propagates to the parent controller via attribute binding
   */
  saveEvent() {
   if (typeof this.onSave === 'function') {
    this.onSave();
   }
  }

  /**
   * Navigates to the user list
   */
  goToUserList() {
   this.state.go('userList');
  }
 }

 return {
  restrict: 'E',
  templateUrl: 'components/usermanagement/edit/user-details-footer/user-details-footer.directive.html',
  controller: UserDetailsFooterDirective,
  controllerAs: 'controller',
  bindToController: true,
  scope: {
   onSave: '&?',
   savingAllowed: '=?'
  }
 }
}
<nav class="navbar navbar-fixed-bottom">
 <div class="container-fluid pull-right">
  <button class="btn btn-default" ng-click="controller.goToUserList()"><i class="fontIcon fontIconX"></i> Cancel</button>
  <button class="btn btn-primary" ng-disabled="controller.savingAllowed !== true" ng-click="controller.saveEvent()"><i class="fontIcon fontIconSave"></i> Save</button>
 </div>
</nav>

First tab's directive

/**
 * Displays the contents of the tab "Account data"
 */
export function MainDataDirective() {
 class MainDataDirective {

  /*@ngInject*/
  constructor(
   formlyFormService,
   mainDataFieldProviders,
   $state,
   userSubmitService,
   $timeout,
      formlyChangeService,
      $scope
  ) {
   this.state = $state;
   this.$timeout = $timeout;

   this.userSubmitService = userSubmitService;

   this.model = {};
   this.originalUser = this.originalUser || {};
   this.fields = [];

   this.form = null;
   var that = this;

            /* Third try: Watching the form instance => partial success */
   this.watch('formMainData', function(x, y, form) {
    console.log('formMainData', form);
    that.form = form;
    form.watch('$invalid', function(foo, bar, value) {
                    /* This will react on field changes but it seems really dirty to me */
     console.log('$invalid', arguments);
    });

   });


   formlyFormService.getFormConfiguration(mainDataFieldProviders).then((result) => {
    /* Here the formly fields are set */
                this.fields = result;
                /* Second try: A service which provides a callback that will be executed on field invalidation => no success */
    formlyChangeService.registerFields(this.fields, 'mainData');
   }, (error) => {
    console.error('getMainDataFields error:', error);
   });

   this.api = {
    isValid: angular.bind(this, this.isValid),
    submit: angular.bind(this, this.onSubmit)
   }
  }

        /* First try to get the validity of the fields => no success */
  isValid() {
   //return this.$timeout(() => {
    let isValid = true;

    this.fields.some((field) => {
     if (
      field.validation.errorExistsAndShouldBeVisible === true
      || field.validation.serverMessages.length > 0
     ) {
      isValid = false;
      return true;
     }
    });

    //return isValid;
   //}, 10);

            return isValid;
  }

  /**
   * Method triggered by the formSubmit event
         */
  onSubmit() {
   this.userSubmitService.submitUser(this.fields, this.model);
  }
    }

 return {
  restrict: 'E',
  templateUrl: 'components/usermanagement/edit/main-data/main-data.directive.html',
  controller: MainDataDirective,
  controllerAs: 'controller',
  bindToController: true,
  scope: {
   originalUser: '=user',
   api: '=?'
  },
  link: (scope) => {
   scope.$watch('controller.originalUser', (newValue) => {
    if (newValue.hasOwnProperty('ID')) {
     scope.controller.model = angular.copy(newValue);
    }
   });
  }
 }
}
<form name="controller.form" ng-submit="controller.onSubmit()" class="form-horizontal" novalidate>
 <formly-form form="controller.formMainData" model="controller.model" fields="controller.fields" ></formly-form>
</form>

Second try: FormlyChangeService => got change event fired but before validation => no success

export /*@ngInject*/ function FormlyChangeService() {
 let callbacks = [];

 return {
  triggerFormChangeEvent: triggerFormChangeEvent,
  registerFields: registerFields,
  onFormChange: onFormChange
 };

 function triggerFormChangeEvent(value, options) {
  callbacks.forEach((callback) => {
   if (
    typeof callback === 'function'
    && callback.formDirective === options.templateOptions.formDirective
   ) {
    callback();
   }
  });
 }

 function onFormChange(formDirective, callback) {
  callback.formDirective = formDirective;
  callbacks.push(callback);
 }

 function registerField(fieldConfig) {
  fieldConfig.templateOptions.changeEvents.push(
   triggerFormChangeEvent
  );
 }

 function registerFields(fieldConfigs, formDirective) {
  fieldConfigs.forEach((fieldConfig) => {
   fieldConfig.templateOptions.formDirective = formDirective;
   registerField(fieldConfig);

   fieldConfig.watcher = {
    listener: function() {
     console.log('listener', arguments);
    }
   };

   console.log('fieldConfig', fieldConfig);


   fieldConfig.watch('$valid', function() {
console.log('valid field', arguments);
   });




  });
 }

}

The Formly forms are fed with an user model, which is provided by the main directive.

I have to save all four tabs at the same time because there are several mandatory fields that have to be present to save the entered record. Now here comes the tricky part:

I want the save button to be disabled if the model hasn't changed or an error occurred at any field in any form. I also want to know which form the error comes from.

What i thought about is an event or watcher in the Formly field config or something similar.

I've tried the onChange event on the field config but it is fired right before the field validation runs, so i won't get the current error status of that field.

The error status has to be passed up to the main directive from where it should be passed down to the save button.

Can anyone help me getting the forms (or even better the respective fields) to tell the main directive that there is an invalid field?

It's really difficult to exemplify such a complex task, so if there is any obscurity please let me know.

Thank you very much in advance.

Julian

jhend
  • 51
  • 3

1 Answers1

0

I think you should have a service or factory that all your directive depend on that holds the data for all your forms.

This way you can set up a watch in your directive that will call whatever method on your shared service to validate / invalidate forms on your other tabs.

I hope this helps

Tonio
  • 4,082
  • 4
  • 35
  • 60