1
\$\begingroup\$

I have a rather large UI for editing a user within a Angular application. It is split in multiple tabs and in order to reduce complexity the user cannot change the tab unless there is no validation failure on that particular tab.

However, some validation is asynchronously performed on server side. The validation flow can be the following:

  1. edit some data on tab 1
  2. go next to tab 2 and the async validation kicks in (on tab change)
  3. if it fails user selected tab is reverted to tab 1
  4. however, in some rare cases tab 2 validation also fails, so if this is not handled correctly I can get into an infinite loop

user-edit.component.ts (holds the container component for all tabs)

private stepperTabChanging = false;

private revertToTabIndex(previouslySelectedIndex: number) {
    // this is required to allow Angular change detection another cycle 
    // to process selected index change
    setTimeout(() => {
      this.stepper.selectedIndex = previouslySelectedIndex;

      // delaying cancel of tab changing to avoid infinite save error tab changes
      setTimeout(() => {
        this.stepperTabChanging = false;
      }, 200);

    });
}

stepperTabChanged(event: StepperSelectionEvent) {
    console.log("Tab changed with event: ", event);
    if (this.stepperTabChanging)
      return;

    this.stepperTabChanging = true;

    // check if current tab can be changed
    // sync validations are handled here
    if (!this.currentStepValid(event.previouslySelectedIndex)) {
      const msg = "Please fix tab validation errors before going to next step";
      this.snackBarService.open(msg);
      this.notificationService.addNotification(msg);
      this.revertToTabIndex(event.previouslySelectedIndex);
      return;
    }

    this.userManagementStoreService.tabChanged(event.selectedIndex, event.previouslySelectedIndex, 
      () => this.revertToTabIndex(event.previouslySelectedIndex));
    this.stepperTabChanging = false;
}

user-management-store-service.ts (this holds all user related state and some common functionality)

private trySaveGeneralInfo(currentIndex: number, onError: () => void) {
    const loadDataModel: GenericDataLoadModel = {
      loaderMessage: "Saving general information ...",
      okNotificationMessage: "General information successfully saved",
      errorNotificationMessage: "Error saving general information "
    };

    const tdInfo = this.auth.getTdDbIdentity();
    const savePayload: UserGeneralInfoSaveModel = {
        requestMetadata: {
          databaseApplicationUserId: this.databaseApplicationUserId,
          reqDatabaseApplicationUserId: tdInfo.databaseApplicationUserId,
          environmentExtensionId: tdInfo.environmentExtensionId
        },
          environmentId: tdInfo.environmentId,
          userTypeCodeId: +this.userEditModel.userType.key,
        emailAddress: this.userEditModel.email,
        userName: this.userEditModel.userName,
        userPrincipalName: this.userEditModel.userPrincipalName
      };

     this.basicOps.genericDataSaveByUrl(loadDataModel, "UserManagement/SaveGeneralInfo",
      savePayload, (result: IValidationResult<UserEditModel>) => {
        this.userEditModel = result.payload;

      }, error => {
        console.log("Save general info error: ", error);
        if (onError) {
          onError();
        }
      });
} 

private trySaveSecurityInfo(currentIndex: number, onError: () => void) {
    const loadDataModel: GenericDataLoadModel = {
      loaderMessage: "Saving security information ...",
      okNotificationMessage: "Security information successfully saved",
      errorNotificationMessage: "Error saving security information "
    };

    const tdInfo = this.auth.getTdDbIdentity();
    const savePayload: UserEditSecurityInfoModel = {
        requestMetadata: {
          databaseApplicationUserId: this.databaseApplicationUserId,
          reqDatabaseApplicationUserId: tdInfo.databaseApplicationUserId,
          environmentExtensionId: tdInfo.environmentExtensionId
        },

        securityUserServiceId: +this.userEditModel.userService.key,
        securityUserRoleId: +this.userEditModel.userRole.key,
        databaseUserGroupCode: this.userEditModel.userGroup.value,
        userGroupDefaultDatabaseId: +this.userEditModel.defaultDatabase.key
    };

    this.basicOps.genericDataSaveByUrl(loadDataModel, "UserManagement/SaveSecurityInfo", savePayload, (_) => {

    this.setSecurityInformationInitialValues();
    this.tabChangedSource.next(currentIndex);

  }, error => {
    console.log("Save security info error: ", error);
    if (onError) {
      onError();
    }
  });
} 

tabChanged(currentIndex: number, previousIndex: number, onError: () => void) {
    console.log("Tab changed to index: ", currentIndex, " with previous index = ", previousIndex);
    this.trySaveCurrentInfo(currentIndex, previousIndex, onError);
}

private trySaveCurrentInfo(currentIndex: number, prevIndex: number, onError: () => void): any {

    // this is the first tab
    if (prevIndex === UserManagementStoreService.GeneralInfoIndex && this.generalInfoIsDirty) {
      this.trySaveGeneralInfo(currentIndex, onError);
      return;
    }

    // this is the second tab
    if (prevIndex === UserManagementStoreService.SecurityInfoIndex && this.securityInfoIsDirty) {
      this.trySaveSecurityInfo(currentIndex, onError);

      return;
    }
}

So, when tab is changed the upper component goes to service which in turn tries to save current tab information. When this fails, it calls onError callback which basically reverts the tab.

There are two aspects I particularly dislike about the code above, but any other suggestions are welcomed:

  1. onError callback is sent through quite many functions
  2. setTimeout nesting looks more like a hack than a real solution
\$\endgroup\$

Your Answer

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

Browse other questions tagged or ask your own question.