Skip to content
This repository was archived by the owner on May 29, 2019. It is now read-only.

Commit 2ade054

Browse files
committed
feat(modal): add component support
- Add support for `component` option Closes #5683 Closes #6179
1 parent f5ff12c commit 2ade054

File tree

3 files changed

+199
-52
lines changed

3 files changed

+199
-52
lines changed

src/modal/docs/readme.md

+28-14
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,23 @@ The `$uibModal` service has only one method: `open(options)`.
1010
* `animation`
1111
_(Type: `boolean`, Default: `true`)_ -
1212
Set to false to disable animations on new modal/backdrop. Does not toggle animations for modals/backdrops that are already displayed.
13-
14-
* `appendTo`
13+
14+
* `appendTo`
1515
_(Type: `angular.element`, Default: `body`: Example: `$document.find('aside').eq(0)`)_ -
1616
Appends the modal to a specific element.
1717

18-
* `ariaDescribedBy`
18+
* `ariaDescribedBy`
1919
_(Type: `string`, `my-modal-description`)_ -
2020
Sets the [`aria-describedby`](https://www.w3.org/TR/wai-aria/states_and_properties#aria-describedby) property on the modal. The value should be an id (without the leading `#`) pointing to the element that describes your modal. Typically, this will be the text on your modal, but does not include something the user would interact with, like buttons or a form. Omitting this option will not impact sighted users but will weaken your accessibility support.
2121

22-
* `ariaLabelledBy`
22+
* `ariaLabelledBy`
2323
_(Type: `string`, `my-modal-title`)_ -
2424
Sets the [`aria-labelledby`](https://www.w3.org/TR/wai-aria/states_and_properties#aria-labelledby) property on the modal. The value should be an id (without the leading `#`) pointing to the element that labels your modal. Typically, this will be a header element. Omitting this option will not impact sighted users but will weaken your accessibility support.
25-
25+
2626
* `backdrop`
2727
_(Type: `boolean|string`, Default: `true`)_ -
2828
Controls presence of a backdrop. Allowed values: `true` (default), `false` (no backdrop), `'static'` (disables modal closing by click on the backdrop).
29-
29+
3030
* `backdropClass`
3131
_(Type: `string`)_ -
3232
Additional CSS class(es) to be added to a modal backdrop template.
@@ -35,15 +35,29 @@ The `$uibModal` service has only one method: `open(options)`.
3535
_(Type: `boolean`, Default: `false`)_ -
3636
When used with `controllerAs` & set to `true`, it will bind the $scope properties onto the controller.
3737

38+
* `component`
39+
_(Type: `string`, Example: `myComponent`)_ -
40+
A string reference to the component to be rendered that is registered with Angular's compiler. If using a directive, the directive must have `restrict: 'E'` and a template or templateUrl set.
41+
42+
It supports these bindings:
43+
44+
* `close` - A method that can be used to close a modal, passing a result. The result must be passed in this format: `{$value: myResult}`
45+
46+
* `dismiss` - A method that can be used to dismiss a modal, passing a result. The result must be passed in this format: `{$value: myRejectedResult}`
47+
48+
* `modalInstance` - The modal instance. This is the same `$uibModalInstance` injectable found when using `controller`.
49+
50+
* `resolve` - An object of the modal resolve values. See [UI Router resolves](#ui-router-resolves) for details.
51+
3852
* `controller`
3953
_(Type: `function|string|array`, Example: `MyModalController`)_ -
4054
A controller for the modal instance, either a controller name as a string, or an inline controller function, optionally wrapped in array notation for dependency injection. Allows the controller-as syntax. Has a special `$uibModalInstance` injectable to access the modal instance.
4155

4256
* `controllerAs`
43-
_(Type: `string`, Example: `ctrl`)_ -
57+
_(Type: `string`, Example: `ctrl`)_ -
4458
An alternative to the controller-as syntax. Requires the `controller` option to be provided as well.
4559

46-
* `keyboard` -
60+
* `keyboard` -
4761
_(Type: `boolean`, Default: `true`)_ -
4862
Indicates whether the dialog should be closable by hitting the ESC key.
4963

@@ -84,7 +98,7 @@ The `$uibModal` service has only one method: `open(options)`.
8498
CSS class(es) to be added to the top modal window.
8599

86100
Global defaults may be set for `$uibModal` via `$uibModalProvider.options`.
87-
101+
88102
#### return
89103

90104
The `open` method returns a modal instance, an object with the following properties:
@@ -111,8 +125,8 @@ The `open` method returns a modal instance, an object with the following propert
111125

112126
* `rendered`
113127
_(Type: `promise`)_ -
114-
Is resolved when a modal is rendered.
115-
128+
Is resolved when a modal is rendered.
129+
116130
---
117131

118132
The scope associated with modal's content is augmented with:
@@ -133,9 +147,9 @@ Also, when using `bindToController`, you can define an `$onInit` method in the c
133147

134148
Events fired:
135149

136-
* `$uibUnscheduledDestruction` -
150+
* `$uibUnscheduledDestruction` -
137151
This event is fired if the $scope is destroyed via unexpected mechanism, such as it being passed in the modal options and a $route/$state transition occurs. The modal will also be dismissed.
138-
152+
139153
* `modal.closing` -
140154
This event is broadcast to the modal scope before the modal closes. If the listener calls preventDefault() on the event, then the modal will remain open.
141155
Also, the `$close` and `$dismiss` methods returns true if the event was executed. This event also includes a parameter for the result/reason and a boolean that indicates whether the modal is being closed (true) or dismissed.
@@ -144,4 +158,4 @@ Events fired:
144158

145159
If one wants to have the modal resolve using [UI Router's](https://github.com/angular-ui/ui-router) pre-1.0 resolve mechanism, one can call `$uibResolve.setResolver('$resolve')` in the configuration phase of the application. One can also provide a custom resolver as well, as long as the signature conforms to UI Router's [$resolve](http://angular-ui.github.io/ui-router/site/#/api/ui.router.util.$resolve).
146160

147-
When the modal is opened with a controller, a `$resolve` object is exposed on the template with the resolved values from the resolve object.
161+
When the modal is opened with a controller, a `$resolve` object is exposed on the template with the resolved values from the resolve object. If using the component option, see details on how to access this object in component section of the modal documentation.

src/modal/modal.js

+83-33
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,15 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap', 'ui.bootstrap.p
260260
'button:not([disabled]):not([tabindex=\'-1\']),select:not([disabled]):not([tabindex=\'-1\']), textarea:not([disabled]):not([tabindex=\'-1\']), ' +
261261
'iframe, object, embed, *[tabindex]:not([tabindex=\'-1\']), *[contenteditable=true]';
262262
var scrollbarPadding;
263+
var SNAKE_CASE_REGEXP = /[A-Z]/g;
264+
265+
// TODO: extract into common dependency with tooltip
266+
function snake_case(name) {
267+
var separator = '-';
268+
return name.replace(SNAKE_CASE_REGEXP, function(letter, pos) {
269+
return (pos ? separator : '') + letter.toLowerCase();
270+
});
271+
}
263272

264273
function isVisible(element) {
265274
return !!(element.offsetWidth ||
@@ -496,6 +505,21 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap', 'ui.bootstrap.p
496505
}
497506
}
498507

508+
var content;
509+
if (modal.component) {
510+
content = document.createElement(snake_case(modal.component.name));
511+
content = angular.element(content);
512+
content.attr({
513+
resolve: '$resolve',
514+
'modal-instance': '$uibModalInstance',
515+
close: 'close($value)',
516+
dismiss: 'dismiss($value)'
517+
});
518+
content = $compile(content)(modal.scope);
519+
} else {
520+
content = modal.content;
521+
}
522+
499523
// Set the top modal index based on the index of the previous top modal
500524
topModalIndex = previousTopOpenedModal ? parseInt(previousTopOpenedModal.value.modalDomEl.attr('index'), 10) + 1 : 0;
501525
var angularDomEl = angular.element('<div uib-modal-window="modal-window"></div>');
@@ -513,7 +537,7 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap', 'ui.bootstrap.p
513537
'tabindex': -1,
514538
'uib-modal-animation-class': 'fade',
515539
'modal-in-class': 'in'
516-
}).html(modal.content);
540+
}).html(content);
517541
if (modal.windowClass) {
518542
angularDomEl.addClass(modal.windowClass);
519543
}
@@ -682,12 +706,17 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap', 'ui.bootstrap.p
682706
modalOptions.appendTo = modalOptions.appendTo || $document.find('body').eq(0);
683707

684708
//verify options
685-
if (!modalOptions.template && !modalOptions.templateUrl) {
686-
throw new Error('One of template or templateUrl options is required.');
709+
if (!modalOptions.component && !modalOptions.template && !modalOptions.templateUrl) {
710+
throw new Error('One of component or template or templateUrl options is required.');
687711
}
688712

689-
var templateAndResolvePromise =
690-
$q.all([getTemplatePromise(modalOptions), $uibResolve.resolve(modalOptions.resolve, {}, null, null)]);
713+
var templateAndResolvePromise;
714+
if (modalOptions.component) {
715+
templateAndResolvePromise = $q.when($uibResolve.resolve(modalOptions.resolve, {}, null, null));
716+
} else {
717+
templateAndResolvePromise =
718+
$q.all([getTemplatePromise(modalOptions), $uibResolve.resolve(modalOptions.resolve, {}, null, null)]);
719+
}
691720

692721
function resolveWithTemplate() {
693722
return templateAndResolvePromise;
@@ -713,17 +742,34 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap', 'ui.bootstrap.p
713742
}
714743
});
715744

745+
var modal = {
746+
scope: modalScope,
747+
deferred: modalResultDeferred,
748+
renderDeferred: modalRenderDeferred,
749+
closedDeferred: modalClosedDeferred,
750+
animation: modalOptions.animation,
751+
backdrop: modalOptions.backdrop,
752+
keyboard: modalOptions.keyboard,
753+
backdropClass: modalOptions.backdropClass,
754+
windowTopClass: modalOptions.windowTopClass,
755+
windowClass: modalOptions.windowClass,
756+
windowTemplateUrl: modalOptions.windowTemplateUrl,
757+
ariaLabelledBy: modalOptions.ariaLabelledBy,
758+
ariaDescribedBy: modalOptions.ariaDescribedBy,
759+
size: modalOptions.size,
760+
openedClass: modalOptions.openedClass,
761+
appendTo: modalOptions.appendTo
762+
};
763+
764+
var component = {};
716765
var ctrlInstance, ctrlInstantiate, ctrlLocals = {};
717766

718-
//controllers
719-
if (modalOptions.controller) {
720-
ctrlLocals.$scope = modalScope;
721-
ctrlLocals.$scope.$resolve = {};
722-
ctrlLocals.$uibModalInstance = modalInstance;
723-
angular.forEach(tplAndVars[1], function(value, key) {
724-
ctrlLocals[key] = value;
725-
ctrlLocals.$scope.$resolve[key] = value;
726-
});
767+
if (modalOptions.component) {
768+
constructLocals(component, false, true, false);
769+
component.name = modalOptions.component;
770+
modal.component = component;
771+
} else if (modalOptions.controller) {
772+
constructLocals(ctrlLocals, true, false, true);
727773

728774
// the third param will make the controller instantiate later,private api
729775
// @see https://github.com/angular/angular.js/blob/master/src/ng/controller.js#L126
@@ -744,27 +790,31 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap', 'ui.bootstrap.p
744790
}
745791
}
746792

747-
$modalStack.open(modalInstance, {
748-
scope: modalScope,
749-
deferred: modalResultDeferred,
750-
renderDeferred: modalRenderDeferred,
751-
closedDeferred: modalClosedDeferred,
752-
content: tplAndVars[0],
753-
animation: modalOptions.animation,
754-
backdrop: modalOptions.backdrop,
755-
keyboard: modalOptions.keyboard,
756-
backdropClass: modalOptions.backdropClass,
757-
windowTopClass: modalOptions.windowTopClass,
758-
windowClass: modalOptions.windowClass,
759-
windowTemplateUrl: modalOptions.windowTemplateUrl,
760-
ariaLabelledBy: modalOptions.ariaLabelledBy,
761-
ariaDescribedBy: modalOptions.ariaDescribedBy,
762-
size: modalOptions.size,
763-
openedClass: modalOptions.openedClass,
764-
appendTo: modalOptions.appendTo
765-
});
793+
if (!modalOptions.component) {
794+
modal.content = tplAndVars[0];
795+
}
796+
797+
$modalStack.open(modalInstance, modal);
766798
modalOpenedDeferred.resolve(true);
767799

800+
function constructLocals(obj, template, instanceOnScope, injectable) {
801+
obj.$scope = modalScope;
802+
obj.$scope.$resolve = {};
803+
if (instanceOnScope) {
804+
obj.$scope.$uibModalInstance = modalInstance;
805+
} else {
806+
obj.$uibModalInstance = modalInstance;
807+
}
808+
809+
var resolves = template ? tplAndVars[1] : tplAndVars;
810+
angular.forEach(resolves, function(value, key) {
811+
if (injectable) {
812+
obj[key] = value;
813+
}
814+
815+
obj.$scope.$resolve[key] = value;
816+
});
817+
}
768818
}, function resolveError(reason) {
769819
modalOpenedDeferred.reject(reason);
770820
modalResultDeferred.reject(reason);

src/modal/test/modal.spec.js

+88-5
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,16 @@ describe('$uibModal', function() {
131131
elem.focus();
132132
}
133133
};
134+
}).component('fooBar', {
135+
bindings: {
136+
resolve: '<',
137+
modalInstance: '<',
138+
close: '&',
139+
dismiss: '&'
140+
},
141+
controller: angular.noop,
142+
controllerAs: 'foobar',
143+
template: '<div>Foo Bar</div>'
134144
});
135145
}));
136146

@@ -930,16 +940,89 @@ describe('$uibModal', function() {
930940
});
931941
});
932942

933-
describe('option by option', function () {
934-
describe('template and templateUrl', function () {
935-
it('should throw an error if none of template and templateUrl are provided', function() {
943+
describe('option by option', function() {
944+
describe('component', function() {
945+
function getModalComponent($document) {
946+
return $document.find('body > div.modal > div.modal-dialog > div.modal-content foo-bar');
947+
}
948+
949+
it('should use as modal content', function() {
950+
open({
951+
component: 'fooBar'
952+
});
953+
954+
var component = getModalComponent($document);
955+
expect(component.html()).toBe('<div>Foo Bar</div>');
956+
});
957+
958+
it('should bind expected values', function() {
959+
var modal = open({
960+
component: 'fooBar',
961+
resolve: {
962+
foo: function() {
963+
return 'bar';
964+
}
965+
}
966+
});
967+
968+
var component = getModalComponent($document);
969+
var componentScope = component.isolateScope();
970+
971+
expect(componentScope.foobar.resolve.foo).toBe('bar');
972+
expect(componentScope.foobar.modalInstance).toBe(modal);
973+
expect(componentScope.foobar.close).toEqual(jasmine.any(Function));
974+
expect(componentScope.foobar.dismiss).toEqual(jasmine.any(Function));
975+
});
976+
977+
it('should close the modal', function() {
978+
var modal = open({
979+
component: 'fooBar',
980+
resolve: {
981+
foo: function() {
982+
return 'bar';
983+
}
984+
}
985+
});
986+
987+
var component = getModalComponent($document);
988+
var componentScope = component.isolateScope();
989+
990+
componentScope.foobar.close({
991+
$value: 'baz'
992+
});
993+
994+
expect(modal.result).toBeResolvedWith('baz');
995+
});
996+
997+
it('should dismiss the modal', function() {
998+
var modal = open({
999+
component: 'fooBar',
1000+
resolve: {
1001+
foo: function() {
1002+
return 'bar';
1003+
}
1004+
}
1005+
});
1006+
1007+
var component = getModalComponent($document);
1008+
var componentScope = component.isolateScope();
1009+
1010+
componentScope.foobar.dismiss({
1011+
$value: 'baz'
1012+
});
1013+
1014+
expect(modal.result).toBeRejectedWith('baz');
1015+
});
1016+
});
1017+
1018+
describe('template and templateUrl', function() {
1019+
it('should throw an error if none of component, template and templateUrl are provided', function() {
9361020
expect(function(){
9371021
var modal = open({});
938-
}).toThrow(new Error('One of template or templateUrl options is required.'));
1022+
}).toThrow(new Error('One of component or template or templateUrl options is required.'));
9391023
});
9401024

9411025
it('should not fail if a templateUrl contains leading / trailing white spaces', function() {
942-
9431026
$templateCache.put('whitespace.html', ' <div>Whitespaces</div> ');
9441027
open({templateUrl: 'whitespace.html'});
9451028
expect($document).toHaveModalOpenWithContent('Whitespaces', 'div');

0 commit comments

Comments
 (0)