AngularJS-ui toggle accordion depending on window width - angularjs

I am looking to add the accordion functionality programatically when the browser is under a certain width. I thought I might just destroy the accordion when the $window watch reports a width under, say 400px, and re-initiate it again when not. But that seems to be a silly idea after searching for a couple hours. Is there a way to do this or a better way to archive the same result?

Ok so for everyone that might face a similar issue, this is what we ended up doing:
In the Angular config we added a dynamic router:
$routeProvider
.when('/:a', {
template: '<div data-ng-include="templateUrl">Loading...</div>',
controller: 'DynamicController'
})
Then the controller looks like this:
.controller('DynamicController', function ($scope, $rootScope, $routeParams) {
var mobile = false; //set desktop first as mobil has more logic
var maxWidth = 768; //adjust this value in media queries aswell!
var _isMobile = "";
//listen for changes on the scope var windowWidth
$rootScope.$watch('windowWidth',function(){
//check that we have not exceeded our max width
if($rootScope.windowWidth < maxWidth) {
_isMobile = "mobile"; //set the current view based on the width to mobile
}
else {
_isMobile = "desktop"; //set the current view based on the width to desktop
}
$scope.templateUrl = function() {
var temp = (_isMobile ? _isMobile : 'desktop');
return 'views/' + temp + '.html';
}(); //immediate gets called every time the windowWidth var changes passes in the current required view as a string.
});
})
Which has a watch set up for this directive:
angular.module('app.config',[])
.directive('resize', function ($window) {
return {
controller: function ($scope, $rootScope) {
$rootScope.windowWidth = $window.innerWidth;
angular.element($window).bind('resize', function () {
$rootScope.$apply(function () {
$rootScope.windowWidth = $window.innerWidth;
});
});
}
};
});
Now we got two views for mobile and desktop which both draw in the same content but wrap them into either an accordion or in another container.
That seems to work nicely and hope will help some other internet traveler.

Related

Scrollable element in Internet Explorer does not remember last position with ng-show

Scrollable content inside ng-show element forgets the scroll position in Internet Explorer but not in Firefox.
Run the Plunker to describe the issue in Internet Explorer and Firefox. You get different results.
Anyone know why?
I can suggest the following workaround - Save the state of the position on the controller and use it to reset the location:
$scope.position = "top"; // Default location of the list
$scope.show = true;
$scope.gotoBottom = function() {
$location.hash('bottom');
$scope.position = "bottom"; // save the current position
$anchorScroll();
};
$scope.gotoTop = function() {
$location.hash('top');
$scope.position = "top"; // save the current position
$anchorScroll();
};
$scope.toggleShow = function() {
$scope.show = !$scope.show;
if( $scope.position == "bottom" ) { // If position is "bottom" call "$scope.gotoBottom()" to reset the position
$timeout(function() { // The code is inside "$timeout" to allow the view to render before updating the location
$scope.gotoBottom();
});
}
};
And on the view, change how you showing/hiding the list:
<button ng-click="toggleShow()">{{show ? 'Show' : 'Hide'}}</button>
And don't forget to inject $timeout into your controller:
.controller('ScrollController', ['$scope', '$location', '$anchorScroll', '$timeout',
function ($scope, $location, $anchorScroll, $timeout) {
Here is a working example - http://plnkr.co/edit/oXKpmwQtV8ICRvGNeQby?p=preview
Solved using css visibility : hidden instead of ng-show clue from #Alon Etian's link IE10 reset the scrollbars (position to top-left) for a block (overflow:auto) after {display:none, display:block} sequence in his comment.
Plunkr that solves this

ng-hide & ng-show in leaflet legend

I tried to create a leaflet legend using the 'ng-show' and 'ng-hide' attributes.
Unfortunately, the legend is not created on site load but on map load.
The attributes don't seem to work if they are added with javascript directly.
This code:
onAdd: function() {
var controlDiv = L.DomUtil.create('div', 'air-quality-legend');
controlDiv.setAttribute('ng-hide', 'true');
controlDiv.className = "airQualityIndex";
L.DomEvent
.addListener(controlDiv, 'click', L.DomEvent.stopPropagation)
.addListener(controlDiv, 'click', L.DomEvent.preventDefault);
var table = document.createElement('table');
var tr = document.createElement('table');
var td = document.createElement('table');
td.innerHTML = "test";
tr.appendChild(td);
table.appendChild(tr);
controlDiv.appendChild(table);
return controlDiv;
}
Produces that output.
As described there is a table when there should not.
Is there any way to add 'ng-hide' or 'ng-show' via javascript on runtime?
Thank you for your help in advance.
You'll need to compile the DOM of your custom control. To do that, you'll need to inject $compile into your controller, then after having added the control to your map use the getContainer method on your control instance and run $compile on it and attach it to the scope:
Control:
L.Control.Custom = L.Control.extend({
onAdd: function () {
var container = L.DomUtil.create('div', 'leaflet-control-custom')
header = L.DomUtil.create('h1', 'leaflet-control-custom-header', container);
header.textContent = 'NG-Hide test';
header.setAttribute('ng-hide', 'hide');
return container;
}
});
Controller:
angular.module('app').controller('controller', [
'$scope', 'leaflet', '$compile',
function ($scope, leaflet, $compile) {
$scope.hide = false;
leaflet.map.then(function (map) {
var control = new L.Control.Custom().addTo(map);
$compile(control.getContainer())($scope);
});
}
]);
Here's a working example on Plunker: http://plnkr.co/edit/xzRwTp9OZ8Zp8v7ktt2c?p=preview

How to properly unit test directives with DOM manipulation?

Before asking my real question, I have a different one... Does it make sense to unit test DOM manipulation in Angular directives?
For instance, here's my complete linking function:
function linkFn(scope, element) {
var ribbon = element[0];
var nav = ribbon.children[0];
scope.ctrl.ribbonItemClick = function (index) {
var itemOffsetLeft;
var itemOffsetRight;
var item;
if (scope.ctrl.model.selectedIndex === index) {
return;
}
scope.ctrl.model.selectedIndex = index;
item = nav.querySelectorAll('.item')[index];
itemOffsetLeft = item.offsetLeft - ribbon.offsetLeft;
itemOffsetRight = itemOffsetLeft + item.clientWidth;
if (itemOffsetLeft < nav.scrollLeft) {
nav.scrollLeft = itemOffsetLeft - MAGIC_PADDING;
}
if(itemOffsetRight > nav.clientWidth + nav.scrollLeft) {
nav.scrollLeft = itemOffsetRight - nav.clientWidth + MAGIC_PADDING;
}
this.itemClick({
item: scope.ctrl.model.items[index],
index: index
});
$location.path(scope.ctrl.model.items[index].href);
};
$timeout(function $timeout() {
var item = nav.querySelector('.item.selected');
nav.scrollLeft = item.offsetLeft - ribbon.offsetLeft - MAGIC_PADDING;
});
}
This is for a scrollable tabbed component and I have no idea how to test the 3 instances of nav.scrollLeft = x.
The first two if statements happen when an item - which is only partially visible - is clicked. The left/right (each if) item will be snapped to the left/right border of the component.
The third one, is to place the selected item in view if it's not visible when the component is loaded.
How do I unit test this with Karma/Jasmine. does it even make sense to do it or should I do functional tests with Protractor instead?
When testing directives, look for things that set or return explicit values. These are generally easy to assert and it makes sense to unit test them with Jasmine and Karma.
Take a look at Angular's tests for ng-src. Here, they test that the directive works by asserting that the src attribute on the element gets set to the right values. It is explicit: either the src attribute has a specific value or it does not.
it('should not result empty string in img src', inject(function($rootScope, $compile) {
$rootScope.image = {};
element = $compile('<img ng-src="{{image.url}}">')($rootScope);
$rootScope.$digest();
expect(element.attr('src')).not.toBe('');
expect(element.attr('src')).toBe(undefined);
}));
The same with ng-bind. Here, they pass a string of HTML with to the $compiler and then assert that the return value has had its HTML populated with actual scope values. Again, it is explicit.
it('should set text', inject(function($rootScope, $compile) {
element = $compile('<div ng-bind="a"></div>')($rootScope);
expect(element.text()).toEqual('');
$rootScope.a = 'misko';
$rootScope.$digest();
expect(element.hasClass('ng-binding')).toEqual(true);
expect(element.text()).toEqual('misko');
}));
When you get into more complicated scenarios like testing against viewport visibility or testing whether specific elements are positioned in the right places on the page, you could try to test that CSS and style attributes get set properly, but that gets fiddly real quick and is not recommended. At this point you should be looking at Protractor or a similar e2e testing tool.
I would 100% want to test all the paths of your directive even if it isn't the easiest thing. But there are approaches you can take to make this process simpler.
Break Complicated Logic into Service
The first thing that stands out to me is the complicated piece of logic about setting the nav scrollLeft. Why not break this into a separate service that can be unit tested on its own?
app.factory('AutoNavScroller', function() {
var MAGIC_PADDING;
MAGIC_PADDING = 25;
return function(extraOffsetLeft) {
this.getScrollPosition = function(item, nav) {
var itemOffsetLeft, itemOffsetRight;
itemOffsetLeft = item.offsetLeft - extraOffsetLeft;
itemOffsetRight = itemOffsetLeft + item.clientWidth;
if ( !!nav && itemOffsetRight > nav.clientWidth + nav.scrollLeft) {
return itemOffsetRight - nav.clientWidth + MAGIC_PADDING;
} else {
return itemOffsetLeft - MAGIC_PADDING;
}
};
}
});
This makes it much easier to test all the paths and refactor (which you can see I was able to do above. The tests can be seen below:
describe('AutoNavScroller', function() {
var AutoNavScroller;
beforeEach(module('app'));
beforeEach(inject(function(_AutoNavScroller_) {
AutoNavScroller = _AutoNavScroller_;
}));
describe('#getScrollPosition', function() {
var scroller, item;
function getScrollPosition(nav) {
return scroller.getScrollPosition(item, nav);
}
beforeEach(function() {
scroller = new AutoNavScroller(50);
item = {
offsetLeft: 100
};
})
describe('with setting initial position', function() {
it('gets the initial scroll position', function() {
expect(getScrollPosition()).toEqual(25);
});
});
describe('with item offset left of the nav scroll left', function() {
it('gets the scroll position', function() {
expect(getScrollPosition({
scrollLeft: 100
})).toEqual(25);
});
});
describe('with item offset right of the nav width and scroll left', function() {
beforeEach(function() {
item.clientWidth = 300;
});
it('gets the scroll position', function() {
expect(getScrollPosition({
scrollLeft: 25,
clientWidth: 50
})).toEqual(325);
});
});
});
});
Test That Directive is Calling Service
Now that we've broken up our directive, we can just inject the service and make sure it is called correctly.
app.directive('ribbonNav', function(AutoNavScroller, $timeout) {
return {
link: function(scope, element) {
var navScroller;
var ribbon = element[0];
var nav = ribbon.children[0];
// Assuming ribbon offsetLeft remains the same
navScroller = new AutoNavScroller(ribbon.offsetLeft);
scope.ctrl.ribbonItemClick = function (index) {
if (scope.ctrl.model.selectedIndex === index) {
return;
}
scope.ctrl.model.selectedIndex = index;
item = nav.querySelectorAll('.item')[index];
nav.scrollLeft = navScroller.getScrollLeft(item, nav);
// ...rest of directive
};
$timeout(function $timeout() {
var item = nav.querySelector('.item.selected');
// Sets initial nav scroll left
nav.scrollLeft = navScroller.getScrollLeft(item);
});
}
}
});
The easiest way to make sure our directive keeps using the service, is to just spy on the methods that it will call and make sure they are receiving the correct parameters:
describe('ribbonNav', function() {
var $compile, $el, $scope, AutoNavScroller;
function createRibbonNav() {
$el = $compile($el)($scope);
angular.element(document)
$scope.$digest();
document.body.appendChild($el[0]);
}
beforeEach(module('app'));
beforeEach(module(function ($provide) {
AutoNavScroller = jasmine.createSpy();
AutoNavScroller.prototype.getScrollLeft = function(item, nav) {
return !nav ? 50 : 100;
};
spyOn(AutoNavScroller.prototype, 'getScrollLeft').and.callThrough();
$provide.provider('AutoNavScroller', function () {
this.$get = function () {
return AutoNavScroller;
}
});
}));
beforeEach(inject(function(_$compile_, $rootScope) {
$compile = _$compile_;
$el = "<div id='ribbon_nav' ribbon-nav><div style='width:50px;overflow:scroll;float:left;'><div class='item selected' style='height:100px;width:200px;float:left;'>An Item</div><div class='item' style='height:100px;width:200px;float:left;'>An Item</div></div></div>";
$scope = $rootScope.$new()
$scope.ctrl = {
model: {
selectedIndex: 0
}
};
createRibbonNav();
}));
afterEach(function() {
document.getElementById('ribbon_nav').remove();
});
describe('on link', function() {
it('calls AutoNavScroller with selected item', inject(function($timeout) {
expect(AutoNavScroller).toHaveBeenCalledWith(0);
}));
it('calls AutoNavScroller with selected item', inject(function($timeout) {
$timeout.flush();
expect(AutoNavScroller.prototype.getScrollLeft)
.toHaveBeenCalledWith($el[0].children[0].children[0]);
}));
it('sets the initial nav scrollLeft', inject(function($timeout) {
$timeout.flush();
expect($el[0].children[0].scrollLeft).toEqual(50);
}));
});
describe('ribbonItemClick', function() {
beforeEach(function() {
$scope.ctrl.ribbonItemClick(1);
});
it('calls AutoNavScroller with item', inject(function($timeout) {
expect(AutoNavScroller.prototype.getScrollLeft)
.toHaveBeenCalledWith($el[0].children[0].children[1], $el[0].children[0]);
}));
it('sets the nav scrollLeft', function() {
expect($el[0].children[0].scrollLeft).toEqual(100);
});
});
});
Now, obviously these specs can be refactored a 100 ways but you can see that a higher coverage is much easier to achieve once we started breaking out the complicated logic. There are some risks around mocking objects too much because it can make your tests brittle but I believe the tradeoff is worth it here. Plus I can definitely see that AutoNavScroller being generalized and reused elsewhere. That would not have been possible if the code existed in the directive before.
Conclusion
Anyways, the reason why I believe Angular is great is the ability to test these directives and how they interact with the DOM. These jasmine specs can be run in any browser and will quickly surface inconsistencies or regressions.
Also, here is a plunkr so you can see all the moving pieces and experiment: http://plnkr.co/edit/wvj4TmmJtxTG0KW7v9rn?p=preview

Angular: how to call a function after changing state

I am building a small message system. I use tabs in my state (inbox, outbox). Also, i want to sent a message when i click a "contact" link. If i click that link, this should happen:
change state to messages state
open other tab, called "newmsg"
At this moment, this is what i have:
<a ng-click="mailContact()">contact</a>
and i my controller:
$scope.mailContact = function() {
$state.go('root.messages');
$scope.openTab('new');
};
Obviously this is not working, because $scope.openTab('new'); will never execute. The state changes to what i want, but the tab is not opened. I do not have a clue of how to get it done.
Ok, stupid thing was i had an init which opened the "inbox"...
Now i wrote a service which does the trick.
app.factory('msgTabService', ['$rootScope', function($rootScope) {
var msgTabService = {};
var tabname = "inbox";
msgTabService.set = function(tab) {
tabname = tab;
};
msgTabService.get = function() {
return tabname;
};
msgTabService.openTab = function(tab) {
if ($rootScope.currenttab !== tab)
{
$rootScope.currenttab = tab;
msgTabService.set(tab);
}
};
return msgTabService;
}]);
The question may be similar to: Save State of Tab content when changing Route with angularjs and BootStrap Tabs

Access Element Style from Angular directive

I'm sure this is going to be a "dont do that!" but I am trying to display the style on an angular element.
<div ng-repeat="x in ['blue', 'green']" class="{{x}}">
<h3 insert-style>{{theStyle['background-color']}}</h3>
</div>
Result would be
<div class='blue'><h3>blue(psudeo code hex code)</h3></div>
<div class='green'><h3>green(psudeo code hex code)</h3></div>
I basically need to get the style attributes and display them.
Directive Code...
directives.insertStyle = [ function(){
return {
link: function(scope, element, attrs) {
scope.theStyle = window.getComputedStyle(element[0], null);
}
}
}];
Fiddle example: http://jsfiddle.net/ncapito/G33PE/
My final solution (using a single prop didn't work, but when I use the whole obj it works fine)...
Markup
<div insert-style class="box blue">
<h4 > {{ theStyle['color'] | toHex}} </h4>
</div>
Directive
directives.insertStyle = [ "$window", function($window){
return {
link: function(scope, element, attrs) {
var elementStyleMap = $window.getComputedStyle(element[0], null);
scope.theStyle = elementStyleMap
}
}
}];
Eureka!
http://jsfiddle.net/G33PE/5/
var leanwxApp = angular.module('LeanwxApp', [], function () {});
var controllers = {};
var directives = {};
directives.insertStyle = [ function(){
return {
link: function(scope, element, attrs) {
scope.theStyle = window.getComputedStyle(element[0].parentElement, null)
}
}
}];
leanwxApp.controller(controllers);
leanwxApp.directive(directives);
So that just took lots of persistence and guessing. Perhaps the timeout is unnecessary but while debugging it seemed I only got the style value from the parent after the timeout occurred.
Also I'm not sure why but I had to go up to the parentElement to get the style (even though it would realistically be inherited (shrug)?)
Updated fiddle again
Did one without the timeout but just looking at the parentElement for the style and it seems to still work, so scratch the suspicions about the style not being available at all, it's just not available where I would expect it.
Also holy cow there are a lot of ways to debug in Chrome:
https://developers.google.com/chrome-developer-tools/docs/javascript-debugging
I used
debugger;
statements in the code to drop in breakpoints without having to search all the fiddle files.
One more quick update
The code below comes out of Boostrap-UI from the AngularUI team and claims to provide a means to watch the appropriate events (haven't tried this but it looks like it should help).
http://angular-ui.github.io/bootstrap/
/**
* $transition service provides a consistent interface to trigger CSS 3 transitions and to be informed when they complete.
* #param {DOMElement} element The DOMElement that will be animated.
* #param {string|object|function} trigger The thing that will cause the transition to start:
* - As a string, it represents the css class to be added to the element.
* - As an object, it represents a hash of style attributes to be applied to the element.
* - As a function, it represents a function to be called that will cause the transition to occur.
* #return {Promise} A promise that is resolved when the transition finishes.
*/
.factory('$transition', ['$q', '$timeout', '$rootScope', function($q, $timeout, $rootScope) {
var $transition = function(element, trigger, options) {
options = options || {};
var deferred = $q.defer();
var endEventName = $transition[options.animation ? "animationEndEventName" : "transitionEndEventName"];
var transitionEndHandler = function(event) {
$rootScope.$apply(function() {
element.unbind(endEventName, transitionEndHandler);
deferred.resolve(element);
});
};
if (endEventName) {
element.bind(endEventName, transitionEndHandler);
}
// Wrap in a timeout to allow the browser time to update the DOM before the transition is to occur
$timeout(function() {
if ( angular.isString(trigger) ) {
element.addClass(trigger);
} else if ( angular.isFunction(trigger) ) {
trigger(element);
} else if ( angular.isObject(trigger) ) {
element.css(trigger);
}
//If browser does not support transitions, instantly resolve
if ( !endEventName ) {
deferred.resolve(element);
}
});
// Add our custom cancel function to the promise that is returned
// We can call this if we are about to run a new transition, which we know will prevent this transition from ending,
// i.e. it will therefore never raise a transitionEnd event for that transition
deferred.promise.cancel = function() {
if ( endEventName ) {
element.unbind(endEventName, transitionEndHandler);
}
deferred.reject('Transition cancelled');
};
return deferred.promise;
};
// Work out the name of the transitionEnd event
var transElement = document.createElement('trans');
var transitionEndEventNames = {
'WebkitTransition': 'webkitTransitionEnd',
'MozTransition': 'transitionend',
'OTransition': 'oTransitionEnd',
'transition': 'transitionend'
};
var animationEndEventNames = {
'WebkitTransition': 'webkitAnimationEnd',
'MozTransition': 'animationend',
'OTransition': 'oAnimationEnd',
'transition': 'animationend'
};
function findEndEventName(endEventNames) {
for (var name in endEventNames){
if (transElement.style[name] !== undefined) {
return endEventNames[name];
}
}
}
$transition.transitionEndEventName = findEndEventName(transitionEndEventNames);
$transition.animationEndEventName = findEndEventName(animationEndEventNames);
return $transition;
}]);
The issue you'll face is that getComputedStyle is considered a very slow running method, so you will run into performance issues if using that, especially if you want angularjs to update the view whenever getComputedStyle changes.
Also, getComputedStyle will resolve every single style declaration possible, which i think will not be very useful. So i think a method to reduce the number of possible style is needed.
Definitely consider this an anti-pattern, but if you still insist in this foolishness:
module.directive('getStyleProperty', function($window){
return {
//Child scope so properties are not leaked to parent
scope : true,
link : function(scope, element, attr){
//A map of styles you are interested in
var styleProperties = ['text', 'border'];
scope.$watch(function(){
//A watch function to get the styles
//Since this runs every single time there is an angularjs loop, this would not be a very performant way to do this
var obj = {};
var computedStyle = $window.getComputedStyle(element[0]);
angular.forEach(styleProperties, function(value){
obj[value] = computedStyle.getPropertyValue(value);
});
return obj;
}, function(newValue){
scope.theStyle = newValue;
});
}
}
});
This solution works if you don't HAVE to have the directive on the child element. If you just place the declaration on the ng-repeat element itself, your solution works:
<div insert-style ng-repeat="x in ['blue', 'green']" class="{{x}}">
Fiddle

Resources