AngularJS自定義插件實現(xiàn)網(wǎng)站用戶引導功能示例
本文實例講述了AngularJS自定義插件實現(xiàn)網(wǎng)站用戶引導功能。分享給大家供大家參考,具體如下:
最近由于項目進行了較大的改版,為了讓用戶能夠適應這次新的改版,因此在系統(tǒng)中引入了“用戶引導”功能,對于初次進入系統(tǒng)的用戶一些簡單的使用培訓training。對于大多數(shù)網(wǎng)站來說,這是一個很常見的功能。所以在開發(fā)這個任務之前,博主嘗試將其抽象化,獨立于現(xiàn)有系統(tǒng)的業(yè)務邏輯,將其封裝為一個通用的插件,使得代碼更容易擴展和維護。
無圖無真相,先上圖:

關(guān)于這款trainning插件的使用很簡單,它采用了類似Angular路由一樣的配置,只需要簡單的配置其每一步training信息。
title:step的標題信息;
template/templateUrl: step的內(nèi)容模板信息。這類可以配置html元素,或者是模板的url地址,同時templateUrl也支持Angular route一樣的function語法;
controller: step的控制器配置;在controller中可注入如下參數(shù):當前step – currentStep、所有step的配置 – trainnings、當前step的配置 – currentTrainning、以及下一步的操作回調(diào) – trainningInstance(其中nextStep:為下一步的回調(diào),cancel為取消用戶引導回調(diào));
controllerAs: controller的別名;
resolve:在controller初始化前的數(shù)據(jù)配置,同Angular路由中的resolve;
locals:本地變量,和resolve相似,可以傳遞到controller中。區(qū)別之處在于它不支持function調(diào)用,對于常量書寫會比resolve更方便;
placement: step容器上三角箭頭的顯示方位,
position: step容器的具體顯示位置,這是一個絕對坐標;可以傳遞{left: 100, top: 100}的絕對坐標,也可以是#stepPanelHost配置相對于此元素的placement位置。同時它也支持自定義function和注入Angular的其他組件語法。并且默認可注入:所有step配置 – trainnings,當前步驟 – step,當前step的配置 – currentTrainning,以及step容器節(jié)點 – stepPanel;
backdrop:是否需要顯示遮罩層,默認顯示,除非顯示聲明為false配置,則不會顯示遮罩層;
stepClass:每一個step容器的樣式信息;
backdropClass: 每一個遮罩層的樣式信息。
了解了這些配置后,并根據(jù)特定需求定制化整個用戶引導的配置信息后,我們就可以使用trainningService的trainning方法來在特定實際啟動用戶引導,傳入?yún)?shù)為每一步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) {
// 自定義函數(shù),使其屏幕居中
var win = angular.element($window);
return {
top: (win.height() - stepPanel.height()) / 2,
left: (win.width() - stepPanel.width()) / 2
}
}]
}]
})
本文插件源碼和演示效果唯一codepen上,效果圖如下:

在trainning插件的源碼設計中,包含如下幾個要點:
提供service api。因為關(guān)于trainning這個插件,它是一個全局的插件,正好在Angular中所有的service也是單例的,所以將用戶引導邏輯封裝到Angular的service中是一個不錯的設計。但對于trainning的每一步展示內(nèi)容信息則是DOM操作,在Angular的處理中它不該存在于service中,最佳的方式是應該把他封裝到Directive中。所以這里采用Directive的定義,并在service中compile,然后append到body中。
對于每一個這類獨立的插件應該封裝一個獨立的scope,這樣便于在后續(xù)的銷毀,以及不會與現(xiàn)有的scope變量的沖突。
$q對延時觸發(fā)的結(jié)果包裝。對于像該trainning插件或者modal這類操作結(jié)果采用promise的封裝,是一個不錯的選擇。它取代了回調(diào)參數(shù)的復雜性,并以流暢API的方式展現(xiàn),更利于代碼的可讀性。同時也能與其他Angular service統(tǒng)一返回API。
對于controller、controllerAs、resolve、template、templateUrl這類類似路由的處理代碼,完全可以移到到你的同類插件中去。它們可以增加插件的更多定制化擴展。關(guān)于這部分代碼的解釋,博主將會在后續(xù)文章中為大家推送。
利用$injector.invoke動態(tài)注入和調(diào)用Angular service,這樣既能獲取Angular其他service注入的擴展性,也能獲取到函數(shù)的動態(tài)性。如上例中的屏幕居中的自定義擴展方式。
這類設計要點,同樣可以運用到想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程序設計有所幫助。
- AngularJS中實現(xiàn)用戶訪問的身份認證和表單驗證功能
- 3個可以改善用戶體驗的AngularJS指令介紹
- AngularJS用戶選擇器指令實例分析
- AngularJS實現(xiàn)用戶登錄狀態(tài)判斷的方法(Model添加攔截過濾器,路由增加限制)
- Angular.js與Bootstrap相結(jié)合實現(xiàn)表格分頁代碼
- 總結(jié)十個Angular.js由淺入深的面試問題
- 淺談angular.js中實現(xiàn)雙向綁定的方法$watch $digest $apply
- Angular.js如何從PHP讀取后臺數(shù)據(jù)
- Angular.js回顧ng-app和ng-model使用技巧
- Angular.js實現(xiàn)注冊系統(tǒng)的實例詳解
相關(guān)文章
詳解AngularJS controller調(diào)用factory
本篇文章主要介紹了詳解AngularJS controller調(diào)用factory,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-05-05
Angular?Ngrx?Store應用程序狀態(tài)典型示例詳解
這篇文章主要為大家介紹了Angular?Ngrx?Store應用程序狀態(tài)典型示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-07-07
利用Ionic2 + angular4實現(xiàn)一個地區(qū)選擇組件
ionic是一個移動端開發(fā)框架,使用hybird技術(shù),只要使用前端開發(fā)技術(shù)就可以開發(fā)出電腦端,安卓端和ios端的站點程序。下面這篇文章主要給大家介紹了關(guān)于利用Ionic2 + angular4實現(xiàn)一個地區(qū)選擇組件的相關(guān)資料,需要的朋友可以參考借鑒,下面來一起看看吧。2017-07-07
angularJS與bootstrap結(jié)合實現(xiàn)動態(tài)加載彈出提示內(nèi)容
這篇文章主要介紹了angularJS與bootstrap結(jié)合實現(xiàn)動態(tài)加載彈出提示內(nèi)容,通過bootstrp彈出提示。感興趣的朋友可以參考下本篇文章2015-10-10
詳解Angular的雙向數(shù)據(jù)綁定(MV-VM)
本文主要對Angular的雙向數(shù)據(jù)綁定(MV-VM)進行實例分析,具有一定的參考價值,下面跟著小編一起來看下吧2016-12-12

