diff --git a/src/components/dialog/dialog.js b/src/components/dialog/dialog.js index 5fcfe967add..d1186c7c761 100644 --- a/src/components/dialog/dialog.js +++ b/src/components/dialog/dialog.js @@ -267,10 +267,10 @@ function MdDialogProvider($$interimElementProvider) { }); /* @ngInject */ - function advancedDialogOptions($mdDialog) { + function advancedDialogOptions($mdDialog, $mdUtil) { return { - template: [ - '', + template: $mdUtil.replaceInterpolationSymbols([ + '', '', '

{{ dialog.title }}

', '

{{ dialog.content }}

', @@ -284,7 +284,7 @@ function MdDialogProvider($$interimElementProvider) { '', '', '
' - ].join(''), + ].join('')), controller: function mdDialogCtrl() { this.hide = function() { $mdDialog.hide(true); @@ -427,7 +427,7 @@ function MdDialogProvider($$interimElementProvider) { return dialogTransitionEnd(dialogEl); } - + function dialogPopOut(container, parentElement, clickElement) { var dialogEl = container.find('md-dialog'); diff --git a/src/components/dialog/dialog.spec.js b/src/components/dialog/dialog.spec.js index 7c1940bd5bd..b35ec321652 100644 --- a/src/components/dialog/dialog.spec.js +++ b/src/components/dialog/dialog.spec.js @@ -15,7 +15,7 @@ describe('$mdDialog', function() { })); describe('#alert()', function() { - hasConfigurationMethods([ + hasConfigurationMethods('alert', [ 'title', 'content', 'ariaLabel', 'ok', 'targetEvent' ]); @@ -33,7 +33,7 @@ describe('$mdDialog', function() { ).then(function() { resolved = true; }); - + $rootScope.$apply(); var container = angular.element(parent[0].querySelector('.md-dialog-container')); container.triggerHandler('transitionend'); @@ -53,20 +53,10 @@ describe('$mdDialog', function() { expect(parent.find('h2').length).toBe(0); expect(resolved).toBe(true); })); - - function hasConfigurationMethods(methods) { - angular.forEach(methods, function(method) { - return it('supports config method #' + method, inject(function($mdDialog) { - var alert = $mdDialog.alert(); - expect(typeof alert[method]).toBe('function'); - expect(alert[method]()).toEqual(alert); - })); - }); - } }); describe('#confirm()', function() { - hasConfigurationMethods([ + hasConfigurationMethods('confirm', [ 'title', 'content', 'ariaLabel', 'ok', 'cancel', 'targetEvent' ]); @@ -106,16 +96,6 @@ describe('$mdDialog', function() { expect(parent.find('h2').length).toBe(0); expect(rejected).toBe(true); })); - - function hasConfigurationMethods(methods) { - angular.forEach(methods, function(method) { - return it('supports config method #' + method, inject(function($mdDialog) { - var alert = $mdDialog.confirm(); - expect(typeof alert[method]).toBe('function'); - expect(alert[method]()).toEqual(alert); - })); - }); - } }); describe('#build()', function() { @@ -409,4 +389,78 @@ describe('$mdDialog', function() { expect(dialog.attr('aria-label')).toEqual('Some Other Thing'); })); }); + + function hasConfigurationMethods(preset, methods) { + angular.forEach(methods, function(method) { + return it('supports config method #' + method, inject(function($mdDialog) { + var dialog = $mdDialog[preset](); + expect(typeof dialog[method]).toBe('function'); + expect(dialog[method]()).toEqual(dialog); + })); + }); + } +}); + +describe('$mdDialog with custom interpolation symbols', function() { + beforeEach(TestUtil.mockRaf); + beforeEach(module('material.components.dialog', 'ngAnimateMock')); + + beforeEach(module(function($interpolateProvider) { + $interpolateProvider.startSymbol('[[').endSymbol(']]'); + })); + + it('displays #alert() correctly', inject(function($mdDialog, $rootScope) { + var parent = angular.element('
'); + var dialog = $mdDialog. + alert({parent: parent}). + ariaLabel('test alert'). + title('Title'). + content('Hello, world !'). + ok('OK'); + + $mdDialog.show(dialog); + $rootScope.$digest(); + + var mdContainer = angular.element(parent[0].querySelector('.md-dialog-container')); + var mdDialog = mdContainer.find('md-dialog'); + var mdContent = mdDialog.find('md-content'); + var title = mdContent.find('h2'); + var content = mdContent.find('p'); + var mdActions = angular.element(mdDialog[0].querySelector('.md-actions')); + var buttons = mdActions.find('md-button'); + + expect(mdDialog.attr('aria-label')).toBe('test alert'); + expect(title.text()).toBe('Title'); + expect(content.text()).toBe('Hello, world !'); + expect(buttons.eq(0).text()).toBe('OK'); + })); + + it('displays #confirm() correctly', inject(function($mdDialog, $rootScope) { + var parent = angular.element('
'); + var dialog = $mdDialog. + confirm({parent: parent}). + ariaLabel('test alert'). + title('Title'). + content('Hello, world !'). + cancel('CANCEL'). + ok('OK'); + + $mdDialog.show(dialog); + $rootScope.$digest(); + + var mdContainer = angular.element(parent[0].querySelector('.md-dialog-container')); + var mdDialog = mdContainer.find('md-dialog'); + var mdContent = mdDialog.find('md-content'); + var title = mdContent.find('h2'); + var content = mdContent.find('p'); + var mdActions = angular.element(mdDialog[0].querySelector('.md-actions')); + var buttons = mdActions.find('md-button'); + + expect(mdDialog.attr('aria-label')).toBe('test alert'); + expect(title.text()).toBe('Title'); + expect(content.text()).toBe('Hello, world !'); + expect(buttons.eq(0).text()).toBe('CANCEL'); + expect(buttons.eq(1).text()).toBe('OK'); + })); }); + diff --git a/src/components/toast/toast.js b/src/components/toast/toast.js index 973f494a5c3..699245ccd0e 100644 --- a/src/components/toast/toast.js +++ b/src/components/toast/toast.js @@ -27,7 +27,7 @@ function MdToastDirective() { * @module material.components.toast * * @description - * `$mdToast` is a service to butild a toast nofication on any position + * `$mdToast` is a service to build a toast nofication on any position * on the screen with an optional duration, and provides a simple promise API. * * @@ -86,7 +86,7 @@ function MdToastDirective() { * * @description Shows the toast. * - * @param {object} optionsOrPreset Either provide an `$mdToastPreset` returned from `simple()` + * @param {object} optionsOrPreset Either provide an `$mdToastPreset` returned from `simple()` * and `build()`, or an options object with the following properties: * * - `templateUrl` - `{string=}`: The url of an html template file that will @@ -147,16 +147,16 @@ function MdToastProvider($$interimElementProvider) { .addPreset('simple', { argOption: 'content', methods: ['content', 'action', 'highlightAction'], - options: /* @ngInject */ function($mdToast) { + options: /* @ngInject */ function($mdToast, $mdUtil) { return { - template: [ + template: $mdUtil.replaceInterpolationSymbols([ '', '{{ toast.content }}', '', - '{{toast.action}}', + '{{ toast.action }}', '', '' - ].join(''), + ].join('')), controller: function mdToastCtrl() { this.resolve = function() { $mdToast.hide(); diff --git a/src/components/toast/toast.spec.js b/src/components/toast/toast.spec.js index 3bc005c9751..86568255483 100644 --- a/src/components/toast/toast.spec.js +++ b/src/components/toast/toast.spec.js @@ -35,7 +35,7 @@ describe('$mdToast service', function() { expect(rejected).toBe(true); })); - it('supports an action toast', inject(function($mdToast, $rootScope, $timeout, $animate) { + it('supports an action toast', inject(function($mdToast, $rootScope, $animate) { var resolved = false; var parent = angular.element('
'); $mdToast.show( @@ -58,6 +58,29 @@ describe('$mdToast service', function() { expect(resolved).toBe(true); })); + describe('when using custom interpolation symbols', function() { + beforeEach(module(function($interpolateProvider) { + $interpolateProvider.startSymbol('[[').endSymbol(']]'); + })); + + it('displays correctly', inject(function($mdToast, $rootScope) { + var parent = angular.element('
'); + var toast = $mdToast.simple({ + content: 'Do something', + parent: parent + }).action('Click me'); + + $mdToast.show(toast); + $rootScope.$digest(); + + var content = parent.find('span').eq(0); + var button = parent.find('button'); + + expect(content.text()).toBe('Do something'); + expect(button.text()).toBe('Click me'); + })); + }); + function hasConfigMethods(methods) { angular.forEach(methods, function(method) { return it('supports config method #' + method, inject(function($mdToast) { diff --git a/src/core/util/constant.js b/src/core/util/constant.js index 11b1f1660bc..e051e3c4326 100644 --- a/src/core/util/constant.js +++ b/src/core/util/constant.js @@ -21,6 +21,10 @@ function MdConstantFactory($$rAF, $sniffer) { RIGHT_ARROW : 39, DOWN_ARROW : 40 }, + INTERPOLATION_SYMBOLS: { + START: '{{', + END: '}}' + }, CSS: { /* Constants */ TRANSITIONEND: 'transitionend' + (webkit ? ' webkitTransitionEnd' : ''), diff --git a/src/core/util/util.js b/src/core/util/util.js index a25954843fa..55fbb78a287 100644 --- a/src/core/util/util.js +++ b/src/core/util/util.js @@ -1,16 +1,21 @@ (function() { 'use strict'; -/* +/* * This var has to be outside the angular factory, otherwise when * there are multiple material apps on the same page, each app - * will create its own instance of this array and the app's IDs + * will create its own instance of this array and the app's IDs * will not be unique. */ var nextUniqueId = ['0','0','0']; angular.module('material.core') -.factory('$mdUtil', ['$cacheFactory', function($cacheFactory) { +.factory('$mdUtil', function($cacheFactory, $interpolate, $mdConstant) { + var interpolationSymbols = { + start: $interpolate.startSymbol(), + end: $interpolate.endSymbol() + }; + var Util; return Util = { now: window.performance ? angular.bind(window.performance, window.performance.now) : Date.now, @@ -91,6 +96,12 @@ angular.module('material.core') return nextUniqueId.join(''); }, + /** + * Replace `{{` and `}}` in a string with the actual start-/endSymbols used for interpolation. + * @see replaceInterpolationSymbols below + */ + replaceInterpolationSymbols: replaceInterpolationSymbols, + // Stop watchers and events from firing on a scope without destroying it, // by disconnecting it from its parent and its siblings' linked lists. disconnectScope: function disconnectScope(scope) { @@ -291,7 +302,7 @@ angular.module('material.core') } /* - * Find the next item. If reloop is true and at the end of the list, it will + * Find the next item. If reloop is true and at the end of the list, it will * go back to the first item. If given ,the `validate` callback will be used * determine whether the next item is valid. If not valid, it will try to find the * next item again. @@ -313,7 +324,7 @@ angular.module('material.core') } /* - * Find the previous item. If reloop is true and at the beginning of the list, it will + * Find the previous item. If reloop is true and at the beginning of the list, it will * go back to the last item. If given ,the `validate` callback will be used * determine whether the previous item is valid. If not valid, it will try to find the * previous item again. @@ -376,9 +387,33 @@ angular.module('material.core') return cache; } -}]); -/* + /* + * Replace `{{` and `}}` in a string (usually a template) with the actual start-/endSymbols used + * for interpolation. This allows pre-defined templates (for components such as dialog, toast etc) + * to continue to work in apps that use custom interpolation start-/endSymbols. + * + * @param {string} text The test in which to replace `{{`/`}}` + * @returns {string} The modified string using the actual interpolation start-/endSymbols + */ + function replaceInterpolationSymbols(text) {debugger + var actualStart = interpolationSymbols.start; + var defaultStart = $mdConstant.INTERPOLATION_SYMBOLS.START; + if (actualStart !== defaultStart) { + text = text.split(defaultStart).join(actualStart); + } + + var actualEnd = interpolationSymbols.end; + var defaultEnd = $mdConstant.INTERPOLATION_SYMBOLS.END; + if (actualEnd !== defaultEnd) { + text = text.split(defaultEnd).join(actualEnd); + } + + return text; + } +}); + +/* * Since removing jQuery from the demos, some code that uses `element.focus()` is broken. * * We need to add `element.focus()`, because it's testable unlike `element[0].focus`. diff --git a/src/core/util/util.spec.js b/src/core/util/util.spec.js index 7800fcba18f..fbee565ad35 100644 --- a/src/core/util/util.spec.js +++ b/src/core/util/util.spec.js @@ -1,26 +1,86 @@ describe('util', function() { beforeEach(module('material.core')); - var disconnectScope, reconnectScope; - beforeEach(inject(function($mdUtil) { - disconnectScope = $mdUtil.disconnectScope; - reconnectScope = $mdUtil.reconnectScope; - })); + describe('re-/disconectScope()', function() { + var disconnectScope, reconnectScope; + beforeEach(inject(function($mdUtil) { + disconnectScope = $mdUtil.disconnectScope; + reconnectScope = $mdUtil.reconnectScope; + })); - it('disconnectScope events', inject(function($rootScope) { - var scope1 = $rootScope.$new(); + it('re-/disconnects scope to/from the event chain', inject(function($rootScope) { + var scope1 = $rootScope.$new(); - var spy = jasmine.createSpy('eventSpy'); - scope1.$on('event', spy); + var spy = jasmine.createSpy('eventSpy'); + scope1.$on('event', spy); - disconnectScope(scope1); + disconnectScope(scope1); - $rootScope.$broadcast('event'); - expect(spy).not.toHaveBeenCalled(); + $rootScope.$broadcast('event'); + expect(spy).not.toHaveBeenCalled(); - reconnectScope(scope1); + reconnectScope(scope1); - $rootScope.$broadcast('event'); - expect(spy).toHaveBeenCalled(); - })); + $rootScope.$broadcast('event'); + expect(spy).toHaveBeenCalled(); + })); + }); + + describe('replaceInterpolationSymbols()', function() { + beforeEach(module(function($interpolateProvider) { + $interpolateProvider.startSymbol('[[').endSymbol(']]'); + })); + + var replaceInterpolationSymbols; + beforeEach(inject(function($mdUtil) { + replaceInterpolationSymbols = $mdUtil.replaceInterpolationSymbols; + })); + + it('replaces default interpolation symbols with actual', function() { + var tmpl1 = 'Template 1'; + var tmpl2 = '[[ Template 2 ]]'; + var tmpl3 = '(( Template 3 ))'; + var tmpl4 = '{{ Template 4 }}'; + var tmpl5 = '{ { Template 5 } }'; + var tmpl6 = '[[ {{ Template }} {{ 6 }} ]]'; + var tmpl7 = '[[ {{ Template ]] }} {{ [[ 7 }} ]]'; + + expect(replaceInterpolationSymbols(tmpl1)).toBe('Template 1'); + expect(replaceInterpolationSymbols(tmpl2)).toBe('[[ Template 2 ]]'); + expect(replaceInterpolationSymbols(tmpl3)).toBe('(( Template 3 ))'); + expect(replaceInterpolationSymbols(tmpl4)).toBe('[[ Template 4 ]]'); + expect(replaceInterpolationSymbols(tmpl5)).toBe('{ { Template 5 } }'); + expect(replaceInterpolationSymbols(tmpl6)).toBe('[[ [[ Template ]] [[ 6 ]] ]]'); + expect(replaceInterpolationSymbols(tmpl7)).toBe('[[ [[ Template ]] ]] [[ [[ 7 ]] ]]'); + }); + + it('should not operate on `text` when interpolation symbols are the default', inject( + function($interpolate, $mdConstant) { + var originalSplit = String.prototype.split; + + var tmpl = 'Template'; + spyOn(String.prototype, 'split').andCallThrough(); + + // Different startSymbol, different endSymbol + replaceInterpolationSymbols(tmpl); + + expect(String.prototype.split).toHaveBeenCalled(); + expect(String.prototype.split.callCount).toBe(2); + + // Same startSymbol, different endSymbol + $mdConstant.INTERPOLATION_SYMBOLS.START = $interpolate.startSymbol(); + replaceInterpolationSymbols(tmpl); + + expect(String.prototype.split.callCount).toBe(3); + + // Same startSymbol, same endSymbol + $mdConstant.INTERPOLATION_SYMBOLS.END = $interpolate.endSymbol(); + replaceInterpolationSymbols(tmpl); + + expect(String.prototype.split.callCount).toBe(3); + + String.prototype.split = originalSplit; + } + )); + }); });