AngularJS自定义插件实现网站用户引导功能示例


Posted in Javascript onNovember 07, 2016

本文实例讲述了AngularJS自定义插件实现网站用户引导功能。分享给大家供大家参考,具体如下:

最近由于项目进行了较大的改版,为了让用户能够适应这次新的改版,因此在系统中引入了“用户引导”功能,对于初次进入系统的用户一些简单的使用培训training。对于大多数网站来说,这是一个很常见的功能。所以在开发这个任务之前,博主尝试将其抽象化,独立于现有系统的业务逻辑,将其封装为一个通用的插件,使得代码更容易扩展和维护。

无图无真相,先上图:

AngularJS自定义插件实现网站用户引导功能示例

关于这款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上,效果图如下:

AngularJS自定义插件实现网站用户引导功能示例

在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#0。

上述代码摘录如下:

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程序设计有所帮助。

Javascript 相关文章推荐
可拖动窗口,附带鼠标控制渐变透明,开启关闭功能
Jun 26 Javascript
javascript function调用时的参数检测常用办法
Feb 26 Javascript
统计jQuery中各字符串出现次数的工具
May 03 Javascript
window.opener用法和用途实例介绍
Aug 19 Javascript
jquery ajax 局部刷新小案例
Feb 08 Javascript
JavaScript获取table中某一列的值的方法
May 06 Javascript
Node.js的基本知识简单汇总
Sep 19 Javascript
JavaScript和jQuery获取input框的绝对位置实现方法
Oct 13 Javascript
解决layui-open关闭自身窗口的问题
Sep 10 Javascript
javascript设计模式 ? 代理模式原理与用法实例分析
Apr 16 Javascript
vue项目启动出现cannot GET /服务错误的解决方法
Apr 26 Javascript
js实现html滑动图片拼图验证
Jun 24 Javascript
webpack常用配置项配置文件介绍
Nov 07 #Javascript
jQuery Mobile和HTML5开发App推广注册页
Nov 07 #Javascript
学习JavaScript图片预加载模块
Nov 07 #Javascript
pc加载更多功能和移动端下拉刷新加载数据
Nov 07 #Javascript
jquery html5 视频播放控制代码
Nov 06 #Javascript
js无提示关闭浏览器窗口的两种方法分析
Nov 06 #Javascript
详解Angular.js的$q.defer()服务异步处理
Nov 06 #Javascript
You might like
PHP中extract()函数的定义和用法
2012/08/17 PHP
php中Redis的应用--消息传递
2017/03/28 PHP
不常用但很实用的PHP预定义变量分析
2019/06/25 PHP
php实现商城购物车的思路和源码分析
2020/07/23 PHP
Javascript+XMLHttpRequest+asp.net无刷新读取数据库数据
2009/08/09 Javascript
VBS通过WMI监视注册表变动的代码
2011/10/27 Javascript
EasyUI中的tree用法介绍
2011/11/01 Javascript
javascript之典型高阶函数应用介绍二
2013/01/10 Javascript
jQuery的DOM操作之删除节点示例
2014/01/03 Javascript
一个检测表单数据的JavaScript实例
2014/10/31 Javascript
jQuery对指定元素中指定字符串进行替换的方法
2015/03/17 Javascript
Javascript实现颜色rgb与16进制转换的方法
2015/04/18 Javascript
javascript弹出拖动窗口
2015/08/11 Javascript
jquery mobile 实现自定义confirm确认框效果的简单实例
2016/06/17 Javascript
AngularJS 执行流程详细介绍
2016/08/18 Javascript
微信公众号菜单配置微信小程序实例详解
2017/03/31 Javascript
JS中call和apply函数用法实例分析
2018/06/20 Javascript
vue封装一个简单的div框选时间的组件的方法
2019/01/06 Javascript
使用axios请求时,发送formData请求的示例
2019/10/29 Javascript
浅谈Vue 函数式组件的使用技巧
2020/06/16 Javascript
JS hasOwnProperty()方法检测一个属性是否是对象的自有属性的方法
2021/01/29 Javascript
11个并不被常用但对开发非常有帮助的Python库
2015/03/31 Python
TensorFlow Session使用的两种方法小结
2018/07/30 Python
利用Python求阴影部分的面积实例代码
2018/12/05 Python
python mac下安装虚拟环境的图文教程
2019/04/12 Python
基于pytorch中的Sequential用法说明
2020/06/24 Python
九年级数学教学反思
2014/02/02 职场文书
艾滋病宣传标语
2014/06/25 职场文书
安全生产年活动总结
2014/08/29 职场文书
小时代观后感
2015/06/10 职场文书
刘胡兰观后感
2015/06/16 职场文书
安全守法证明
2015/06/23 职场文书
2015少先队大队辅导员工作总结
2015/07/24 职场文书
保护环境建议书作文300字
2015/09/14 职场文书
《折线统计图》教学反思
2016/02/22 职场文书