From 603e5d68623dda4003917989e752fda4e603f36a Mon Sep 17 00:00:00 2001 From: Thomas Burleson Date: Fri, 13 Feb 2015 11:53:24 -0600 Subject: [PATCH] fix(icon): improve error recovery and item caching Merged additional unit tests and announce error methods Improved caching of Icon items - Only prepareIcon styles 1x - Add xmlns name space attribute to SVG (if not present). Restored simplified icon - demoBasic version Resolved conflicts of different components using different default icon sets. Closes #1477. --- .../gridList/demoBasicUsage/index.html | 56 ++++++-- .../gridList/demoBasicUsage/script.js | 58 +------- .../gridList/demoBasicUsage/styles.css | 13 ++ .../gridList/demoDynamicTiles/index.html | 18 +++ .../gridList/demoDynamicTiles/script.js | 55 ++++++++ .../style.scss | 0 src/components/icon/demoSvgIconSets/script.js | 5 +- .../icon/demoUsingTemplateCache/script.js | 5 +- .../{icon.spec.js => iconDirective.spec.js} | 118 ++++++++-------- src/components/icon/iconService.js | 98 +++++++++----- src/components/icon/iconService.spec.js | 127 ++++++++++++++++++ .../radioButton/demoBasicUsage/index.html | 2 + .../radioButton/demoBasicUsage/script.js | 8 +- 13 files changed, 388 insertions(+), 175 deletions(-) create mode 100644 src/components/gridList/demoBasicUsage/styles.css create mode 100644 src/components/gridList/demoDynamicTiles/index.html create mode 100644 src/components/gridList/demoDynamicTiles/script.js rename src/components/gridList/{demoBasicUsage => demoDynamicTiles}/style.scss (100%) rename src/components/icon/{icon.spec.js => iconDirective.spec.js} (54%) create mode 100644 src/components/icon/iconService.spec.js diff --git a/src/components/gridList/demoBasicUsage/index.html b/src/components/gridList/demoBasicUsage/index.html index 4d3e476619f..1e948899a97 100644 --- a/src/components/gridList/demoBasicUsage/index.html +++ b/src/components/gridList/demoBasicUsage/index.html @@ -1,18 +1,48 @@ -
+
+ - + + +

#1: (3r x 2c)

+
+
- - -

{{tile.title}}

-
+ + +

#2: (1r x 1c)

+
+
-
+ + +

#3: (1r x 1c)

+
+
+ + +

#4: (2r x 1c)

+
+
+ + + +

#5: (2r x 2c)

+
+
+ + + +

#6: (2r x 1c)

+
+
+ +
diff --git a/src/components/gridList/demoBasicUsage/script.js b/src/components/gridList/demoBasicUsage/script.js index 85a6a294462..0798439a1a8 100644 --- a/src/components/gridList/demoBasicUsage/script.js +++ b/src/components/gridList/demoBasicUsage/script.js @@ -1,57 +1,3 @@ -angular - .module('gridListDemoApp', ['ngMaterial']) - .controller('gridListDemoCtrl', function($scope) { - - this.tiles = buildGridModel({ - icon : "avatar:svg-", - title: "Svg-", - background: "" - }); - - function buildGridModel(tileTmpl){ - var it, results = [ ]; - - for (var j=0; j<11; j++) { - - it = angular.extend({},tileTmpl); - it.icon = it.icon + (j+1); - it.title = it.title + (j+1); - it.span = { row : "1", col : "1" }; - - switch(j+1) { - case 1: - it.background = "red"; - it.span.row = it.span.col = 2; - break; - - case 2: it.background = "green"; break; - case 3: it.background = "darkBlue"; break; - case 4: - it.background = "blue"; - it.span.col = 2; - break; - - case 5: - it.background = "yellow"; - it.span.row = it.span.col = 2; - break; - - case 6: it.background = "pink"; break; - case 7: it.background = "darkBlue"; break; - case 8: it.background = "purple"; break; - case 9: it.background = "deepBlue"; break; - case 10: it.background = "lightPurple"; break; - case 11: it.background = "yellow"; break; - } - - results.push(it); - } - return results; - } - }) - .config( function( $mdIconProvider ){ - $mdIconProvider - .iconSet("avatar", './icons/avatar-icons.svg') - .defaultIconSize(128); - }); +angular.module('gridListDemo1', ['ngMaterial']) +.controller('AppCtrl', function($scope) {}); diff --git a/src/components/gridList/demoBasicUsage/styles.css b/src/components/gridList/demoBasicUsage/styles.css new file mode 100644 index 00000000000..e513cb6ecc1 --- /dev/null +++ b/src/components/gridList/demoBasicUsage/styles.css @@ -0,0 +1,13 @@ +md-grid-list { margin: 8px; } +.gray { background: #f5f5f5; } +.green { background: #b9f6ca; } +.yellow { background: #ffff8d; } +.blue { background: #84ffff; } +.purple { background: #b388ff; } +.red { background: #ff8a80; } + + + +md-grid-tile { + transition: all 700ms ease-out 50ms; +} diff --git a/src/components/gridList/demoDynamicTiles/index.html b/src/components/gridList/demoDynamicTiles/index.html new file mode 100644 index 00000000000..4d3e476619f --- /dev/null +++ b/src/components/gridList/demoDynamicTiles/index.html @@ -0,0 +1,18 @@ +
+ + + + + +

{{tile.title}}

+
+ +
+ +
diff --git a/src/components/gridList/demoDynamicTiles/script.js b/src/components/gridList/demoDynamicTiles/script.js new file mode 100644 index 00000000000..71116d26056 --- /dev/null +++ b/src/components/gridList/demoDynamicTiles/script.js @@ -0,0 +1,55 @@ + +angular + .module('gridListDemoApp', ['ngMaterial']) + .controller('gridListDemoCtrl', function($scope) { + + this.tiles = buildGridModel({ + icon : "avatar:svg-", + title: "Svg-", + background: "" + }); + + function buildGridModel(tileTmpl){ + var it, results = [ ]; + + for (var j=0; j<11; j++) { + + it = angular.extend({},tileTmpl); + it.icon = it.icon + (j+1); + it.title = it.title + (j+1); + it.span = { row : "1", col : "1" }; + + switch(j+1) { + case 1: + it.background = "red"; + it.span.row = it.span.col = 2; + break; + + case 2: it.background = "green"; break; + case 3: it.background = "darkBlue"; break; + case 4: + it.background = "blue"; + it.span.col = 2; + break; + + case 5: + it.background = "yellow"; + it.span.row = it.span.col = 2; + break; + + case 6: it.background = "pink"; break; + case 7: it.background = "darkBlue"; break; + case 8: it.background = "purple"; break; + case 9: it.background = "deepBlue"; break; + case 10: it.background = "lightPurple"; break; + case 11: it.background = "yellow"; break; + } + + results.push(it); + } + return results; + } + }) + .config( function( $mdIconProvider ){ + $mdIconProvider.iconSet("avatar", './icons/avatar-icons.svg', 128); + }); diff --git a/src/components/gridList/demoBasicUsage/style.scss b/src/components/gridList/demoDynamicTiles/style.scss similarity index 100% rename from src/components/gridList/demoBasicUsage/style.scss rename to src/components/gridList/demoDynamicTiles/style.scss diff --git a/src/components/icon/demoSvgIconSets/script.js b/src/components/icon/demoSvgIconSets/script.js index ef35f4bd65b..e35ce140810 100644 --- a/src/components/icon/demoSvgIconSets/script.js +++ b/src/components/icon/demoSvgIconSets/script.js @@ -2,6 +2,7 @@ angular.module('appSvgIconSets', ['ngMaterial']) .controller('DemoCtrl', function($scope) {}) .config(function($mdIconProvider) { - $mdIconProvider.iconSet('social', 'img/icons/sets/social-icons.svg') - .defaultIconSet('img/icons/sets/core-icons.svg'); + $mdIconProvider + .iconSet('social', 'img/icons/sets/social-icons.svg', 24) + .defaultIconSet('img/icons/sets/core-icons.svg', 24); }); diff --git a/src/components/icon/demoUsingTemplateCache/script.js b/src/components/icon/demoUsingTemplateCache/script.js index 809109ed893..0da8fc846ea 100644 --- a/src/components/icon/demoUsingTemplateCache/script.js +++ b/src/components/icon/demoUsingTemplateCache/script.js @@ -6,8 +6,9 @@ angular.module('appUsingTemplateCache', ['ngMaterial']) // Register icon IDs with sources. Future $mdIcon( ) lookups // will load by url and retrieve the data via the $http and $templateCache - $mdIconProvider.iconSet('core', 'img/icons/sets/core-icons.svg') - .icon('social:cake', 'img/icons/cake.svg'); + $mdIconProvider + .iconSet('core', 'img/icons/sets/core-icons.svg',24) + .icon('social:cake', 'img/icons/cake.svg',24); }) .run(function($http, $templateCache) { diff --git a/src/components/icon/icon.spec.js b/src/components/icon/iconDirective.spec.js similarity index 54% rename from src/components/icon/icon.spec.js rename to src/components/icon/iconDirective.spec.js index bde793056aa..2aa35452a68 100644 --- a/src/components/icon/icon.spec.js +++ b/src/components/icon/iconDirective.spec.js @@ -1,59 +1,84 @@ describe('mdIcon directive', function() { var el; + var $scope; + var $compile; + var $q; beforeEach(module('material.core')); - beforeEach(module('material.components.icon',function($mdIconProvider){ - $mdIconProvider - .icon('android' , 'android.svg') - .iconSet('social', 'social.svg' ) - .defaultIconSet('core.svg'); - })); + beforeEach(module('material.components.icon')); + + var mockIconSvc = function(id) { + var deferred = $q.defer(); + switch(id) { + case 'android': + deferred.resolve(''); + break; + case 'cake': + deferred.resolve(''); + break; + case 'android.svg': + deferred.resolve(''); + break; + case 'cake.svg': + deferred.resolve(''); + break; + } + return deferred.promise; + } - beforeEach(inject(function($templateCache){ + function make(html) { + var el; + el = $compile(html)($scope); + $scope.$digest(); + return el; + } - $templateCache.put('android.svg', ''); - $templateCache.put('social.svg' , ''); - $templateCache.put('core.svg' , ''); + beforeEach(function() { + module(function($provide) { + $provide.value('$mdIcon', mockIconSvc); + }); - })); + inject(function($rootScope, _$compile_, _$q_){ + $scope = $rootScope; + $compile = _$compile_; + $q = _$q_; + }); + }); describe('using md-font-icon=""', function() { + it('should render correct HTML with md-font-icon value as class', function() { el = make( ''); expect(el.html()).toEqual(''); }); - }); + }); describe('using md-svg-icon=""', function() { - it('should append configured SVG single icon', function() { - el = make(''); - var expected = updateDefaults(''); - expect(el.html()).toEqual(expected); - }); - - it('should append configured SVG icon from named group', function() { - el = make(''); - var expected = updateDefaults(''); - expect(el.html()).toEqual(expected); - }); - - it('should append configured SVG icon from default group', function() { - el = make(''); - var expected = updateDefaults(''); - expect(el.html()).toEqual(expected); + it('should update mdSvgIcon when attribute value changes', function() { + $scope.iconName = 'android'; + el = make(''); + var iScope = el.isolateScope(); + expect(iScope.svgIcon).toEqual('android'); + $scope.iconName = 'cake'; + $scope.$digest(); + expect(iScope.svgIcon).toEqual('cake'); }); }); - describe('using md-svg-src=""', function() { - it('should append SVG from URL to md-icon', function() { - el = make(''); - expect(el.html()).toEqual( updateDefaults('') ); + it('should update mdSvgSrc when attribute value changes', function() { + $scope.url = 'android.svg'; + el = make(''); + var iScope = el.isolateScope(); + expect(iScope.svgSrc).toEqual('android.svg'); + $scope.url = 'cake.svg'; + $scope.$digest(); + expect(iScope.svgSrc).toEqual('cake.svg'); }); }); @@ -100,33 +125,4 @@ describe('mdIcon directive', function() { }); - - function make(html) { - var el; - inject(function($compile, $rootScope) { - el = $compile(html)($rootScope); - $rootScope.$digest(); - }); - - return el; - } - - function updateDefaults(svg) { - svg = angular.element(svg); - - svg.attr({ - 'fit' : '', - 'height': '100%', - 'width' : '100%', - 'preserveAspectRatio': 'xMidYMid meet', - 'viewBox' : svg.attr('viewBox') || '0 0 24 24' - }) - .css( { - 'pointer-events' : 'none', - 'display' : 'block' - }); - - return svg[0].outerHTML; - } - }); diff --git a/src/components/icon/iconService.js b/src/components/icon/iconService.js index 0f9267d6059..3328aebd2a6 100644 --- a/src/components/icon/iconService.js +++ b/src/components/icon/iconService.js @@ -201,6 +201,9 @@ if ( !config[setName] ) { config[setName] = new ConfigurationItem(url, iconSize ); } + + config[setName].iconSize = iconSize || config.defaultIconSize; + return this; }, @@ -267,6 +270,8 @@ var iconCache = {}; var urlRegex = /[-a-zA-Z0-9@:%_\+.~#?&//=]{2,256}\.[a-z]{2,4}\b(\/[-a-zA-Z0-9@:%_\+.~#?&//=]*)?/i; + Icon.prototype = { clone : cloneSVG, prepare: prepareAndStyle }; + return function getIcon(id) { id = id || ''; @@ -279,6 +284,7 @@ return loadByID(id) .catch(loadFromIconSet) + .catch(announceIdNotFound) .catch(announceNotFound) .then( cacheIcon(id) ); }; @@ -289,14 +295,9 @@ function cacheIcon( id ) { return function updateCache( icon ) { - var iconConfig = config[id]; - - icon = !isIcon(icon) ? new Icon(icon, iconConfig) : icon; - icon = prepareAndStyle(icon); - - iconCache[id] = icon; + iconCache[id] = isIcon(icon) ? icon : new Icon(icon, config[id]); - return icon.clone(); + return iconCache[id].clone(); }; } @@ -329,27 +330,6 @@ } } - /** - * Prepare the DOM element that will be cached in the - * loaded iconCache store. - */ - function prepareAndStyle(icon) { - var iconSize = icon.config ? icon.config.iconSize : config.defaultIconSize; - var svg = angular.element(icon.element); - - return svg.attr({ - 'fit' : '', - 'height': '100%', - 'width' : '100%', - 'preserveAspectRatio': 'xMidYMid meet', - 'viewBox' : svg.attr('viewBox') || ('0 0 ' + iconSize + ' ' + iconSize) - }) - .css( { - 'pointer-events' : 'none', - 'display' : 'block' - }); - } - /** * Load the icon by URL (may use the $templateCache). * Extract the data for later conversion to Icon @@ -372,12 +352,26 @@ * User did not specify a URL and the ID has not been registered with the $mdIcon * registry */ - function announceNotFound(id) { - var msg = 'icon ' + id + ' not found'; - $log.warn(msg); - throw new Error(msg); + function announceIdNotFound(id) { + var msg; + + if (angular.isString(id)) { + msg = 'icon ' + id + ' not found'; + $log.warn(msg); + } + + return $q.reject(msg || id); } + /** + * Catch HTTP or generic errors not related to incorrect icon IDs. + */ + function announceNotFound(err) { + var msg = angular.isString(err) ? err : (err.message || err.data || err.statusText); + $log.warn(msg); + + return $q.reject(msg); + } /** * Check target signature to see if it is an Icon instance. @@ -386,7 +380,6 @@ return angular.isDefined(target.element) && angular.isDefined(target.config); } - /** * Define the Icon class */ @@ -394,16 +387,47 @@ if (el.tagName != 'svg') { el = angular.element('').append(el)[0]; } + el = angular.element(el); + + // Inject the namespace if not available... + if ( !el.attr('xmlns') ) { + el.attr('xmlns', "http://www.w3.org/2000/svg"); + } this.element = el; this.config = config; + this.prepare(); } - // Clone the Icon DOM element; which is stored as an angular.element() + /** + * Prepare the DOM element that will be cached in the + * loaded iconCache store. + */ + function prepareAndStyle() { + var iconSize = this.config ? this.config.iconSize : config.defaultIconSize; + var svg = angular.element( this.element ); + svg.attr({ + 'fit' : '', + 'height': '100%', + 'width' : '100%', + 'preserveAspectRatio': 'xMidYMid meet', + 'viewBox' : svg.attr('viewBox') || ('0 0 ' + iconSize + ' ' + iconSize) + }) + .css( { + 'pointer-events' : 'none', + 'display' : 'block' + }); + + this.element = svg; + } + + /** + * Clone the Icon DOM element; which is stored as an angular.element() + */ + function cloneSVG(){ + return angular.element( this.element[0].cloneNode(true) ); + } - Icon.prototype.clone = function (){ - return this.element.cloneNode(true)[0]; - }; } })(); diff --git a/src/components/icon/iconService.spec.js b/src/components/icon/iconService.spec.js new file mode 100644 index 00000000000..5ed1865d4d4 --- /dev/null +++ b/src/components/icon/iconService.spec.js @@ -0,0 +1,127 @@ +describe('mdIcon service', function() { + + var $mdIcon; + var $httpBackend; + var $scope; + + beforeEach(module('material.core')); + beforeEach(module('material.components.icon',function($mdIconProvider){ + $mdIconProvider + .icon('android', 'android.svg') + .icon('c2', 'c2.svg') + .iconSet('social', 'social.svg' ) + .iconSet('notfound', 'notfoundgroup.svg' ) + .defaultIconSet('core.svg'); + })); + + beforeEach(inject(function($templateCache, _$httpBackend_, _$mdIcon_, $rootScope){ + $mdIcon = _$mdIcon_; + $httpBackend = _$httpBackend_; + $scope = $rootScope; + $templateCache.put('android.svg', ''); + $templateCache.put('social.svg' , ''); + $templateCache.put('core.svg' , ''); + $templateCache.put('c2.svg' , ''); + + $httpBackend.whenGET('notfoundgroup.svg').respond(404, 'Cannot GET notfoundgroup.svg'); + + })); + + describe('when $mdIcon() is passed and icon ID', function() { + + it('should append configured SVG single icon', function() { + var expected = updateDefaults(''); + $mdIcon('android').then(function(el) { + expect(el[0].outerHTML).toEqual(expected); + }) + $scope.$digest(); + }); + + it('should append configured SVG icon from named group', function() { + var expected = updateDefaults(''); + $mdIcon('social:s1').then(function(el) { + expect(el[0].outerHTML).toEqual(expected); + }) + $scope.$digest(); + }); + + it('should append configured SVG icon from default group', function() { + var expected = updateDefaults(''); + $mdIcon('c1').then(function(el) { + expect(el[0].outerHTML).toEqual(expected); + }) + $scope.$digest(); + }); + + it('should allow single icon defs to override those defined in groups', function() { + $mdIcon('c2').then(function(el) { + expect(el.find('g').hasClass('override')).toBe(true); + }) + $scope.$digest(); + }); + + }); + + describe('When $mdIcon() is passed a URL', function() { + + it('should return correct SVG markup', function() { + $mdIcon('android.svg').then(function(el) { + expect(el[0].outerHTML).toEqual( updateDefaults('') ); + }) + $scope.$digest(); + }); + + }); + + describe('When icon set URL is not found', function() { + it('should throw Error', function() { + var msg; + try { + $mdIcon('notconfigured') + .catch(function(error){ + msg = error; + }); + + $scope.$digest(); + } finally { + expect(msg).toEqual('icon $default:notconfigured not found'); + } + }); + }); + + describe('When icon is not found', function() { + it('should throw Error', function() { + var msg; + try { + $mdIcon('notfound:someIcon') + .catch(function(error){ + msg = error; + }); + + $httpBackend.flush(); + } finally { + expect(msg).toEqual('Cannot GET notfoundgroup.svg'); + } + }); + }); + + function updateDefaults(svg) { + svg = angular.element(svg); + + svg.attr({ + 'xmlns' : 'http://www.w3.org/2000/svg', + 'fit' : '', + 'height': '100%', + 'width' : '100%', + 'preserveAspectRatio': 'xMidYMid meet', + 'viewBox' : svg.attr('viewBox') || '0 0 24 24' + }) + .css( { + 'pointer-events' : 'none', + 'display' : 'block' + }); + + return svg[0].outerHTML; + } + +}); diff --git a/src/components/radioButton/demoBasicUsage/index.html b/src/components/radioButton/demoBasicUsage/index.html index 9b196db27c2..1860eb7b76f 100644 --- a/src/components/radioButton/demoBasicUsage/index.html +++ b/src/components/radioButton/demoBasicUsage/index.html @@ -1,4 +1,5 @@ +

Selected Value: {{ data.group1 }}

@@ -42,3 +43,4 @@
+
diff --git a/src/components/radioButton/demoBasicUsage/script.js b/src/components/radioButton/demoBasicUsage/script.js index ef8de8eb9ef..c57d08235ef 100644 --- a/src/components/radioButton/demoBasicUsage/script.js +++ b/src/components/radioButton/demoBasicUsage/script.js @@ -10,15 +10,15 @@ angular }; $scope.avatarData = [{ - id: "svg-1", + id: "avatars:svg-1", title: 'avatar 1', value: 'avatar-1' },{ - id: "svg-2", + id: "avatars:svg-2", title: 'avatar 2', value: 'avatar-2' },{ - id: "svg-3", + id: "avatars:svg-3", title: 'avatar 3', value: 'avatar-3' }]; @@ -46,5 +46,5 @@ angular }) .config(function($mdIconProvider) { - $mdIconProvider.defaultIconSet('icons/avatar-icons.svg'); + $mdIconProvider.iconSet("avatars", 'icons/avatar-icons.svg',128); });