Angular: Next section shown before previous section hidden - using ng-show - javascript

I'm implementing a reusable step-by-step wizard directive in angular based on this example. It's working pretty well, but like in the example, I'm using ng-show to hide all steps but the current one. This results in a quick flicker whenever I change steps where both the current and next step is shown simultaneously. What I can't figure out is how to do away with the flicker and make sure only a single step is shown at any one time.
What I've tried:
My own initial attempt at solving this issue was to change the show/hide-mechanic to use ng-switch but it doesn't work well since the ng-switch-when directive only accepts strings (so I can't populate it automatically with an index). Furthermore, ng-switch-when works through translcusion meaning I would have 2 transclusion directives on a single element which doesn't really make sense.
The wizard-directive as I've currently implemented it looks like this:
// Wizard
// ======
//
// This component implements a wizard-directive and a dependent step-directive
// which together can be used to define a step-by-step wizard with arbitrary
// html in each step.
//
// Ex:
// ```html
// <wizard>
// <step>
// <h1>Step one</h1>
// </step>
// <step>
// <h1>Step two</h1>
// </step>
// </wizard>
// ```
//
angular.module('wizard', [])
// Wizard Directive
// ----------------
//
// The main directive which defines the wizard element. A wizard can contain
// arbitrary html, but will only display a single step-element at a time. The
// directive also defines a couple of ways to navigate the wizard - through
// buttons and bottom "tabs".
//
.directive('wizard', function($rootScope) {
return {
restrict: 'E',
transclude: true,
scope: {},
templateUrl: $rootScope.templateBasePath + '/components/wizard/wizard.html',
controller: function($scope, $element) {
// Initialize the array of steps. This will be filled by any child
// steps added to the wizard.
var steps = $scope.steps = [];
// Search through the wizard to find what step is currently visible.
function getCurrentStepIndex() {
var index;
angular.forEach(steps, function(step, i) {
if (step.selected) {
index = i;
// End early when the selected step is found.
return;
}
});
return index;
}
// Make the imagePath available to the template.
$scope.imagePath = $rootScope.imagePath;
// Move to the next step in the wizard.
$scope.next = function () {
var index = getCurrentStepIndex();
if (index < steps.length - 1) {
steps[index].selected = false;
steps[index+1].selected = true;
}
};
// Move to the previous step of the wizard.
$scope.previous = function () {
var index = getCurrentStepIndex();
if (index > 0) {
steps[index].selected = false;
steps[index-1].selected = true;
}
};
// Select a given step in the wizard.
$scope.select = function(step) {
angular.forEach(steps, function(step) {
step.selected = false;
});
step.selected = true;
};
$scope.onFirstStep = function() {
return getCurrentStepIndex() === 0;
}
$scope.onLastStep = function() {
return getCurrentStepIndex() === steps.length - 1;
}
// Called by the step directive to add itself to the wizard.
this.addStep = function(step) {
// Select the first step when added.
if (steps.length === 0) {
$scope.select(step);
}
// Add the step to the step list.
steps.push(step);
};
}
};
})
// Step Directive
// --------------
//
// The Step Directive defines a section of code which constitues a distinct step
// in the overall goal of the wizard. The directive can only exist as a direct
// child of a wizard-tag.
//
.directive('step', function() {
return {
require: '^wizard', // require a wizard parent
restrict: 'E',
transclude: true,
scope: true,
template: '<div class="wizard__step ng-hide" ng-show="selected"></div>',
link: function(scope, element, attrs, wizardCtrl, transclude) {
// Add itself to the wizard's list of steps.
wizardCtrl.addStep(scope);
// Make the wizard scope available under "wizard" in the transcluded
// html scope.
scope.wizard = scope.$parent.$parent;
// Transclude the tag content in order to set the scope. This allows
// the content to access the wizard's next() and previous() functions.
var transDiv = angular.element(element).find('.wizard__step');
transclude(scope, function (clone) {
transDiv.append(clone);
});
}
};
});
The corresponding wizard template looks like so:
<div class="wizard" tabindex="1">
<div class="wizard__display">
<div class="wizard__previous" ng-click="previous()"><div class="guideBack" ng-hide="onFirstStep()"></div></div>
<div class="wizard__content" ng-transclude></div>
<div class="wizard__next" ng-click="next()"><div class="guideNext" ng-hide="onLastStep()"></div></div>
</div>
<ul class="nav wizard__tabs">
<li ng-repeat="step in steps" ng-click="select(step)" ng-class="{active:step.selected}"></li>
</ul>
</div>

I was making it unnecessarily difficult for myself by focusing all my attention on making it work with ng-show or ng-switch. The problem is eliminated quickly if you just perform the showing and hiding manually through jqlite/jquery. After some refactoring, the wizard-code looks like this:
// Wizard
// ======
//
// This component implements a wizard-directive and a dependent step-directive
// which together can be used to define a step-by-step wizard with arbitrary
// html in each step.
//
// Ex:
// ```html
// <wizard>
// <step>
// <h1>Step one</h1>
// </step>
// <step>
// <h1>Step two</h1>
// </step>
// </wizard>
// ```
//
angular.module('ssbbtip.wizard', [])
// Wizard Directive
// ----------------
//
// The main directive which defines the wizard element. A wizard can contain
// arbitrary html, but will only display a single step-element at a time. The
// directive also defines a couple of ways to navigate the wizard - through
// buttons, bottom breadcrumbs and keyboard arrow keys.
//
.directive('wizard', function($rootScope) {
return {
restrict: 'E',
transclude: true,
scope: {},
templateUrl: $rootScope.templateBasePath + '/components/wizard/wizard.html',
controller: function($scope, $element) {
// Initialize the array of steps. This will be filled by any child
// steps added to the wizard.
var steps = $scope.steps = [],
// currentStep is the shadow variable supporting the
// `$scope.currentStep` property.
currentStep = 0;
// This utility function will adjust a value to fit inside the
// specified range inclusively.
function clampToRange(min, max, value) {
// Make sure the max is at least as big as the min.
max = (min > max) ? min : max;
if (value < min) {
return min;
} else if (value > max) {
return max;
} else {
return value;
}
}
// This property specifies the currently visible step in the wizard.
Object.defineProperty($scope, 'currentStep', {
enumerable: true,
configurable: false,
get: function () { return currentStep; },
set: function (value) {
if (value && typeof(value) === 'number') {
currentStep = clampToRange(0, steps.length-1, value);
} else {
currentStep = 0;
}
}
});
// Make the imagePath available to the template.
$scope.imagePath = $rootScope.imagePath;
// Handle keyboard events on the wizard to allow navigation by
// keyboard arrows.
$scope.onKeydown = function (event) {
event.preventDefault();
console.log(event);
switch (event.which) {
case 37: // left arrow
case 38: // up arrow
$scope.previous();
break;
case 39: // right arrow
case 40: // down arrow
case 32: // space bar
$scope.next();
break;
}
};
// Move to the next step in the wizard.
$scope.next = function () {
$scope.currentStep = $scope.currentStep + 1;
};
// Move to the previous step of the wizard.
$scope.previous = function () {
$scope.currentStep = $scope.currentStep - 1;
};
$scope.onFirstStep = function() {
return $scope.currentStep === 0;
};
$scope.onLastStep = function() {
return $scope.currentStep === steps.length - 1;
};
// Called by the step directive to add itself to the wizard.
this.addStep = function (step) {
steps.push(step);
};
// This watches the `$scope.currentStep` property and updates the UI
// accordingly.
$scope.$watch(function () {
return $scope.currentStep;
}, function (newValue, oldValue) {
$element.find('step .wizard__step').eq(oldValue).addClass('ng-hide');
$element.find('step .wizard__step').eq(newValue).removeClass('ng-hide');
});
}
};
})
// Step Directive
// --------------
//
// The Step Directive defines a section of code which constitues a distinct step
// in the overall goal of the wizard. The directive can only exist as a direct
// child of a wizard-tag.
//
.directive('step', function() {
return {
require: '^wizard', // require a wizard parent
restrict: 'E',
transclude: true,
scope: true,
template: '<div class="wizard__step ng-hide"></div>',
link: function(scope, element, attrs, wizardCtrl, transclude) {
// Add itself to the wizard's list of steps.
wizardCtrl.addStep(scope);
// Make the wizard scope available under "wizard" in the transcluded
// html scope.
scope.wizard = scope.$parent.$parent;
// Transclude the tag content manually in order to set the scope.
// This allows the content to access the `wizard.next()` and
// `wizard.previous()` functions.
var transDiv = angular.element(element).find('.wizard__step');
transclude(scope, function (clone) {
transDiv.append(clone);
});
}
};
});
and the template:
<div class="wizard" tabindex="1" ng-keydown="onKeydown($event)">
<!-- tabindex 1 is needed to make the div selectable in order to capture keydown -->
<div class="wizard__display">
<div class="wizard__previous" ng-click="previous()"><div class="guideBack" ng-hide="onFirstStep()"></div></div>
<div class="wizard__content" ng-transclude></div>
<div class="wizard__next" ng-click="next()"><div class="guideNext" ng-hide="onLastStep()"></div></div>
</div>
<ul class="nav wizard__tabs">
<li ng-repeat="step in steps track by $index" ng-click="currentStep = $index" ng-class="{active: currentStep === $index}"></li>
</ul>
</div>

Related

Dynamically Add/Remove Attribute Directive based on state

I would like to build a directive that adds and remove a second directive based on a boolean flag.
So, when boolFlag = true, the div in example.html should recompile to include my-dir as an attribute. And it should have additional attributes required by my-dir in the form of "test=1".
Here is the basics of what I have so far:
example.html ----
<div add-remove when={{boolFlag}} dir-name="my-dir" dir-attrs="test, 1"></div>
add-remove-directive.js ----
function($compile, $timeout) {
return {
restrict: 'A',
link: function(scope, elem, attrs) {
var attrArray = attrs.dirAttrs.split(',');
for (a in attrArray) {
attrArray[a] = attrArray[a].trim();
}
attrs.$observe('when', function() {
// flagged when the directive should be attached to the element
var isWhen = attrs.when === 'true';
if (isWhen) {
for (a = 0; a < attrArray.length; a+=2) {
elem.attr(attrArray[a], attrArray[a+1]);
}
newScope = scope.$new();
cElem = $compile(elem)(newScope);
elem.replaceWith(cElem);
$timeout(function() {
scope.$destroy();
});
}
// if the flag is not set, remove the dynamic directive from the element
// but only if the element already has the given directive
else {
normDirName = attrs.$normalize(attrs.dirName);
console.log(normDirName);
console.log(r, 'elsed', attrs);
if (!attrs.hasOwnProperty(normDirName)) return;
elem.removeAttr(scope.dirName);
for (a = 0; a < attrArray.length; a+=2) {
elem.removeAttr(attrArray[a]);
}
newScope = scope.$new();
cElem = $compile(elem)(newScope);
elem.replaceWith(cElem);
$timeout(function() {
scope.$destroy();
});
}
}
}
---EDIT---
Sorry, that was an error with me copying it over from my project. It is functional up to how I described in the original post.
Basically where it's at now is I can dynamically "my-dir" directive correctly with the corresponding required attributes "test=1".
But the problems I'm running into are:
that there are two scopes active the original one without "my-dir" and another scope WITH "my-dir".
my else statement doesn't properly remove "my-dir" from the div when boolFlag is set to false.

Angular auto trigger specific directive in ng-repeat

I have an interesting situation.
I have a directive with isolate scope that generate list of numbers and the user can choose numbers like in lottery.
The problem i have is that i required minimum of 1 line, if the user pick only one line so when he click play i want to auto trigger the next directive in the ng-repeat to pick for him numbers, I made this plunker so you guys can understand better and help me.
http://plnkr.co/edit/vWGmSEpinf7wxRUnqyWq?p=preview
<div ng-repeat="line in [0,1,2,3]">
<div line line-config="lineConfig">
</div>
</div>
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
$scope.lineConfig = {
guessRange: 10
}
$scope.lines = [];
$scope.$on('lineAdded', function(event, line) {
$scope.lines.push(line);
});
$scope.play = function() {
/// here i want to check if $scope.lines.length
//is less then one if yes then auto trigger the next
//line directive to auto do quick pick and continue
}
})
.directive('line', function() {
return {
restrict: 'A',
templateUrl: 'line.html',
scope: {
lineConfig: '='
},
link: function($scope, elem, attr) {
var guessRange = $scope.lineConfig.guessRange;
$scope.cells = [];
$scope.line = {
nums: []
};
$scope.$watch('line', function(lotLine) {
var finaLine = {
line: $scope.line
}
if ($scope.line.nums.length > 4) {
$scope.$emit('lineAdded', finaLine);
}
}, true);
_(_.range(1, guessRange + 1)).forEach(function(num) {
$scope.cells.push({
num: num,
isSelected: false
});
});
$scope.userPickNum = function(cell) {
if (cell.isSelected) {
cell.isSelected = false;
_.pull($scope.lotLine.nums, cell.num);
} else {
cell.isSelected = true;
$scope.lotLine.nums.push(cell.num);
}
};
$scope.quickPick = function() {
$scope.clearLot();
$scope.line.nums = _.sample(_.range(1, guessRange + 1), 5);
_($scope.line.nums).forEach(function(num) {
num = _.find($scope.cells, {
num: num
});
num.isSelected = true;
});
}
$scope.clearLot = function() {
_($scope.cells).forEach(function(num) {
num.isSelected = false;
});
$scope.line.nums = [];
}
}
}
})
You could pass the $index (exists automatically in the ng-repeat scope) - variable into the directive and cause it to broadcast an event unique for ($index + 1) which is the $index for the next instance.
The event could be broadcasted from the $rootScope or a closer scope that's above the repeat.
Then you could capture the event in there.
Probably not the best way to do it.
I can try to elaborate if anything is unclear.
EDIT
So I played around alittle and came up with this:
http://plnkr.co/edit/ChRCyF7yQcN580umVfX1?p=preview
Rather
Rather than using events or services I went with using a directive controller to act as the parent over all the line directives inside it:
.directive('lineHandler', function () {
return {
controller: function () {
this.lines = [];
}
}
})
Then requiring 'lineHandler' controller inside the 'line' directive - the controller being a singleton (same instance injected into all the line directives) - you can then setup that controller to handle communication between your directives.
I commented most of my code in the updated plnkr and setup an example of what I think you requested when clicking in one list - affecting the one beneath.
I hope this helps and if anything is unclear I will try to elaborate.

How do I call a method on an Angularjs directive from outside it's template?

I'm new to Angularjs so please bear with me.
I wanted to create a generic wizard that would allow me to walk through a process. I started by creating a custom directive that handles pretty much all of the "wizarding" stuff ... steps, previous and next, etc.
While the wizard template provides a next and previous button for moving between steps, there may be some actions that would cause the controller/view using the wizard to need to indicate to the wizard that it needs to move forward or backward through a step. I haven't been able to figure out how to do that. I thought maybe $emit or $broadcast might work but I think maybe I'm missing something in regard to scope.
Take a look at this example: Plunker example It's not pretty but it gives you the idea. What I'd like to be able to do is force a "next" on the wizard when the user selects an order.
Here is the code for my directive:
angular.module("app")
.directive('rhWizard', [wizard])
.directive('rhStep', [step]);
function wizard() {
return {
restrict: "E",
transclude: true,
scope: {},
controller: function ($scope) {
var steps = $scope.steps = [];
$scope.selectedIndex = 0;
$scope.selectedStep = {};
$scope.$on('wizNext', function () {
$scope.next();
});
$scope.next = function () {
var result = $scope.selectedStep.completed();
if (!result)
return;
var _next = $scope.selectedIndex + 1;
if (_next < steps.length) {
$scope.select(steps[_next]);
}
};
$scope.prev = function () {
var _next = $scope.selectedIndex - 1;
if (_next > -1) {
$scope.select(steps[_next]);
}
}
$scope.select = function (step) {
for (var x = 0; x < steps.length; x++) {
if (steps[x] == step) {
$scope.selectedIndex = x;
$scope.selectedStep = step;
steps[x].selected = true;
}
else {
steps[x].selected = false;
}
}
};
this.addStep = function (step) {
steps.push(step);
step.id = steps.length;
if (steps.length === 1) {
$scope.select(step);
}
};
},
templateUrl: "/Scripts/app/common/rhwizard.html"
}
};
function step() {
return {
require: '^rhWizard',
restrict: 'E',
transclude: true,
scope: {
title: '#',
completed: '&isComplete',
nextText: '#',
prevText: '#'
},
link: function (scope, element, attrs, wizCtrl) {
wizCtrl.addStep(scope);
},
templateUrl: '/Scripts/app/common/rhsteps.html'
};
};
And the template code (rhwizard.html):
<div class="fuelux">
<div class="wizard">
<ul class="steps">
<li ng-class="{active: step.selected, complete: step.id <= selectedIndex }" ng-repeat="step in steps" ng-click="select(step)">
<span class="badge" ng-class="{'badge-info': step.selected, 'badge-success': step.id <= selectedIndex}" >{{step.id}}</span>{{step.title}}<span class="chevron">
</span>
</li>
</ul>
<div class="actions">
<button class="btn btn-primary btn-mini" ng-click="prev()" ng-disabled="selectedStep.id == 1"><i class="fa fa-chevron-left"></i> {{selectedStep.prevText || "Prev"}}</button>
<button class="btn btn-primary btn-mini" ng-click="next()" ng-disabled="selectedStep.id >= steps.length" >{{selectedStep.nextText || "Next" }} <i class="fa fa-chevron-right"></i></button>
</div>
</div>
<div class="step-content" style="padding-top:20px;" ng-transclude></div>
(rhstep.html)
<div ng-class="{active: selected}" class="step-pane" ng-transclude>
</div>
And a code snippet of it in use:
<div ng-app="app" ng-controller="orders as vm">
<rh-wizard>
<rh-step title="Select an order">
<div ng-repeat="order in vm.orders" ng-click="next()"></div>
</rh-step>
<rh-step title="Select a filter">
<p>This is the content</p>
</rh-step>
</rh-wizard>
</div>
The way this sort of thing is usually handled is by coordinating using a service. The state is managed by the service, and the directive only responds to changes in the state as far as it needs to modify the DOM, hide or display elements, etc. Your controllers outside can also have this service injected and call methods to change the state, such as move the step forward or back, reset the wizard, etc.
It's possible that the service would broadcast events for state changes, such as the step changing, and the directive would listen in on those events to update the view.
Alternately, perhaps in concert with the service, you'd just have a well-known object representing the state of a wizard; the steps, which step is active, which have been completed, etc. Then supply that state as a two-way bound scope item from the controller to the directive. Your directive can then do a deep $watch on that scope item for any changes and display them appropriately. Now, I mentioned "in concert with the service". By that I mean the service's actions, such as stepping forward or back, could operate on this wizard state object . That's preferable because you can make well-known actions that you can unit test, rather than leaving it up to the developer to make sure they modify the state object appropriately for the action they wish to express.
The difference between the two suggestions is in the first, the state is completely wrapped up in the service, and it merely notifies consumers when it wants them to react to an action. The latter is a little looser, as the state is coordinated directly between the view's controller and the directive's controller, and if you choose to still have a service, it's only there to make sure operations are done in a consistent and reliable manner. These aren't your only choices, of course. Which is best? It depends on your needs and how complicated your wizard is. With the former, you'll have to remember to reset the state, and if you use multiple wizards, come up with a way for it to know which wizard your view is working with. It seems like it might be worth it for a multi- or complicated wizard system. If the wizard you need is trivial and only used once, the latter might be quick and dirty but good enough.
I forked your plunk to include a wizard service that both the directive and any other component can use.
.factory('wizard', function() {
// private state
var state = {
steps: [],
selectedIndex: 0,
};
// public methods for manipulating the state
return {
next: function() {
var i = state.selectedIndex + 1;
if (i < state.steps.length) this.select(state.steps[i]);
},
prev: function() {
var i = state.selectedIndex - 1;
if (i > -1) this.select(state.steps[i]);
},
select: function(step) {
for(var x = 0; x < state.steps.length; x++){
var current = state.steps[x];
if ((angular.isNumber(step) && step === x) || current == step) {
current.selected = true;
state.selectedIndex = x;
} else {
current.selected = false;
}
}
},
addStep: function(step) {
state.steps.push(step);
if (state.steps.length === 1) this.select(step);
},
// lets a consumer get or set the current state. this is how the wizard directive grabs the state which is binds to in the template
state: function(newState) {
if (arguments.length === 0) return state;
if (newState) {
state = newState;
if (!state.steps) state.steps = [];
if (isNaN(state.selectedIndex)) state.selectedIndex = 0;
}
}
};
})

AngularJS - same timer for all directive instances

What is a best "Angular Way" to implement the directive that will have a shared timer for all it instances?
For example I have a directive "myComponent" and on the page it appears many times.
Inside of the component, exists some text that blink with some interval.
Because of business requirements and performance considerations, I would like that there will be single "timeout" that will toggle the blink for all instances at once (after document is ready).
I thought about the writing some code within directive definition:
//Pseudo code
angular.module("app",[]).directive("myComponent", function($timeout){
$(function() { $timeout(function(){ $(".blink").toggle(); }, 3000); } );
return {
//Directive definition
};
});
Or by using some kind of service that will receive the $element and add remove class to it:
//Pseudo code
angular.module("app",[])
.service("myService", function($timeout){
var elements = [];
this.addForBlink = function(element) { elements.push(element) };
$(function() { $timeout(function(){ $(elements).toggle(); }, 3000); } );
})
.directive("myComponent", function(myService){
return {
compile:function($element){
myService.addForBlink($element);
return function() {
//link function
}
}
};
});
In my opinion the most elegant and efficient would be to combine both these approaches by specifying the logic of the directive in the very directive initialization function. Here is a scaffold of what I actually mean:
app.directive('blinking', function($timeout){
var blinkingElements = [];
var showAll = function() {
for(var i = 0; i < blinkingElements.length; i++){
blinkingElements[i].addClass("blinking");
}
};
var hideAll = function() {
for(var i = 0; i < blinkingElements.length; i++){
blinkingElements[i].removeClass("blinking");
}
};
var blink = function () {
$timeout(showAll, 500);
$timeout(function(){
hideAll();
if (blinkingElements.length > 0) {
blink();
}
}, 1000);
};
return {
link : function(scope, element, attrs){
blinkingElements.push(element);
if (blinkingElements.length == 1) {
blink();
}
element.on("$destroy", function(){
var index = blinkingElements.indexOf(element);
blinkingElements.splice(index, 1);
});
}
}
});
And here is the working demo.
Moreover you can inject some service that will be responsible for configuration (setting the intervals and / or class) or you can provide the configuration by passing an object directly to the attribute. In the latter case you can enable applying different classes for different elements, but you should think of some policy how to deal with situation, when the interval was set more than once.

AngularJS : How to run JavaScript from inside Directive after directive is compiled and linked

I have a responsive template that I am trying to use with my Angularjs app. This is also my first Angular app so I know I have many mistakes and re-factoring in my future.
I have read enough about angular that I know DOM manipulations are suppose to go inside a directive.
I have a javascript object responsible for template re-sizes the side menu and basically the outer shell of the template. I moved all of this code into a directive and named it responsive-theme.
First I added all the methods that are being used and then I defined the App object at the bottom. I removed the function bodies to shorten the code.
Basically the object at the bottom is a helper object to use with all the methods.
var directive = angular.module('bac.directive-manager');
directive.directive('responsiveTheme', function() {
return {
restrict: "A",
link: function($scope, element, attrs) {
// IE mode
var isRTL = false;
var isIE8 = false;
var isIE9 = false;
var isIE10 = false;
var sidebarWidth = 225;
var sidebarCollapsedWidth = 35;
var responsiveHandlers = [];
// theme layout color set
var layoutColorCodes = {
};
// last popep popover
var lastPopedPopover;
var handleInit = function() {
};
var handleDesktopTabletContents = function () {
};
var handleSidebarState = function () {
};
var runResponsiveHandlers = function () {
};
var handleResponsive = function () {
};
var handleResponsiveOnInit = function () {
};
var handleResponsiveOnResize = function () {
};
var handleSidebarAndContentHeight = function () {
};
var handleSidebarMenu = function () {
};
var _calculateFixedSidebarViewportHeight = function () {
};
var handleFixedSidebar = function () {
};
var handleFixedSidebarHoverable = function () {
};
var handleSidebarToggler = function () {
};
var handleHorizontalMenu = function () {
};
var handleGoTop = function () {
};
var handlePortletTools = function () {
};
var handleUniform = function () {
};
var handleAccordions = function () {
};
var handleTabs = function () {
};
var handleScrollers = function () {
};
var handleTooltips = function () {
};
var handleDropdowns = function () {
};
var handleModal = function () {
};
var handlePopovers = function () {
};
var handleChoosenSelect = function () {
};
var handleFancybox = function () {
};
var handleTheme = function () {
};
var handleFixInputPlaceholderForIE = function () {
};
var handleFullScreenMode = function() {
};
$scope.App = {
//main function to initiate template pages
init: function () {
//IMPORTANT!!!: Do not modify the core handlers call order.
//core handlers
handleInit();
handleResponsiveOnResize(); // set and handle responsive
handleUniform();
handleScrollers(); // handles slim scrolling contents
handleResponsiveOnInit(); // handler responsive elements on page load
//layout handlers
handleFixedSidebar(); // handles fixed sidebar menu
handleFixedSidebarHoverable(); // handles fixed sidebar on hover effect
handleSidebarMenu(); // handles main menu
handleHorizontalMenu(); // handles horizontal menu
handleSidebarToggler(); // handles sidebar hide/show
handleFixInputPlaceholderForIE(); // fixes/enables html5 placeholder attribute for IE9, IE8
handleGoTop(); //handles scroll to top functionality in the footer
handleTheme(); // handles style customer tool
//ui component handlers
handlePortletTools(); // handles portlet action bar functionality(refresh, configure, toggle, remove)
handleDropdowns(); // handle dropdowns
handleTabs(); // handle tabs
handleTooltips(); // handle bootstrap tooltips
handlePopovers(); // handles bootstrap popovers
handleAccordions(); //handles accordions
handleChoosenSelect(); // handles bootstrap chosen dropdowns
handleModal();
$scope.App.addResponsiveHandler(handleChoosenSelect); // reinitiate chosen dropdown on main content resize. disable this line if you don't really use chosen dropdowns.
handleFullScreenMode(); // handles full screen
},
fixContentHeight: function () {
handleSidebarAndContentHeight();
},
setLastPopedPopover: function (el) {
lastPopedPopover = el;
},
addResponsiveHandler: function (func) {
responsiveHandlers.push(func);
},
// useful function to make equal height for contacts stand side by side
setEqualHeight: function (els) {
var tallestEl = 0;
els = jQuery(els);
els.each(function () {
var currentHeight = $(this).height();
if (currentHeight > tallestEl) {
tallestColumn = currentHeight;
}
});
els.height(tallestEl);
},
// wrapper function to scroll to an element
scrollTo: function (el, offeset) {
pos = el ? el.offset().top : 0;
jQuery('html,body').animate({
scrollTop: pos + (offeset ? offeset : 0)
}, 'slow');
},
scrollTop: function () {
App.scrollTo();
},
// wrapper function to block element(indicate loading)
blockUI: function (ele, centerY) {
var el = jQuery(ele);
el.block({
message: '<img src="./assets/img/ajax-loading.gif" align="">',
centerY: centerY !== undefined ? centerY : true,
css: {
top: '10%',
border: 'none',
padding: '2px',
backgroundColor: 'none'
},
overlayCSS: {
backgroundColor: '#000',
opacity: 0.05,
cursor: 'wait'
}
});
},
// wrapper function to un-block element(finish loading)
unblockUI: function (el) {
jQuery(el).unblock({
onUnblock: function () {
jQuery(el).removeAttr("style");
}
});
},
// initializes uniform elements
initUniform: function (els) {
if (els) {
jQuery(els).each(function () {
if ($(this).parents(".checker").size() === 0) {
$(this).show();
$(this).uniform();
}
});
} else {
handleUniform();
}
},
updateUniform : function(els) {
$.uniform.update(els);
},
// initializes choosen dropdowns
initChosenSelect: function (els) {
$(els).chosen({
allow_single_deselect: true
});
},
initFancybox: function () {
handleFancybox();
},
getActualVal: function (ele) {
var el = jQuery(ele);
if (el.val() === el.attr("placeholder")) {
return "";
}
return el.val();
},
getURLParameter: function (paramName) {
var searchString = window.location.search.substring(1),
i, val, params = searchString.split("&");
for (i = 0; i < params.length; i++) {
val = params[i].split("=");
if (val[0] == paramName) {
return unescape(val[1]);
}
}
return null;
},
// check for device touch support
isTouchDevice: function () {
try {
document.createEvent("TouchEvent");
return true;
} catch (e) {
return false;
}
},
isIE8: function () {
return isIE8;
},
isRTL: function () {
return isRTL;
},
getLayoutColorCode: function (name) {
if (layoutColorCodes[name]) {
return layoutColorCodes[name];
} else {
return '';
}
}
};
}
};
});
Originally the App.init() object method would be called at the bottom of any regular html page, and I have others that do certain things also that would be used on specific pages like Login.init() for the login page and so forth.
I did read that stackoverflow post
"Thinking in AngularJS" if I have a jQuery background? and realize that I am trying to go backwards in a sense, but I want to use this template that I have so I need to retro fit this solution.
I am trying to use this directive on my body tag.
<body ui-view="dashboard-shell" responsive-theme>
<div class="page-container">
<div class="page-sidebar nav-collapse collapse" ng-controller="SidemenuController">
<sidemenu></sidemenu>
</div>
<div class="page-content" ui-view="dashboard">
</div>
</div>
</body>
So here is my problem. This kinda sorta works. I don't get any console errors but when I try to use my side menu which the javascript for it is in the directive it doesn't work until I go inside the console and type App.init(). After that all of the template javascript works. I want to know how to do responsive theme stuff in these directives. I have tried using it both in the compile and link sections. I have tried putting the code in compile and link and calling the $scope.App.init() from a controller and also at the bottom after defining everything. I also tried putting this in jsfiddle but can't show a true example without having the console to call App.init().
My end design would be having some way to switch the pages through ui-router and when a route gets switched it calls the appropriate methods or re-runs the directive or something. The only method that will run on every page is the App.init() method and everything else is really page specific. And technically since this is a single page app the App.init() only needs to run once for the application. I have it tied to a parent template inside ui-router and the pages that will switch all use this shell template. There are some objects that need to access other to call their methods.
Im sorry in advance for maybe a confusing post. I am struggling right now trying to put together some of the ways that you do things from an angular perspective. I will continue to edit the post as I get responses to give further examples.
You said I have read enough about angular that I know DOM manipulations are suppose to go inside a directive but it sounds like you missed the point of a directive. A directive should handle DOM manipulation, yes, but not one directive for the entire page. Each element (or segment) of the page should have its own directive (assuming DOM manip needs to be done on that element) and then the $controller should handle the interactions between those elements and your data (or model).
You've created one gigantic directive and are trying to have it do way too much. Thankfully, you've kinda sorta designed your code in such a way that it shouldn't be too hard to break it up into several directives. Basically, each of your handle functions should be its own directive.
So you'd have something like:
.directive('sidebarMenu', function(){
return {
template: 'path/to/sidebar/partial.html',
link: function(scope, elem, attrs){
// insert the code for your 'handleSidebarMenu()' function here
}
};
})
.directive('horizontalMenu', function(){
return {
template: 'path/to/horizontal/partial.html',
link: function(scope, elem, attrs){
// insert the code for your 'handleHorizontalMenu()' function here
}
};
})
and then your view would look something like:
<body ui-view="dashboard-shell" responsive-theme>
<div class="page-container">
<div class="page-sidebar nav-collapse collapse">
<horizontal-menu></horizontal-menu>
<sidebar-menu></sidebar-menu>
</div>
<div class="page-content" ui-view="dashboard">
</div>
</div>
</body>
And then you don't need a SidebarmenuController because your controller functions shouldn't be handling DOM elements like the sidebar. The controller should just handling the data that you're going to display in your view, and then the view (or .html file) will handle the displaying and manipulation of that data by its use of the directives you've written.
Does that make sense? Just try breaking that huge directive up into many smaller directives that handle specific elements or specific tasks in the DOM.

Categories

Resources