Skip to content
This repository has been archived by the owner on Dec 1, 2023. It is now read-only.

Commit

Permalink
feat(modal, aside, alert): refactor with $bsCompiler to add support f…
Browse files Browse the repository at this point in the history
…or `controller`, `controllerAs`, `template`, `templateUrl` options (fixes #732, fixes #728, fixes #1394, fixes #1735)
  • Loading branch information
mgcrea committed Jul 12, 2015
1 parent 2d7304c commit b272203
Show file tree
Hide file tree
Showing 10 changed files with 239 additions and 49 deletions.
5 changes: 3 additions & 2 deletions src/alert/alert.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ angular.module('mgcrea.ngStrap.alert', ['mgcrea.ngStrap.modal'])
prefixClass: 'alert',
prefixEvent: 'alert',
placement: null,
template: 'alert/alert.tpl.html',
templateUrl: 'alert/alert.tpl.html',
template: '',
container: false,
element: null,
backdrop: false,
Expand Down Expand Up @@ -74,7 +75,7 @@ angular.module('mgcrea.ngStrap.alert', ['mgcrea.ngStrap.modal'])

// Directive options
var options = {scope: scope, element: element, show: false};
angular.forEach(['template', 'placement', 'keyboard', 'html', 'container', 'animation', 'duration', 'dismissable'], function(key) {
angular.forEach(['template', 'templateUrl', 'placement', 'keyboard', 'html', 'container', 'animation', 'duration', 'dismissable'], function(key) {
if(angular.isDefined(attr[key])) options[key] = attr[key];
});

Expand Down
2 changes: 1 addition & 1 deletion src/alert/test/alert.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ describe('alert', function() {
},
'options-template': {
scope: {alert: {title: 'Title', content: 'Hello alert!', counter: 0}, items: ['foo', 'bar', 'baz']},
element: '<a data-template="custom" bs-alert="alert">click me</a>'
element: '<a data-template-url="custom" bs-alert="alert">click me</a>'
}
};

Expand Down
5 changes: 3 additions & 2 deletions src/aside/aside.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ angular.module('mgcrea.ngStrap.aside', ['mgcrea.ngStrap.modal'])
prefixClass: 'aside',
prefixEvent: 'aside',
placement: 'right',
template: 'aside/aside.tpl.html',
templateUrl: 'aside/aside.tpl.html',
template: '',
contentTemplate: false,
container: false,
element: null,
Expand Down Expand Up @@ -50,7 +51,7 @@ angular.module('mgcrea.ngStrap.aside', ['mgcrea.ngStrap.modal'])
link: function postLink(scope, element, attr, transclusion) {
// Directive options
var options = {scope: scope, element: element, show: false};
angular.forEach(['template', 'contentTemplate', 'placement', 'backdrop', 'keyboard', 'html', 'container', 'animation'], function(key) {
angular.forEach(['template', 'templateUrl', 'contentTemplate', 'placement', 'backdrop', 'keyboard', 'html', 'container', 'animation'], function(key) {
if(angular.isDefined(attr[key])) options[key] = attr[key];
});

Expand Down
2 changes: 1 addition & 1 deletion src/aside/test/aside.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ describe('aside', function () {
},
'options-template': {
scope: {aside: {title: 'Title', content: 'Hello aside!', counter: 0}, items: ['foo', 'bar', 'baz']},
element: '<a data-template="custom" bs-aside="aside">click me</a>'
element: '<a data-template-url="custom" bs-aside="aside">click me</a>'
},
'options-html': {
scope: {aside: {title: 'title<br>next', content: 'content<br>next'}},
Expand Down
173 changes: 173 additions & 0 deletions src/helpers/compiler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
'use strict';

// NOTICE: This file was forked from the angular-material project (github.com/angular/material)
// MIT Licensed - Copyright (c) 2014-2015 Google, Inc. http://angularjs.org

angular.module('mgcrea.ngStrap.core', [])
.service('$bsCompiler', bsCompilerService);

function bsCompilerService($q, $http, $injector, $compile, $controller, $templateCache) {

/*
* @ngdoc service
* @name $bsCompiler
* @module material.core
* @description
* The $bsCompiler service is an abstraction of angular's compiler, that allows the developer
* to easily compile an element with a templateUrl, controller, and locals.
*
* @usage
* <hljs lang="js">
* $bsCompiler.compile({
* templateUrl: 'modal.html',
* controller: 'ModalCtrl',
* locals: {
* modal: myModalInstance;
* }
* }).then(function(compileData) {
* compileData.element; // modal.html's template in an element
* compileData.link(myScope); //attach controller & scope to element
* });
* </hljs>
*/

/*
* @ngdoc method
* @name $bsCompiler#compile
* @description A helper to compile an HTML template/templateUrl with a given controller,
* locals, and scope.
* @param {object} options An options object, with the following properties:
*
* - `controller` - `{(string=|function()=}` Controller fn that should be associated with
* newly created scope or the name of a registered controller if passed as a string.
* - `controllerAs` - `{string=}` A controller alias name. If present the controller will be
* published to scope under the `controllerAs` name.
* - `template` - `{string=}` An html template as a string.
* - `templateUrl` - `{string=}` A path to an html template.
* - `transformTemplate` - `{function(template)=}` A function which transforms the template after
* it is loaded. It will be given the template string as a parameter, and should
* return a a new string representing the transformed template.
* - `resolve` - `{Object.<string, function>=}` - An optional map of dependencies which should
* be injected into the controller. If any of these dependencies are promises, the compiler
* will wait for them all to be resolved, or if one is rejected before the controller is
* instantiated `compile()` will fail..
* * `key` - `{string}`: a name of a dependency to be injected into the controller.
* * `factory` - `{string|function}`: If `string` then it is an alias for a service.
* Otherwise if function, then it is injected and the return value is treated as the
* dependency. If the result is a promise, it is resolved before its value is
* injected into the controller.
*
* @returns {object=} promise A promise, which will be resolved with a `compileData` object.
* `compileData` has the following properties:
*
* - `element` - `{element}`: an uncompiled element matching the provided template.
* - `link` - `{function(scope)}`: A link function, which, when called, will compile
* the element and instantiate the provided controller (if given).
* - `locals` - `{object}`: The locals which will be passed into the controller once `link` is
* called. If `bindToController` is true, they will be coppied to the ctrl instead
* - `bindToController` - `bool`: bind the locals to the controller, instead of passing them in.
*/
this.compile = function(options) {

if(options.template && /\.html$/.test(options.template)) {
console.warn('Deprecated use of `template` option to pass a file. Please use the `templateUrl` option instead.');
options.templateUrl = options.template;
options.template = '';
}

var templateUrl = options.templateUrl;
var template = options.template || '';
var controller = options.controller;
var controllerAs = options.controllerAs;
var resolve = angular.copy(options.resolve || {});
var locals = angular.copy(options.locals || {});
var transformTemplate = options.transformTemplate || angular.identity;
var bindToController = options.bindToController;

// Take resolve values and invoke them.
// Resolves can either be a string (value: 'MyRegisteredAngularConst'),
// or an invokable 'factory' of sorts: (value: function ValueGetter($dependency) {})
angular.forEach(resolve, function(value, key) {
if (angular.isString(value)) {
resolve[key] = $injector.get(value);
} else {
resolve[key] = $injector.invoke(value);
}
});
// Add the locals, which are just straight values to inject
// eg locals: { three: 3 }, will inject three into the controller
angular.extend(resolve, locals);

if (templateUrl) {
resolve.$template = fetchTemplate(templateUrl);
} else {
resolve.$template = $q.when(template);
}

if (options.contentTemplate) {
// TODO(mgcrea): deprecate?
resolve.$template = $q.all([resolve.$template, fetchTemplate(options.contentTemplate)])
.then(function(templates) {
var templateEl = angular.element(templates[0]);
var contentEl = findElement('[ng-bind="content"]', templateEl[0]).removeAttr('ng-bind').html(templates[1]);
// Drop the default footer as you probably don't want it if you use a custom contentTemplate
if(!options.templateUrl) contentEl.next().remove();
return templateEl[0].outerHTML;
});
}

// Wait for all the resolves to finish if they are promises
return $q.all(resolve).then(function(locals) {

var template = transformTemplate(locals.$template);
if (options.html) {
template = template.replace(/ng-bind="/ig, 'ng-bind-html="');
}
// var element = options.element || angular.element('<div>').html(template.trim()).contents();
var element = angular.element('<div>').html(template.trim()).contents();
var linkFn = $compile(element);

// Return a linking function that can be used later when the element is ready
return {
locals: locals,
element: element,
link: function link(scope) {
locals.$scope = scope;

// Instantiate controller if it exists, because we have scope
if (controller) {
var invokeCtrl = $controller(controller, locals, true);
if (bindToController) {
angular.extend(invokeCtrl.instance, locals);
}
var ctrl = invokeCtrl();
// See angular-route source for this logic
element.data('$ngControllerController', ctrl);
element.children().data('$ngControllerController', ctrl);

if (controllerAs) {
scope[controllerAs] = ctrl;
}
}

return linkFn.apply(null, arguments);
}
};
});

};

function findElement(query, element) {
return angular.element((element || document).querySelectorAll(query));
}

var fetchPromises = {};
function fetchTemplate(template) {
if(fetchPromises[template]) return fetchPromises[template];
return (fetchPromises[template] = $http.get(template, {cache: $templateCache})
.then(function(res) {
return res.data;
}));
}

}
7 changes: 7 additions & 0 deletions src/modal/docs/modal.demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ <h3>Live demo <a class="small edit-plunkr" data-module-name="mgcrea.ngStrapDocs"
<small>(using data-template)</small>
</button>

<!-- You can use a custom html template with the `data-template` attr -->
<button type="button" class="btn btn-lg btn-danger" ng-click="showModal()">Modal
<br />
<small>(using service)</small>
</button>


</div>

<div class="callout callout-info">
Expand Down
18 changes: 11 additions & 7 deletions src/modal/docs/modal.demo.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,16 @@ angular.module('mgcrea.ngStrapDocs')

// Controller usage example
//
// var myModal = $modal({title: 'Title', content: 'Hello Modal<br />This is a multiline message!', show: false});
// $scope.showModal = function() {
// myModal.$promise.then(myModal.show);
// };
// $scope.hideModal = function() {
// myModal.$promise.then(myModal.hide);
// };
function MyModalController($scope, $q) {
console.warn('in');
$scope.foo = 'bar';
}
var myModal = $modal({title: 'Title', content: 'Hello Modal<br />This is a multiline message!', controller: MyModalController, template: 'modal/docs/modal.demo.tpl.html', show: false});
$scope.showModal = function() {
myModal.$promise.then(myModal.show);
};
$scope.hideModal = function() {
myModal.$promise.then(myModal.hide);
};

});
1 change: 1 addition & 0 deletions src/modal/docs/modal.demo.tpl.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ <h4 class="modal-title" ng-bind-html="title"></h4>
<h4>Text in a modal</h4>
<p ng-bind-html="content"></p>
<pre>2 + 3 = {{ 2 + 3 }}</pre>
<pre>{{ foo }}</pre>

<h4>Popover in a modal</h4>
<p>This <a href="#" role="button" class="btn btn-default popover-test" data-title="A Title" data-content="And here's some amazing content. It's very engaging. right?" bs-popover>button</a> should trigger a popover on click.</p>
Expand Down
53 changes: 18 additions & 35 deletions src/modal/modal.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use strict';

angular.module('mgcrea.ngStrap.modal', ['mgcrea.ngStrap.helpers.dimensions'])
angular.module('mgcrea.ngStrap.modal', ['mgcrea.ngStrap.core', 'mgcrea.ngStrap.helpers.dimensions'])

.provider('$modal', function() {

Expand All @@ -10,7 +10,8 @@ angular.module('mgcrea.ngStrap.modal', ['mgcrea.ngStrap.helpers.dimensions'])
prefixClass: 'modal',
prefixEvent: 'modal',
placement: 'top',
template: 'modal/modal.tpl.html',
templateUrl: 'modal/modal.tpl.html',
template: '',
contentTemplate: false,
container: false,
element: null,
Expand All @@ -20,21 +21,23 @@ angular.module('mgcrea.ngStrap.modal', ['mgcrea.ngStrap.helpers.dimensions'])
show: true
};

this.$get = function($window, $rootScope, $compile, $q, $templateCache, $http, $animate, $timeout, $sce, dimensions) {
this.$get = function($window, $rootScope, $bsCompiler, $compile, $q, $templateCache, $http, $animate, $timeout, $sce, dimensions) {

var forEach = angular.forEach;
var trim = String.prototype.trim;
var requestAnimationFrame = $window.requestAnimationFrame || $window.setTimeout;
var bodyElement = angular.element($window.document.body);
var htmlReplaceRegExp = /ng-bind="/ig;

function ModalFactory(config) {

var $modal = {};

// Common vars
var options = $modal.$options = angular.extend({}, defaults, config);
$modal.$promise = fetchTemplate(options.template);

var compilePromise = $modal.$promise = $bsCompiler.compile(options)
var compileData;

var scope = $modal.$scope = options.scope && options.scope.$new() || $rootScope.$new();
if(!options.element && !options.container) {
options.container = 'body';
Expand Down Expand Up @@ -69,29 +72,12 @@ angular.module('mgcrea.ngStrap.modal', ['mgcrea.ngStrap.helpers.dimensions'])
// Publish isShown as a protected var on scope
$modal.$isShown = scope.$isShown = false;

// Support contentTemplate option
if(options.contentTemplate) {
$modal.$promise = $modal.$promise.then(function(template) {
var templateEl = angular.element(template);
return fetchTemplate(options.contentTemplate)
.then(function(contentTemplate) {
var contentEl = findElement('[ng-bind="content"]', templateEl[0]).removeAttr('ng-bind').html(contentTemplate);
// Drop the default footer as you probably don't want it if you use a custom contentTemplate
if(!config.template) contentEl.next().remove();
return templateEl[0].outerHTML;
});
});
}

// Fetch, compile then initialize modal
var modalLinker, modalElement, modalScope;
var backdropElement = angular.element('<div class="' + options.prefixClass + '-backdrop"/>');
backdropElement.css({position:'fixed', top:'0px', left:'0px', bottom:'0px', right:'0px', 'z-index': 1038});
$modal.$promise.then(function(template) {
if(angular.isObject(template)) template = template.data;
if(options.html) template = template.replace(htmlReplaceRegExp, 'ng-bind-html="');
template = trim.apply(template);
modalLinker = $compile(template);
compilePromise.then(function(data) {
compileData = data;
$modal.init();
});

Expand Down Expand Up @@ -144,8 +130,8 @@ angular.module('mgcrea.ngStrap.modal', ['mgcrea.ngStrap.helpers.dimensions'])
// create a new scope, so we can destroy it and all child scopes
// when destroying the modal element
modalScope = $modal.$scope.$new();
// Fetch a cloned element linked from template
modalElement = $modal.$element = modalLinker(modalScope, function(clonedElement, scope) {});
// Fetch a cloned element linked from template (noop callback is required)
modalElement = $modal.$element = compileData.link(modalScope, function(clonedElement, scope) {});

if(scope.$emit(options.prefixEvent + '.show.before', $modal).defaultPrevented) {
return;
Expand Down Expand Up @@ -323,14 +309,6 @@ angular.module('mgcrea.ngStrap.modal', ['mgcrea.ngStrap.helpers.dimensions'])
return angular.element((element || document).querySelectorAll(query));
}

var fetchPromises = {};
function fetchTemplate(template) {
if(fetchPromises[template]) return fetchPromises[template];
return (fetchPromises[template] = $http.get(template, {cache: $templateCache}).then(function(res) {
return res.data;
}));
}

return ModalFactory;

};
Expand All @@ -346,10 +324,15 @@ angular.module('mgcrea.ngStrap.modal', ['mgcrea.ngStrap.helpers.dimensions'])

// Directive options
var options = {scope: scope, element: element, show: false};
angular.forEach(['template', 'contentTemplate', 'placement', 'backdrop', 'keyboard', 'html', 'container', 'animation', 'id', 'prefixEvent', 'prefixClass'], function(key) {
angular.forEach(['template', 'templateUrl', 'contentTemplate', 'controller', 'placement', 'backdrop', 'keyboard', 'html', 'container', 'animation', 'id', 'prefixEvent', 'prefixClass'], function(key) {
if(angular.isDefined(attr[key])) options[key] = attr[key];
});

if(options.template && /\.html$/.test(options.template)) {
options.templateUrl = options.template;
options.template = '';
}

// use string regex match boolean attr falsy values, leave truthy values be
var falseValueRegExp = /^(false|0|)$/i;
angular.forEach(['backdrop', 'keyboard', 'html', 'container'], function(key) {
Expand Down
Loading

0 comments on commit b272203

Please sign in to comment.