Skip to content
This repository has been archived by the owner on Sep 5, 2024. It is now read-only.

mdMedia improvements #978

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 35 additions & 29 deletions src/core/util/media.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
angular.module('material.core')
.factory('$mdMedia', mdMediaFactory);
.factory('$mdMedia', mdMediaFactory);

/**
* Exposes a function on the '$mdMedia' service which will return true or false,
Expand All @@ -10,42 +10,48 @@ angular.module('material.core')
* @example $mdMedia('(min-width: 1200px)') == true if device-width >= 1200px
* @example $mdMedia('max-width: 300px') == true if device-width <= 300px (sanitizes input, adding parens)
*/
function mdMediaFactory($window, $mdUtil, $timeout, $mdConstant) {
var cache = $mdUtil.cacheFactory('$mdMedia', { capacity: 15 });
function mdMediaFactory($mdConstant, $mdUtil, $rootScope, $window) {
var queriesCache = $mdUtil.cacheFactory('$mdMedia:queries', {capacity: 15});
var resultsCache = $mdUtil.cacheFactory('$mdMedia:results', {capacity: 15});

angular.element($window).on('resize', updateAll);
angular.element($window).on('resize', updateAll);

return $mdMedia;
return $mdMedia;

function $mdMedia(query) {
query = validate(query);
var result;
if (!angular.isDefined(result = cache.get(query)) ) {
return add(query);
}
return result;
function $mdMedia(query) {
var validated = queriesCache.get(query);
if (angular.isUndefined(validated)) {
validated = queriesCache.put(query, validate(query));
}

function validate(query) {
return $mdConstant.MEDIA[query] || (
query.charAt(0) != '(' ? ('(' + query + ')') : query
);
var result = resultsCache.get(validated);
if (angular.isUndefined(result)) {
result = add(validated);
}

function add(query) {
return cache.put(query, !!$window.matchMedia(query).matches);
return result;
}

}
function validate(query) {
return $mdConstant.MEDIA[query] ||
((query.charAt(0) !== '(') ? ('(' + query + ')') : query);
}

function updateAll() {
var keys = cache.keys();
if (keys.length) {
for (var i = 0, ii = keys.length; i < ii; i++) {
cache.put(keys[i], !!$window.matchMedia(keys[i]).matches);
}
// trigger a $digest()
$timeout(angular.noop);
}
}
function add(query) {
return resultsCache.put(query, !!$window.matchMedia(query).matches);
}

function updateAll() {
var keys = resultsCache.keys();
var len = keys.length;

if (len) {
for (var i = 0; i < len; i++) {
add(keys[i]);
}

// Trigger a $digest() if not already in progress
$rootScope.$evalAsync();
}
}
}
72 changes: 64 additions & 8 deletions src/core/util/media.spec.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,81 @@
describe('$mdMedia', function() {
var matchMediaResult;
var queriesCache;
var resultsCache;


beforeEach(module('material.core'));

var matchMediaResult = false;
beforeEach(inject(function($window) {
beforeEach(inject(function($cacheFactory, $mdMedia, $window) {
matchMediaResult = false;

queriesCache = $cacheFactory.get('$mdMedia:queries');
resultsCache = $cacheFactory.get('$mdMedia:results');

spyOn($window, 'matchMedia').andCallFake(function() {
return { matches: matchMediaResult };
return {matches: matchMediaResult};
});
}));

it('should validate input', inject(function($window, $mdMedia) {
afterEach(function() {
queriesCache.removeAll();
resultsCache.removeAll();
});


it('should look up queries in `$mdConstant.MEDIA`', inject(
function($mdConstant, $mdMedia, $window) {
$mdConstant.MEDIA.somePreset = 'someQuery';

$mdMedia('somePreset');
expect($window.matchMedia).toHaveBeenCalledWith('someQuery');

delete($mdConstant.MEDIA.somePreset);
}
));

it('should look up validated queries in `queriesCache`', inject(function($mdMedia, $window) {
queriesCache.put('originalQuery', 'validatedQuery');

$mdMedia('originalQuery');
expect($window.matchMedia).toHaveBeenCalledWith('validatedQuery');
}));

it('should validate queries', inject(function($mdMedia, $window) {
$mdMedia('something');
expect($window.matchMedia).toHaveBeenCalledWith('(something)');
}));

it('should return result of matchMedia and recalculate on resize', inject(function($window, $mdMedia) {
it('should cache validated queries in `queriesCache`', inject(function($mdMedia) {
$mdMedia('query');
expect(queriesCache.get('query')).toBe('(query)');
}));

it('should return cached results if available', inject(function($mdMedia) {
resultsCache.put('(query)', 'result');
expect($mdMedia('(query)')).toBe('result');
}));

it('should cache results in `resultsCache`', inject(function($mdMedia) {
$mdMedia('(query)');
expect(resultsCache.get('(query)')).toBe(false);
}));

it('should recalculate on resize', inject(function($mdMedia, $window) {
matchMediaResult = true;
expect($mdMedia('foo')).toBe(true);
expect($mdMedia('query')).toBe(true);
expect($window.matchMedia.callCount).toBe(1);

expect($mdMedia('query')).toBe(true);
expect($window.matchMedia.callCount).toBe(1);

matchMediaResult = false;
expect($mdMedia('foo')).toBe(true);
expect($mdMedia('query')).toBe(true);
expect($window.matchMedia.callCount).toBe(1);

angular.element($window).triggerHandler('resize');
expect($mdMedia('foo')).toBe(false);

expect($mdMedia('query')).toBe(false);
expect($window.matchMedia.callCount).toBe(2);
}));
});
15 changes: 14 additions & 1 deletion src/core/util/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -266,19 +266,32 @@ angular.module('material.core')
*/
function cacheFactory(id, options) {
var cache = $cacheFactory(id, options);

var keys = {};

cache._put = cache.put;
cache.put = function(k,v) {
keys[k] = true;
return cache._put(k, v);
};

cache._remove = cache.remove;
cache.remove = function(k) {
delete keys[k];
return cache._remove(k);
};

cache._removeAll = cache.removeAll;
cache.removeAll = function() {
keys = {};
return cache._removeAll();
};

cache._destroy = cache.destroy;
cache.destroy = function() {
keys = null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gkalpak - think this is dangerous. Perhaps:

cache._destroy = cache.destroy;
    cache.destroy = function() {
      keys = {};
      return cache._destroy();
    };

is safer and allows subsequent calls to cache.put() to work properly.

Even though semantically the cache.put( ) should not be called after cache.destroy( )

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Subsequent calls to put() will not work correctly anyway, because the original put() will fail after having called destroy(). (put() is not supposed to be called after destroy().)

var cache = $mdUtil.cacheFactory('test');
cache.destroy();
cache.put('key', 'value');   // Throws an error regardless of the value of `keys`

So, having keys = null helps GC (instead of having to keep a reference to an empty object around). But, it's impact should be minimal (even more so considering destroy() is rarely used currently).

return cache._destroy();
};

cache.keys = function() {
return Object.keys(keys);
};
Expand Down