1

I have an angularJS application. I have implemented a generic workflow using $routeProvider, templateUrl & Controllers.

Each step(screen) is verified when user click on next button and moves to the next step if validation passes. If validation fails user is required to fix all the error, displayed on the screen, before moving to next step.

When user has visited all the screens(passed validation for each screen) all the breadcrumbs get enabled and now user can move freely between those steps/breadcrumbs.

Requirement:

Now I want to allow user to move freely between steps by clicking on the breadcrumbs and when user clicks on the lodge button, on the last step, validation for current as well as for all previous steps should be invoked, and clicking on the error user should be able taken to the relevant step/screen.

Also I want to keep the functionality of validating the individual steps on the click of next button.

As you can see each screen has a separate controller along with the scope. Once user move from one step to another it can't access the previous scope.

Initially I thought of storing scope of each screen in an array, but once I move between steps new scope is created (as it should) and only current step has a form with valid data model and "valid" flag as false.

Form object at current step

Form object at current step

Form object of other screen without and fields attached

Form object of other screen without and fields attached

I'm not very well versed with Angularjs and trying to get some idea weather

  1. Is it possible what I'm trying to achieve keeping the existing functionality intact. (My understanding is that I can't have a single controller since I need to keep the functionality of validating each step individually)?

  2. Is there a better way to trying to achieve this functionality?

PS: Sadly I can't upgrade to newer version of Angular.

Any help will be highly appreciated.

Form Validation

validator.validate = function($scope, submitCallback, additionalOptions) {
            var options = {};
            $.extend(options, commonOptions, additionalOptions);
            var form = $scope[options.formName];
            hideErrorMessages(options.errorContainerSelectorId);
            if (form.$valid) {
                submitCallback();
                return;
            }

            showErrorMessages({message: composeAngularValidationErrors(form),
                errorContainer: $('#' + options.errorContainerSelectorId)});
        };

View:

<#assign temp=JspTaglibs["http://www.test.com/tags"]>
<div ng-controller="LodgeApplicationWorkflowController" ng-cloak>
    <workflow-breadcrumbs></workflow-breadcrumbs>
    {{model | json}}
    <div ng-view>
    </div>

    <script type="text/ng-template" id="applicant-details">

        <form name="form" method="post" action="#" class="standard">

            <h3>Primary contact details</h3>
            <div class="row">
                <div class="span3">
                    <label for="owner-primary-contact">Primary contact:</label>
                    <select class="span3" id="owner-primary-contact" required name="applicantDetails.primaryContact"
                            ng-model="model.applicantDetails.primaryContactId"
                            ng-options="user.id as user.fullName for user in refData.contactInfos"
                            >
                        <option value=""></option>
                    </select>
                </div>
                <div class="span3">
                    <label for="owner-primary-contact-phone">Phone:</label>
                    <input type="text" class="span3" id="owner-primary-contact-phone" name="applicantDetails.primaryContactPhone"
                           readonly
                           ng-model="model.applicantDetails.primaryContactPhone"/>
                </div>
                <div class="span3">
                    <label for="owner-primary-contact-email">Email:</label>
                    <input type="text" class="span3" id="owner-primary-contact-email" name="applicantDetails.primaryContactEmail"
                           readonly
                           ng-model="model.applicantDetails.primaryContactEmail"/>
                </div>
            </div>

        </form>
    </script>

    <script type="text/ng-template" id="lgc-methodology">

        <form name="form" method="post" action="#" class="standard">
            <h3>Describe the Your Methodology</h3>
            <div class="row">
                <div class="span9">
                    <label for="methodology">Describe the methodology which you propose to employ:
                    </label>
                    <textarea class="span9" id="methodology" name="methodology"
                              rows="10"
                              ng-maxlength="4000"
                              ng-model="model.methodology" required>
                </textarea>
                </div>
            </div>
        </form>
    </script>

    <script type="text/ng-template" id="approval-details">

        <form name="form" method="post" action="#" class="standard">

            <div class="row" ng-if="model.approvalDetails.planningApprovalsObtained === 'true'">
                <div class="span9">
                    <label for="planning-approvals-details">Approval details:</label>
                    <textarea class="span6" id="planning-approvals-details"
                              name="approvalDetails.planningApprovalDetails"
                              ng-if="model.approvalDetails.planningApprovalsObtained === 'true'"
                              required ng-maxlength="4000"
                              ng-model="model.approvalDetails.planningApprovalDetails"></textarea>
                </div>
            </div>
            <div class="row" ng-if="model.approvalDetails.planningApprovalsObtained === 'false'">
                <div class="span9">
                    <label for="planning-approvals-details">Reasons:</label>
                    <textarea class="span6" id="planning-approvals-details"
                              name="approvalDetails.planningApprovalDetails"
                              ng-if="model.approvalDetails.planningApprovalsObtained === 'false'"
                              required ng-maxlength="4000"
                              ng-model="model.approvalDetails.planningApprovalDetails"></textarea>
                </div>
            </div>

            <div >

                <div class="row" >
                    <div class="span9">
                        <label for="environment-approval-details">Approval details:</label>
                        <textarea class="span6" id="environment-approval-details"
                                  name="approvalDetails.environmentApprovalDetails"
                                  ng-maxlength="4000"
                                  ng-required="model.approvalDetails.environmentApprovalsObtained === 'true'"
                                  ng-model="model.approvalDetails.environmentApprovalDetails"></textarea>
                    </div>
                </div>
                <div class="row" ng-if="model.approvalDetails.environmentApprovalsObtained === 'false'">
                    <div class="span9">
                        <label for="environment-approval-details">Reasons:</label>
                        <textarea class="span6" id="environment-approval-details"
                                  name="approvalDetails.environmentApprovalDetails"
                                  ng-maxlength="4000"
                                  ng-required="model.approvalDetails.environmentApprovalsObtained === 'false'"
                                  ng-model="model.approvalDetails.environmentApprovalDetails"></textarea>
                    </div>
                </div>

            </div>
        </form>
    </script>

    <script type="text/ng-template" id="confirmation">
        <form id="form" method="post" name="form" action="#" class="standard">


            <div class="row">
                <div class="span9">
                <span class="checkbox inline">
                    <label for="confirm-information">
                        <input type="checkbox" id="confirm-information" name="confirmInformation"
                               ng-model="model.Confirmed" required />
                        I confirm that all the details are correct
                    </label>
                </span>

                </div>
            </div>
        </form>
    </script>


    <div class="form-actions">
        <input type="button" class="btn" value="Cancel" ng-click="cancel()"/>

        <div class="pull-right btn-toolbar">
            <input id="previous" type="button" class="btn" value="Previous"
                   ng-click="workflow.handlePrevious()" ng-show="!workflow.isFirstStep()" ng-cloak/>

            <input id="save-and-close" type="button" class="btn" value="Save draft and close"
                   ng-show="model.canSaveDraftAndClose && !workflow.isLastStep()"
                   ng-click="saveDraftAndClose()" ng-cloak/>

            <input id="submit" type="button" class="btn btn-primary" value="{{workflow.getNextLabel()}}"
                   ng-disabled="!workflow.canNavigateToNextStep()"
                   ng-click="workflow.handleNext()" ng-cloak/>
        </div>
    </div>
</div>

Controllers:

angular.module('Test')
    .config(function ($routeProvider) {
        $routeProvider
            .when('/applicant-details', {
                templateUrl: 'applicant-details',
                controller: 'ApplicantDetailsController'
            })
            .when('/methodology', {
                templateUrl: 'methodology',
                controller: 'MethodologyController'
            })
            .when('/approval-details', {
                templateUrl: 'approval-details',
                controller: 'ApprovalDetailsController'
            })
            .when('/confirmation', {
                templateUrl: 'confirmation',
                controller: 'ConfirmationController'
            })
            .otherwise({redirectTo: '/applicant-details'});
    })
;


function LodgeApplicationWorkflowController( $scope, ctx, workflow, workflowModel, server, navigation) {
    workflow.setSteps([
        {
            name: 'Applicant details',
            path: 'applicant-details',
            validationUrl: '/some url'
        },

        {
            name: 'Methodology',
            path: 'methodology'
        },
        {
            name: 'Approval details',
            path: 'approval-details'
        },
        {
            name: 'Confirmation',
            path: 'confirmation',
            nextButtonLabel: 'Lodge',
            onSubmit: function () {
                disable('submit');

                $scope.model.lodgeApplication = JSON.stringify($scope.model);
                server.post({
                    url: ctx + '/some url' ,
                    json: JSON.stringify($scope.model),
                    successHandler: function () {
                    },
                    completeHandler: function () {
                        enable('submit');
                    },
                    validationErrorTitle: 'The request could not be completed because of the following issues:'
                });
            }
        }
    ]);

    function postInit() {
        // To DO
    }

    function loadLodgement() {
        // To DO
    }


    $scope.workflow = workflow;
    $scope.model = workflowModel.model();
    $scope.refData = workflowModel.refData();
    $scope.accountDetails = {};
    $scope.userDetails = {};
    $scope.model.canSaveDraftAndClose = true;

    server.getReferenceData([
        '/URL1'
    ], function onAllReferenceDataRetrieved(data) {
        $scope.$apply(function() {
            $scope.refData.fuelSourceOptions = data[0];
            $scope.refData.contactInfos = data[1].result;
            $scope.refData.address = data[2];
            $scope.refData.yearOptions = data[3];
            $scope.refData.nmiNetworkOptions = data[4];
        });
        loadLodgement();
        loadDraft();
    });

    $scope.saveDraftAndClose = function () {

        var command = {};

        server.post({
            url: ctx + '/URL',
            json: JSON.stringify(command),
            successHandler: function (data) {

            },
            validationErrorTitle: 'The request could not be completed because of the following issues:'
        });
    };

    $scope.cancel = function() {
        navigation.to('Some URL');
    };
}

function ApplicantDetailsController($scope, workflow, workflowModel, addressService, applicantServiceFactory) {
    var applicantService = applicantServiceFactory();
    if (!workflow.setCurrentScope($scope)) {
        return;
    }
    $scope.model = workflowModel.model();
    $scope.model.applicantDetails = _.extend({
        owner: { address: {} },
        operator: { address: {} }
    }, $scope.model.applicantDetails);

    addressService.initialiseAddress($scope.model.applicantDetails);

}
function MethodologyController($scope, workflow, workflowModel) {
    if (!workflow.setCurrentScope($scope)) {
        return;
    }
    $scope.model = workflowModel.model();
    // Do something
}

function ApprovalDetailsController($scope, workflow, workflowModel) {
    if (!workflow.setCurrentScope($scope)) {
        return;
    }
    $scope.model = workflowModel.model();
    $scope.model.approvalDetails = $scope.model.approvalDetails || {};
    // Do something
}

function ConfirmationController($scope, workflow, workflowModel) {
    if (!workflow.setCurrentScope($scope)) {
        return;
    }
    $scope.model = workflowModel.model();
    $scope.model.confirmation = { owner: {}, operator: {} };
    $scope.model.confirmationConfirmed = false;
    // Do something

}

WorkFlow

angular.module('Test')
    .service('workflowModel', function() {
        var refData = {};
        var workflowModel = {};

        return {
            reset: function() {
                workflowModel = {};
            },
            get : function(fragmentName) {
                if (!workflowModel[fragmentName]) {
                    workflowModel[fragmentName] = {};
                }
                return workflowModel[fragmentName];
            },
            model : function(newWorkflowModel) {
                if (newWorkflowModel) {
                    workflowModel = newWorkflowModel;
                } else {
                    return workflowModel;
                }
            },
            refData : function() {
                return refData;
            },
            toJSON: function() {
                return JSON.stringify(workflowModel);
            }
        };
    })
    .directive('workflowBreadcrumbs', function() {
        return {
            restrict: 'E',
            template: '<ul class="breadcrumb">' +
            '<li ng-class="{\'active\': workflow.currentStepPathIs(\'{{step.path}}\')}" ng-repeat="step in workflow.configuredSteps" ng-cloak>' +
            '<a href="#/{{step.path}}" ng-if="workflow.visitedStep(step.path)">{{step.name}}</a><span ng-if="!workflow.visitedStep(step.path)">{{step.name}}</span><span class="divider" ng-if="!$last">/</span>' +
            '</li>' +
            '</ul>',
            transclude: true
        };
    })
    .factory('workflow', function ($rootScope, $location, server, validator, workflowModel, $timeout) {
        function getStepByPath(configuredSteps, stepPath) {
            return _.find(configuredSteps, function(step) { return step.path === stepPath; });
        }

        function validateServerSide(currentStep, callback) {
            if (currentStep.validationUrl) {
                server.post({
                    url: ctx + currentStep.validationUrl,
                    json : JSON.stringify(workflowModel.model()),
                    successHandler : function() {
                        $rootScope.$apply(function() {
                            callback();
                        });
                    }
                });
            } else {
                callback();
            }
        }

        function navigateNext(configuredSteps, currentStep) {
            var currentStepIndex = _.indexOf(configuredSteps, currentStep);
            navigateTo(configuredSteps[currentStepIndex + 1]);
        }

        function navigateTo(step) {
            $location.path(step.path);
        }

        return {
            setCurrentScope: function(scope) {
                this.currentScope = scope;
                this.firstStep = _.first(this.configuredSteps);
                this.lastStep = _.last(this.configuredSteps);
                this.currentStep = this.getCurrentStep();
                if (!(this.currentStep === this.firstStep || this.hasEverVisitedSteps())) {
                    this.reset();
                    return false;
                }
                this.currentStep.visited = true;
                hideErrorMessages();
                this.focusOnFirstInputElementAndScrollToTop();
                return true;
            },
            setSteps: function(steps) {
                this.configuredSteps = steps;
            },
            focusOnFirstInputElementAndScrollToTop: function() {
                $timeout(function() {
                    angular.element('select, input, textarea, button', '[ng-view]')
                        .filter(':visible')
                        .first()
                        .one('focus', scrollToTitle)
                        .focus();
                    scrollToTitle();
                });
            },
            hasEverVisitedSteps: function() {
                return _.find(this.configuredSteps, function(step) {
                        return step.visited;
                    }) !== undefined;
            },
            isFirstStep: function() {
                return this.currentStep === this.firstStep;
            },
            isLastStep: function() {
                return this.currentStep === this.lastStep;
            },
            currentStepPathIs: function(stepPath) {
                return this.currentStep && stepPath === this.currentStep.path;
            },
            visitedStep: function(stepPath) {
                return getStepByPath(this.configuredSteps, stepPath).visited;
            },
            getNextLabel: function() {
                if (this.currentStep && this.currentStep.nextButtonLabel) {
                    return this.currentStep.nextButtonLabel;
                }
                return (this.isLastStep()) ? 'Submit' : 'Next';
            },
            handlePrevious: function() {
                if (!this.isFirstStep()) {
                    var currentStepIndex = _.indexOf(this.configuredSteps, this.currentStep);
                    navigateTo(this.configuredSteps[currentStepIndex - 1]);
                }
            },
            canNavigateToNextStep: function() {
                return this.currentScope && (!this.currentScope.canSubmit || this.currentScope.canSubmit());
            },
            handleNext: function() {
                var configuredSteps = this.configuredSteps;
                var currentStep = this.currentStep;

                if(this.isLastStep()) {
                    this.validateCurrentStep(this.currentStep.onSubmit);
                } else {
                    this.validateCurrentStep(function() {
                        navigateNext(configuredSteps, currentStep);
                    });
                }
            },
            validateCurrentStep: function(callback) {
                var currentStep = this.currentStep;

                if (this.currentScope.form) {
                    validator.validate(this.currentScope, function() {
                        validateServerSide(currentStep, callback);
                    });
                } else {
                    validateServerSide(currentStep, callback);
                }
            },
            getCurrentStep: function() {
                return getStepByPath(this.configuredSteps, $location.path().substring(1));
            },
            reset: function() {
                _.each(this.configuredSteps, function(step) { step.visited = false; });
                this.firstStep.visited = true;
                navigateTo(this.firstStep);
            }
        };
    });
Fareed
  • 33
  • 4

0 Answers0