AngularJS自訂外掛程式實現網站使用者引導功能樣本_AngularJS

來源:互聯網
上載者:User

本文執行個體講述了AngularJS自訂外掛程式實現網站使用者引導功能。分享給大家供大家參考,具體如下:

最近由於項目進行了較大的改版,為了讓使用者能夠適應這次新的改版,因此在系統中引入了“使用者引導”功能,對於初次進入系統的使用者一些簡單的使用培訓training。對於大多數網站來說,這是一個很常見的功能。所以在開發這個任務之前,博主嘗試將其抽象化,獨立於現有系統的商務邏輯,將其封裝為一個通用的外掛程式,使得代碼更容易擴充和維護。

無圖無真相,先上圖:

關於這款trainning外掛程式的使用很簡單,它採用了類似Angular路由一樣的配置,只需要簡單的配置其每一步training資訊。

title:step的標題資訊;
template/templateUrl: step的內容範本資訊。這類可以配置html元素,或者是模板的url地址,同時templateUrl也支援Angular route一樣的function文法;
controller: step的控制器配置;在controller中可注入如下參數:當前step – currentStep、所有step的配置 – trainnings、當前step的配置 – currentTrainning、以及下一步的操作回調 – trainningInstance(其中nextStep:為下一步的回調,cancel為取消使用者引導回調);
controllerAs: controller的別名;
resolve:在controller初始化前的資料配置,同Angular路由中的resolve;
locals:本地變數,和resolve相似,可以傳遞到controller中。區別之處在於它不支援function調用,對於常量書寫會比resolve更方便;
placement: step容器上三角箭頭的顯示方位,
position: step容器的具體顯示位置,這是一個絕對座標;可以傳遞{left: 100, top: 100}的絕對座標,也可以是#stepPanelHost配置相對於此元素的placement位置。同時它也支援自訂function和注入Angular的其他組件文法。並且預設可注入:所有step配置 – trainnings,當前步驟 – step,當前step的配置 – currentTrainning,以及step容器節點 – stepPanel;
backdrop:是否需要顯示遮罩層,預設顯示,除非顯示聲明為false配置,則不會顯示遮罩層;
stepClass:每一個step容器的樣式資訊;
backdropClass: 每一個遮罩層的樣式資訊。

瞭解了這些配置後,並根據特定需求定製化整個使用者引導的配置資訊後,我們就可以使用trainningService的trainning方法來在特定實際啟動使用者引導,傳入參數為每一步step的配置資訊。並可以註冊其done或者cancel事件:

trainningService.trainning(trainningCourses.courses).done(function() { vm.isDone = true;});

下面是一個示範的配置資訊:

.constant('trainningCourses', {  courses: [{   title: 'Step 1:',   templateUrl: 'trainning-content.html',   controller: 'StepPanelController',   controllerAs: 'stepPanel',   placement: 'left',   position: '#blogControl'  },{   title: 'Step 3:',   templateUrl: 'trainning-content.html',   controller: 'StepPanelController',   controllerAs: 'stepPanel',   placement: 'top',   position: {    top: 200,    left: 100   }  },   ...  {   stepClass: 'last-step',   backdropClass: 'last-backdrop',   templateUrl: 'trainning-content-done.html',   controller: 'StepPanelController',   controllerAs: 'stepPanel',   position: ['$window', 'stepPanel', function($window, stepPanel) {    // 自訂函數,使其螢幕置中    var win = angular.element($window);    return {     top: (win.height() - stepPanel.height()) / 2,     left: (win.width() - stepPanel.width()) / 2    }   }]  }]})

本文外掛程式源碼和示範效果唯一codepen上,效果圖如下:

在trainning外掛程式的源碼設計中,包含如下幾個要點:

提供service api。因為關於trainning這個外掛程式,它是一個全域的外掛程式,正好在Angular中所有的service也是單例的,所以將使用者引導邏輯封裝到Angular的service中是一個不錯的設計。但對於trainning的每一步展示內容資訊則是DOM操作,在Angular的處理中它不該存在於service中,最佳的方式是應該把他封裝到Directive中。所以這裡採用Directive的定義,並在service中compile,然後append到body中。

對於每一個這類獨立的外掛程式應該封裝一個獨立的scope,這樣便於在後續的銷毀,以及不會與現有的scope變數的衝突。

$q對延時觸發的結果封裝。對於像該trainning外掛程式或者modal這類操作結果採用promise的封裝,是一個不錯的選擇。它取代了回調參數的複雜性,並以流暢API的方式展現,更利於代碼的可讀性。同時也能與其他Angular service統一返回API。

對於controller、controllerAs、resolve、template、templateUrl這類類似路由的處理代碼,完全可以移到到你的同類外掛程式中去。它們可以增加外掛程式的更多定製化擴充。關於這部分代碼的解釋,博主將會在後續文章中為大家推送。

利用$injector.invoke動態注入和調用Angular service,這樣既能擷取Angular其他service注入的擴充性,也能擷取到函數的動態性。如上例中的螢幕置中的自訂擴充方式。

這類設計要點,同樣可以運用到想modal、alert、overload這類全域外掛程式中。有興趣的讀者,你可以在博主的codepen筆記中閱讀這段代碼http://codepen.io/greengerong/pen/pjwXQW。

上述代碼摘錄如下:

HTML:

<div ng-app="com.github.greengerong" ng-controller="DemoController as demo"> <div class="alert alert-success fade in" ng-if='demo.isDone'>  <strong>All trainning setps done!</strong> </div> <button id="startAgain" class="btn btn-primary start-again" ng-click="demo.trainning()">You can start trainning again</button> <div class="blog">  <form class="form-inline">   <div class="form-group">    <label class="sr-only" for="exampleInputAmount">Blog :</label>    <div class="input-group">     <input id="blogControl" type="text" class="form-control" />    </div>   </div>   <button id="submitBlog" class="btn btn-primary" ng-click="demo.backdrop()">Public blog</button>  </form> </div> <script type="text/ng-template" id="modal-backdrop.html">  <div class="modal-backdrop fade in {{backdropClass}}" ng-style="{'z-index': zIndex || 1040}"></div> </script> <script type="text/ng-template" id="trainning-step.html">  <div class="trainning-step">   <div style="display:block; z-index:1080;left:-1000px;top:-1000px;" ng-style="positionStyle" class="step-panel {{currentTrainning.placement}} fade popover in {{currentTrainning.stepClass}}" ng-show="!isProgressing">    <div class="arrow"></div>    <div class="popover-inner">     <h3 class="popover-title" ng-if='currentTrainning.title'>{{currentTrainning.title}}</h3>     <div class="popover-content">     </div>    </div>   </div>   <ui-backdrop backdrop-class="currentTrainning.backdropClass" ng-if="currentTrainning.backdrop !== false"></ui-backdrop>  </div> </script> <script type="text/ng-template" id="trainning-content.html">  <div class="step-content">   <div>{{ stepPanel.texts[stepPanel.currentStep - 1]}}</div>   <div class="next-step">    <ul class="step-progressing">    <li data-ng-repeat="item in stepPanel.trainnings.length | range"     data-ng-class="{active: stepPanel.currentStep == item}">    </li>   </ul>    <button type="button" class="btn btn-link btn-next pull-right" ng-click="stepPanel.trainningInstance.nextStep({$event:$event, step:step});">Next</button>   </div>  </div> </script> <script type="text/ng-template" id="trainning-content-done.html">  <div class="step-content">    <div> {{ stepPanel.texts[stepPanel.currentStep - 1]}}   </div>   <div class="next-step">    <ul class="step-progressing">    <li data-ng-repeat="item in stepPanel.trainnings.length | range"     data-ng-class="{active: stepPanel.currentStep == item}">    </li>   </ul>    <button type="button" class="btn btn-link pull-right" ng-click="nextStep({$event:$event, step:step});">Got it</button>   </div>  </div> </script></div>

CSS:

.last-step{ /* background-color: blue;*/}.last-backdrop{ background-color: #FFFFFF;}.blog{ position: absolute; left: 300px; top: 100px;}.start-again{ position: absolute; left: 400px; top: 250px;}.next-step { .step-progressing {  margin: 10px 0px;  display: inline-block;  li {  margin-right: 5px;  border: 1px solid #fff;  background-color: #6E6E6E;  width: 12px;  height: 12px;  border-radius: 50%;  display: inline-block;  &.active {   background-color: #0000FF;  }  } }}

JS:

//Please set step content to fixed width when complex content or dynamic loading.angular.module('com.github.greengerong.backdrop', []) .directive('uiBackdrop', ['$document', function($document) {  return {   restrict: 'EA',   replace: true,   templateUrl: 'modal-backdrop.html',   scope: {    backdropClass: '=',    zIndex: '='   }   /* ,link: function(){    $document.bind('keydown', function(evt){     evt.preventDefault();     evt.stopPropagation();    });    scope.$on('$destroy', function(){     $document.unbind('keydown');    });    }*/  }; }]) .service('modalBackdropService', ['$rootScope', '$compile', '$document', function($rootScope, $compile, $document) {  var self = this;  self.backdrop = function(backdropClass, zIndex) {   var $backdrop = angular.element('<ui-backdrop></ui-backdrop>')    .attr({     'backdrop-class': 'backdropClass',     'z-index': 'zIndex'    });   var backdropScope = $rootScope.$new(true);   backdropScope.backdropClass = backdropClass;   backdropScope.zIndex = zIndex;   $document.find('body').append($compile($backdrop)(backdropScope));   return function() {    $backdrop.remove();    backdropScope.$destroy();   };  }; }]);angular.module('com.github.greengerong.trainning', ['com.github.greengerong.backdrop', 'ui.bootstrap']) .directive('trainningStep', ['$timeout', '$http', '$templateCache', '$compile', '$position', '$injector', '$window', '$q', '$controller', function($timeout, $http, $templateCache, $compile, $position, $injector, $window, $q, $controller) {  return {   restrict: 'EA',   replace: true,   templateUrl: 'trainning-step.html',   scope: {    step: '=',    trainnings: '=',    nextStep: '&',    cancel: '&'   },   link: function(stepPanelScope, elm) {    var stepPanel = elm.find('.step-panel');    stepPanelScope.$watch('step', function(step) {     if (!step) {      return;     }     stepPanelScope.currentTrainning = stepPanelScope.trainnings[stepPanelScope.step - 1];     var contentScope = stepPanelScope.$new(false);     loadStepContent(contentScope, {      'currentStep': stepPanelScope.step,      'trainnings': stepPanelScope.trainnings,      'currentTrainning': stepPanelScope.currentTrainning,      'trainningInstance': {       'nextStep': stepPanelScope.nextStep,       'cancel': stepPanelScope.cancel      }     }).then(function(tplAndVars) {      elm.find('.popover-content').html($compile(tplAndVars[0])(contentScope));     }).then(function() {      var pos = stepPanelScope.currentTrainning.position;      adjustPosition(stepPanelScope, stepPanel, pos);     });    });    angular.element($window).bind('resize', function() {     adjustPosition(stepPanelScope, stepPanel, stepPanelScope.currentTrainning.position);    });    stepPanelScope.$on('$destroy', function() {     angular.element($window).unbind('resize');    });    function getPositionOnElement(stepScope, setpPos) {     return $position.positionElements(angular.element(setpPos), stepPanel, stepScope.currentTrainning.placement, true);    }    function positionOnElement(stepScope, setpPos) {     var targetPos = angular.isString(setpPos) ? getPositionOnElement(stepScope, setpPos) : setpPos;     var positionStyle = stepScope.currentTrainning || {};     positionStyle.top = targetPos.top + 'px';     positionStyle.left = targetPos.left + 'px';     stepScope.positionStyle = positionStyle;    }    function adjustPosition(stepScope, stepPanel, pos) {     if (!pos) {      return;     }     var setpPos = angular.isFunction(pos) || angular.isArray(pos) ? $injector.invoke(pos, null, {      trainnings: stepScope.trainnings,      step: stepScope.setp,      currentTrainning: stepScope.currentTrainning,      stepPanel: stepPanel     }) : pos;     //get postion should wait for content setup     $timeout(function() {      positionOnElement(stepScope, setpPos);     });    }    function loadStepContent(contentScope, ctrlLocals) {     var trainningOptions = contentScope.currentTrainning,      getTemplatePromise = function(options) {       return options.template ? $q.when(options.template) :        $http.get(angular.isFunction(options.templateUrl) ? (options.templateUrl)() : options.templateUrl, {         cache: $templateCache        }).then(function(result) {         return result.data;        });      },      getResolvePromises = function(resolves) {       var promisesArr = [];       angular.forEach(resolves, function(value) {        if (angular.isFunction(value) || angular.isArray(value)) {         promisesArr.push($q.when($injector.invoke(value)));        }       });       return promisesArr;      },      controllerLoader = function(trainningOptions, trainningScope, ctrlLocals, tplAndVars) {       var ctrlInstance;       ctrlLocals = angular.extend({}, ctrlLocals || {}, trainningOptions.locals || {});       var resolveIter = 1;       if (trainningOptions.controller) {        ctrlLocals.$scope = trainningScope;        angular.forEach(trainningOptions.resolve, function(value, key) {         ctrlLocals[key] = tplAndVars[resolveIter++];        });        ctrlInstance = $controller(trainningOptions.controller, ctrlLocals);        if (trainningOptions.controllerAs) {         trainningScope[trainningOptions.controllerAs] = ctrlInstance;        }       }       return trainningScope;      };     var templateAndResolvePromise = $q.all([getTemplatePromise(trainningOptions)].concat(getResolvePromises(trainningOptions.resolve || {})));     return templateAndResolvePromise.then(function resolveSuccess(tplAndVars) {      controllerLoader(trainningOptions, contentScope, ctrlLocals, tplAndVars);      return tplAndVars;     });    }   }  }; }]) .service('trainningService', ['$compile', '$rootScope', '$document', '$q', function($compile, $rootScope, $document, $q) {  var self = this;  self.trainning = function(trainnings) {   var trainningScope = $rootScope.$new(true),    defer = $q.defer(),    $stepElm = angular.element('<trainning-step></trainning-step>')    .attr({     'step': 'step',     'trainnings': 'trainnings',     'next-step': 'nextStep($event, step);',     'cancel': 'cancel($event, step)'    }),    destroyTrainningPanel = function(){     if (trainningScope) {      $stepElm.remove();      trainningScope.$destroy();     }    };   trainningScope.cancel = function($event, step){    defer.reject('cancel');   };   trainningScope.nextStep = function($event, step) {    if (trainningScope.step === trainnings.length) {     destroyTrainningPanel();     return defer.resolve('done');    }    trainningScope.step++;   };   trainningScope.trainnings = trainnings;   trainningScope.step = 1;   $document.find('body').append($compile($stepElm)(trainningScope));   trainningScope.$on('$locationChangeStart', destroyTrainningPanel);   return {    done: function(func) {     defer.promise.then(func);     return this;    },    cancel: function(func) {     defer.promise.then(null, func);     return this;    }   };  }; }]);angular.module('com.github.greengerong', ['com.github.greengerong.trainning']).filter('range', [function () {   return function (len) {    return _.range(1, len + 1); }; }]) .controller('StepPanelController', ['currentStep', 'trainnings', 'trainningInstance', 'trainnings', function(currentStep, trainnings, trainningInstance, trainnings) {  var vm = this;  vm.currentStep = currentStep;  vm.trainningInstance = trainningInstance;  vm.trainnings = trainnings;  vm.texts = ['Write your own sort blog.', 'Click button to public your blog.', 'View your blog info on there.', 'Click this button, you can restart this trainning when .', 'All trainnings done!'];  return vm; }]) .constant('trainningCourses', {  courses: [{   title: 'Step 1:',   templateUrl: 'trainning-content.html',   controller: 'StepPanelController',   controllerAs: 'stepPanel',   placement: 'left',   position: '#blogControl'  }, {   title: 'Step 2:',   templateUrl: 'trainning-content.html',   controller: 'StepPanelController',   controllerAs: 'stepPanel',   placement: 'right',   backdrop: false,   position: '#submitBlog'  }, {   title: 'Step 3:',   templateUrl: 'trainning-content.html',   controller: 'StepPanelController',   controllerAs: 'stepPanel',   placement: 'top',   position: {    top: 200,    left: 100   }  }, {   title: 'Step 4:',   templateUrl: 'trainning-content.html',   controller: 'StepPanelController',   controllerAs: 'stepPanel',   placement: 'bottom',   position: '#startAgain'  }, {   stepClass: 'last-step',   backdropClass: 'last-backdrop',   templateUrl: 'trainning-content-done.html',   controller: 'StepPanelController',   controllerAs: 'stepPanel',   position: ['$window', 'stepPanel', function($window, stepPanel) {    var win = angular.element($window);    return {     top: (win.height() - stepPanel.height()) / 2,     left: (win.width() - stepPanel.width()) / 2    }   }]  }] }) .controller('DemoController', ['trainningService', 'trainningCourses', 'modalBackdropService', function(trainningService, trainningCourses, modalBackdropService) {  var vm = this;  vm.trainning = function() {   //call this service should wait your really document ready event.   trainningService.trainning(trainningCourses.courses)    .done(function() {     vm.isDone = true;    });  };  var backdropInstance = angular.noop;  vm.backdrop = function() {   modalBackdropService.backdrop();  };  vm.trainning();  return vm; }]);

希望本文所述對大家AngularJS程式設計有所協助。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.