Skip to content

Commit

Permalink
WIP - fix(*): make templates work with custom interpolation symbols
Browse files Browse the repository at this point in the history
Previously, components specifying their own templates (such as Dialog, Toast etc) assumed that the
interpolation start-/endSymbols where `{{` and `}}`, thus breaking in apps using custom symbols.
This commit introduces a new `$mdUtil` method for replacing the "assumed" symbols (`{{`/`}}`) with
the actual ones.

**NOTE:**
This is still work-in-progress.

 ##### Already completed:

* Added `INTERPOLATION_SYMBOLS` property in `$mdConstant` with the default symbols used in
  templates.
* Added `replaceInterpolationSymbols()` method in `$mdUtil` for replacing default symbols
  with actual.
* Added tests for `$mdUtil.replaceInterpolationSymbols()`.
* Updated `$mdToast` to use `replaceInterpolationSymbols()`.
* Added tests for `$mdToast` with custom interpolation symbols.
* Updated `$mdDialog` to use `replaceInterpolationSymbols()`.
* Added tests for `$mdDialog` with custom interpolation symbols.

 ##### Still pending:

* Find out why `mdTextFloatDirective` works as expected (although it seems that is shouldn't).
* Update `mdTextFloatDirective` to use `replaceInterpolationSymbols()`.
* Add tests for `mdTextFloatDirective` with custom interpolation symbols.

Closes angular#877
  • Loading branch information
gkalpak committed Dec 9, 2014
1 parent 44a3322 commit 61bc482
Show file tree
Hide file tree
Showing 7 changed files with 234 additions and 58 deletions.
10 changes: 5 additions & 5 deletions src/components/dialog/dialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -267,10 +267,10 @@ function MdDialogProvider($$interimElementProvider) {
});

/* @ngInject */
function advancedDialogOptions($mdDialog) {
function advancedDialogOptions($mdDialog, $mdUtil) {
return {
template: [
'<md-dialog aria-label="{{dialog.label}}">',
template: $mdUtil.replaceInterpolationSymbols([
'<md-dialog aria-label="{{ dialog.ariaLabel }}">',
'<md-content>',
'<h2>{{ dialog.title }}</h2>',
'<p>{{ dialog.content }}</p>',
Expand All @@ -284,7 +284,7 @@ function MdDialogProvider($$interimElementProvider) {
'</md-button>',
'</div>',
'</md-dialog>'
].join(''),
].join('')),
controller: function mdDialogCtrl() {
this.hide = function() {
$mdDialog.hide(true);
Expand Down Expand Up @@ -427,7 +427,7 @@ function MdDialogProvider($$interimElementProvider) {

return dialogTransitionEnd(dialogEl);
}

function dialogPopOut(container, parentElement, clickElement) {
var dialogEl = container.find('md-dialog');

Expand Down
100 changes: 77 additions & 23 deletions src/components/dialog/dialog.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ describe('$mdDialog', function() {
}));

describe('#alert()', function() {
hasConfigurationMethods([
hasConfigurationMethods('alert', [
'title', 'content', 'ariaLabel',
'ok', 'targetEvent'
]);
Expand All @@ -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');
Expand All @@ -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'
]);
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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('<div>');
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('<div>');
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');
}));
});

12 changes: 6 additions & 6 deletions src/components/toast/toast.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
*
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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([
'<md-toast ng-class="{\'md-capsule\': toast.capsule}">',
'<span flex>{{ toast.content }}</span>',
'<md-button ng-if="toast.action" ng-click="toast.resolve()" ng-class="{\'md-action\': toast.highlightAction}">',
'{{toast.action}}',
'{{ toast.action }}',
'</md-button>',
'</md-toast>'
].join(''),
].join('')),
controller: function mdToastCtrl() {
this.resolve = function() {
$mdToast.hide();
Expand Down
25 changes: 24 additions & 1 deletion src/components/toast/toast.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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('<div>');
$mdToast.show(
Expand All @@ -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('<div>');
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) {
Expand Down
4 changes: 4 additions & 0 deletions src/core/util/constant.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ function MdConstantFactory($$rAF, $sniffer) {
RIGHT_ARROW : 39,
DOWN_ARROW : 40
},
INTERPOLATION_SYMBOLS: {
START: '{{',
END: '}}'
},
CSS: {
/* Constants */
TRANSITIONEND: 'transitionend' + (webkit ? ' webkitTransitionEnd' : ''),
Expand Down
49 changes: 42 additions & 7 deletions src/core/util/util.js
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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`.
Expand Down
Loading

0 comments on commit 61bc482

Please sign in to comment.