4
\$\begingroup\$

I'm currently working on creating a step by step wizard in Angular and twitter-bootstrap. I've actually got it to work as intended but as I'm looking to advance my skills I'd like to hear if you have any feedback on my code.

How can I avoid using !important in the CSS classes? What else could be improved? I'm a little bit concerned about how I handle all the conditional "finished" / "active" / "unfinished" styling, it looks messy. I suspect that it would be possible to make a cleaner solution if my steps would be presented as list items(?).

var myApp = angular.module('myApp', []);

function StepWizardCtrl($scope) {

  // define wizard steps
  $scope.steps = [{
    title: 'Details'
  }, {
    title: 'Preferences'
  }, {}];

  // set current step
  $scope.currentStep = 2;


  // next step
  $scope.nextStep = function() {
    setStep($scope.currentStep + 1);
  };

  // prev step
  $scope.prevStep = function() {
    setStep($scope.currentStep - 1);
  };

  // jump to @step
  function setStep(step) {
    if (step < 0 || step > $scope.steps.length) {
      // throw error, trying to step out of scope
      throw {
        name: "Step by step wizard",
        message: "Trying to step out of the scope"
      };
    } else {
      $scope.currentStep = step;
    }
  };

  // get current status ('completed' / 'active' / 'incompleted') of @index in $scope.steps
  $scope.getStatus = function(index) {
    var status = '';

    if (index < $scope.currentStep) {
      // completed step
      status = 'completed';
    } else if (index == $scope.currentStep) {
      // current step
      status = 'active';
    } else {
      // incompleted step
      status = 'incompleted';
    }

    return status;
  };

  // calculate and get the progress bar value for $scope.currentStep
  $scope.getProgress = function() {
    return $scope.currentStep / $scope.steps.length * 100;
  };

  // determine horizontally where step @index is to be placed on the progress bar
  $scope.getDotPos = function(index) {
    // get position from left edge
    var leftPos = (index / $scope.steps.length) * 100;
    return 'left: ' + leftPos + '%';
  };


}
.modal-body {
  margin: 30px 100px 50px 100px;
}

.mod-progress {
  height: 10px !important;
  margin-bottom: 0px !important;
}

.progress-bar-success {
  background-color: #6ED387 !important;
  -webkit-box-shadow: none !important;
}

.dot {
  position: absolute;
  z-index: 1;
  height: 20px;
  width: 20px;
  border-radius: 25px;
  top: 50%;
  transform: translate(-50%, -50%);
}

.dot-complete {
  background: #23AF44;
  box-shadow: 0 0 0 10px #6ED387;
}

.dot-active {
  height: 35px !important;
  width: 35px !important;
  background: #f5f5f5;
  box-shadow: 0 0 0 10px #6ED387;
  animation: bounce .5s;
}

.dot-incomplete {
  height: 25px;
  width: 25px;
  background: #AAB2BD;
  box-shadow: 0 0 0 7px #CCD1D9;
}

.dot-last {
  font-size: 150%;
  border-radius: 5px;
}

.dot-inner {
  position: absolute !important;
  top: 50% !important;
  left: 50% !important;
  transform: translate(-50%, -50%) !important;
  color: #FFF;
  font-size: 75%;
}

.dot-inner-active {
  font-weight: 900;
  color: #58666e;
}

.dot-title {
  width: 100px;
  margin-top: 35px;
  text-align: center;
  word-break: break-all;
  position: absolute;
  top: 50%;
  left: 50%;
  -webkit-transform: translate(-50%, -50%);
  -moz-transform: translate(-50%, -50%);
  transform: translate(-50%, -50%);
}

@keyframes bounce {
  0% {
    width: 20px;
    height: 20px;
  }
  50% {
    width: 40px;
    height: 40px;
  }
  60% {
    width: 45px;
    height: 45px;
  }
  80% {
    width: 30px;
    height: 30px;
  }
  100% {
    width: 35px;
    height: 35px;
  }
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet" />

<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.8/angular.min.js"></script>

<body ng-app="myApp"></body>
<div ng-controller="StepWizardCtrl">
  <div class="modal show">
    <div class="modal-dialog">
      <div class="modal-content">
        <div class="modal-header">
          <h4 class="modal-title">Step by step wizard</h4>
        </div>
        <div class="modal-body">
          <!-- STEP BY STEP PROGRESS WIZARD -->
          <div class="progress mod-progress">
            <!-- STEPS -->
            <div ng-repeat="step in steps" class="dot" ng-class="{'completed':'dot-complete', 'active':'dot-active', 'incompleted':'dot-incomplete'}[getStatus($index)]" style="{{getDotPos($index)}}">
              <!-- DOT (STEP MILESTONE) -->
              <div class="dot-inner" ng-class="{'completed':'glyphicon glyphicon-ok'}[getStatus($index)]">
                <!-- INSIDE DOT -->
                <div ng-show="getStatus($index) != 'completed'" ng-class="{'active':'dot-inner-active'}[getStatus($index)]">
                  {{$index+1}}
                </div>
              </div>
              <!-- DOT TITLE -->
              <div class="dot-title">
                {{step.title || 'step '+($index+1)}}
              </div>
            </div>
            <!-- LAST STEP -->
            <div class="dot dot-last" ng-class="{'completed':'dot-complete', 'active':'dot-active', 'incompleted':'dot-incomplete'}[getStatus(steps.length)]" style="{{getDotPos(steps.length)}}">
              <div class="dot-inner glyphicon glyphicon-flag" ng-class="{'active':'dot-inner-active'}[getStatus(steps.length)]"></div>
            </div>
            <div class="progress-bar progress-bar-success" style="width: {{getProgress()}}%;"></div>
          </div>
        </div>
        <div class="modal-footer">
          <!-- FOOTER BUTTONS -->
          <button ng-disabled="currentStep==0" type="button" class="btn btn-primary" ng-click="prevStep()">Previous</button>
          <button ng-disabled="currentStep>=steps.length" type="button" class="btn btn-primary" ng-click="nextStep()">Next</button>
        </div>
      </div>
      <!-- /.modal-content -->
    </div>
    <!-- /.modal-dialog -->
  </div>

\$\endgroup\$
1
\$\begingroup\$

You code does seem to work without !important. Here is the plunker demonstrating.

  • To better organize your css you could use SMACSS, BEM, OOCSS or some custom nomenclature
  • You could remove $scope and replace it with controller As syntax
  • You could use one time binding {{:: }} whenever possible
  • If possible you could use ES6
  • Rather having separate variables to maintain state of your wizard you could have a single object and store all the flags and function relating to the wizard state in it so that it would be easy to debug

so your code will become

  // jump to @step
  function setStep(step) {
    if (step < 0 || step > vm.steps.length) {
      // throw error, trying to step out of scope
      throw {
        name: "Step by step wizard",
        message: "Trying to step out of the scope"
      };
    } else {
      vm.currentStep = step;
    }
  };


  vm.wizardState = {

    // flags
    steps: [{
      title: 'Details'
    }, {
      title: 'Preferences'
    }, {}],
    currentStep: 2,

    // functionality
    nextStep: function () {
      setStep(vm.currentStep + 1);
    },
    prevStep: function () {
      setStep(vm.currentStep - 1);
    },
    getStatus: function (index) {
      var status = '';

      if (index < vm.currentStep) {
        // completed step
        status = 'completed';
      } else if (index == vm.currentStep) {
        // current step
        status = 'active';
      } else {
        // incompleted step
        status = 'incompleted';
      }

      return status;
    },
    getProgress: function () {
      return vm.currentStep / vm.steps.length * 100;
    },
    getDotPos: function (index) {
      // get position from left edge
      var leftPos = (index / vm.steps.length) * 100;
      return 'left: ' + leftPos + '%';
    }
  }
|improve this answer|||||
\$\endgroup\$
  • 1
    \$\begingroup\$ For the love of all that is holy, do not use BEM or SMACSS. \$\endgroup\$ – Jonah Jul 10 '17 at 3:46
  • 1
    \$\begingroup\$ I meant any thing similar or custom nomenclature. It is opiniated \$\endgroup\$ – user93 Jul 10 '17 at 4:07
  • \$\begingroup\$ Thank you so much for your time and knowledge, it's more than highly appreciated! \$\endgroup\$ – InfinityStream Jul 10 '17 at 17:57
  • \$\begingroup\$ *!important is required in the editor here on stackexchange but works fine without in external editors such as plunker and JSFiddle. * I'm currently looking into CSS preprocessors but my main concern is that I'm going to be dependent on an additional library that's not going to bring too much value for this tiny project, what's your take on that? * What are the benefits of using "controller as" in place of $scope? * One time binding was an excellent advice, thanks! \$\endgroup\$ – InfinityStream Jul 10 '17 at 18:12
  • 1
    \$\begingroup\$ stackoverflow.com/a/32756679/2445295 \$\endgroup\$ – user93 Jul 11 '17 at 0:59

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.