Opening and closing multiple Angular Material Bottom Sheets - angularjs

Here goes the scenario I'm working on:
Our web app's business logic requires opening a number of dialogs one on top of another and closing them one by one (like the usual dialog UI stack)
This works splendidly with mdDialog using the multiple: true option
We are looking into converting the dialogs into sliding panels (from the right, if it matters) and after using CSS shenanigans we've re-purposed mdBottomSheet for that (it was the most useful for our use case, even if it originally opens from the bottom and not from the right)
It works with multiple: true as expected, since it's a re-purposed mdDialog (even if it's not documented properly) but it introduces a major issue...
The issue: Suppose you've open a main dialog and then a secondary. When you close the secondary it closes the main, too, which is not the intended result. Basically, closing a sub-dialog closes the main dialog (and all the other sub-dialogs, for that matter).
A solution we've found is using the preserveScope: true option, but it introduces a major resource leak, as it keeps the no-longer-relevant scopes of closed dialogs intact and running with all the related problems (faulty logic, unneeded watchers, errors on misisng DOM elements, etc.). Trying to kill any of the remnant scopes selectively after a dialog closes kills all the still open dialogs (same as having preserveScope: false...)
So basically, we're looking for a way to have the cake and eat it, too - have both the functionality of "bottom sheet" sliding from the right and functioning with multiple dialogs as a normal dialog would.
By the way, there are requests for Angular Material team to implement such functionality properly, but for now it's stuck in development limbo...
I would like some interesting ideas, if you know of such or can think of any (we're on the verge of either making mdDialog look like mdBottomSheet [thus re-implementing it ourselves, essentially] or re-implementing Redux for AngularJS to manage dialog states - and would really, really like to avoid either :) ).
Versions: AngularJS (angular 1.6.6) and Angular Material (angular-material 1.1.5)

OK, the actual solution I've found was to edit the definition of MdBottomSheetDirective and remove the $destroy event listener.
So, instead of:
/* #ngInject */
function MdBottomSheetDirective($mdBottomSheet) {
return {
restrict: 'E',
link : function postLink(scope, element) {
element.addClass('_md'); // private md component indicator for styling
// When navigation force destroys an interimElement, then
// listen and $destroy() that interim instance...
scope.$on('$destroy', function() {
$mdBottomSheet.destroy();
});
}
};
}
I am using:
/* #ngInject */
function MdBottomSheetDirective($mdBottomSheet) {
return {
restrict: 'E',
link : function postLink(scope, element) {
element.addClass('_md'); // private md component indicator for styling
}
};
}
Surprisingly enough, it works completely as required with no side-effects (i.e. the dialog to be closed is closed with it's scope destroyed and no other dialog is affected) - even when navigating in the web-app.
If anyone has any idea why it works - I would be much obliged if you comment.

Related

How to capture the closing event of an aside when clicked outside or ESC in AngularJS?

I have a case where in which 2 asides are loaded and I have to clear a variable when the aside is closed pressing ESC or by clicking outside the aside.
I searched online and found that the following code can be used in the case of a modal. What is the code for aside?
scope.$on('modal.hide', function() {
console.log('modal... -hide');
});
How to watch for an Aside closing in angular-strap
The above code is not working for aside.
Is this approach correct? Otherwise, please suggest a method.
In the above case the modal name is not specified. So when we have more than one modal or aside how do we differentiate the closing of the modal or aside? In the current case, I don't have to differentiate the modals. But how to catch the event differently?
Update
The following code woks for aside.
scope.$on('aside.hide', function() {
console.log('aside... -hide');
});
The second question of how to identify each aside closing differently needs to be found out now.
With v2.1.1 of angular-strap the hide event for $aside was changed to aside-hide.
The AngularJS framework invokes handlers of $scope/$rootScope events with arguments:
scope.$on('aside.hide', function(event, data) {
console.log('aside... -hide');
console.log(event);
console.log(data);
});
Use those arguments to get information about the event.

AngularJS: Can I manual boolstrap modules into multiple elements without side effects?

We have a web site that manually bootstraps an angular module onto the document level. There are multiple controllers in this module on each page. This is not a single page application and it has a traditional menu and different pages load different controllers.
That setup has served us well over the past couple years, but now we are adding a new controller for faceted search and it uses html5Mode=true. That had the effect of taking over all of the links on the site.
One solution was to create a directive to dynamically apply a target to each link on the site. Angular doesn't take over links with targets. We didn't like this solution because the site uses different targets in various locations.
The solution we implemented involved grouping the controllers under modules. Since Angular doesn't allow nested modules we are bootstrapping each module via classes (i.e. any given page could have multiple modules containing several controllers).
Here is what is looks like:
angular.element(document).ready(function () {
angular.bootstrap($('.moduleA'), ['moduleA']);
angular.bootstrap($('.moduleB'), ['moduleB']);
});
Main Question:
The bootstrapping idea surprising works, but I'm not knowledgeable of the inner workings to understand if bootstrapping this way is negative. Some pages have the same module boostrapped 4 or 5 times into different divs. It works, but is it okay?
e.g. Does this create a ton of overhead or other side effects?
Yes, there will be a bunch of side effects on the things that are shared among the apps and thus have to be synchronized, e.g. window location and scroll. It is most likely XY problem, and multiple apps per page looks like terrible solution here (as well as in almost every other possible case).
target attribute and absolute links are most popular and solid solutions to prevent $location from capturing links. It is clearly seen here why it is so.
The problem is easily solved with this directive which follows rel="external" convention (particularly backed by jQuery) while keeping existing target attributes intact.
// <a external href="...
// <a rel="external" href="...
// <a data-rel="external" href="...
app.directive('a', function () {
return {
restrict: 'E',
link: function (scope, element, attrs) {
var relExternal = attrs.rel && attrs.rel.split(/\s+/).indexOf('external') >= 0;
if (!('target' in attrs) && (relExternal || 'external' in attrs)) {
attrs.$set('target', '_self');
}
}
};
});
The same thing can be done in reverse, i.e. enabling target everywhere except internal links.

Scrolling problems with kendo-ui Autocomplete in embedded mode (without iframes)

I'm currently altering a website which was devided into iframes to now being embedded (with AngularJS), without any iframes.
There is a big problem with this: I had a Kendo UI auto-complete drop-down element for selecting locations. The behavior with iframe and embedded is totally different concerning scrolling in the area around/beneath the auto-complete drop-down.
Old app: the site (iframe) around scrolled and the drop-down still was visible and moved with the rest of the site until you selected an item.
New app: the drop-down box closes immediately and you have to retype some input to get it open again. Unacceptable usability!
How do I get an auto-complete drop-down (doesn't have to be Kendo if not possible) which does have the OLD scrolling behavior in embedded mode?
Well, I found a workaround which works fine for me:
In the directive html, I added a callback for the event k-close. In this callback in the controller I prevented the default behavior of close event (of course under specific conditions) with the following code in the controller:
$scope.closeCallback= function (e) {
if (someConditionForWhichDropdownShouldntBeClosed) {
e.preventDefault();
}
};
and here's the HTML of the directive:
<input
ng-model="model"
kendo-auto-complete="source"
k-data-source="locationDataSource"
k-select="selectLocation"
k-close="closeCallback">
In my case, I prevented the Dropdown being closed as long as no item was selected.
For this I added a new boolean scope variable which was false by default, was set true if dropdown opened:
$scope.locationDataSource = new kendo.data.DataSource({
type: "json",
serverFiltering: true,
transport: {
read: function (options) {
$scope.keepKendoDropdownOpen = true;
someOtherFuncionalityAfterSelectingAnItem();
}
}
});
and set false again after selecting (in the callback of the directive's k-select).
Would be nice to also watch if the user presses ESC or something, but until now it's okay enough.
Please feel free to make my solution better or post other solutions! :-)

AngularJS: Attempt to dynamically apply directive using ngClass causing weird functional and performance issues

Requirement
I want a textarea that expands or contracts vertically as the user types, alla Facebook comment box.
When the textarea loses focus it contracts to one line (with ellipsis if content overflows) and re-expands to the size of the entered text upon re-focus (this functionality not found on Facebook)
Note: Clicking on the textarea should preserve caret position exactly where user clicked, which precludes any dynamic swapping of div for textarea as the control receives focus
Attempted Solution
I'm already well into an AngularJS implementation, so...
Use Monospaced's Angular Elastic plugin. Nice.
Two attempts...
Attempt A: <textarea ng-focus="isFocussed=true" ng-blur="isFocussed=false" ng-class="'msd-elastic': isFocussed"></textarea> Fails because ng-class triggers no re-$compile of the element after adding the class, so Angular Elastic is never invoked
Attempt B: create a custom directive that does the needed re-$compile upon class add. I used this solution by hassassin. Fails with the following problems
Attempt B problems
Here's a JSFiddle of Attempt B Note that Angular v1.2.15 is used
I. Disappearing text
go to the fiddle
type into one textarea
blur focus on that textarea (eg click in the other textarea)
focus back on the text-containing textarea
result: text disappears! (not expected or desired)
II. Increasingly excessive looping and eventual browser meltdown
click into one textarea
click into the other one
repeat the above for as long as you can until the browser stops responding and you get CPU 100% or unresponsive script warnings.
you'll notice that it starts out OK, but gets worse the more you click
I confirmed this using: XP/Firefox v27, XP/Chrome v33, Win7/Chrome v33
My investigations so far
It seems that traverseScopesLoop in AngularJS starting at line 12012 of v1.2.15, gets out of control, looping hundreds of times. I put a console.log just under the do { // "traverse the scopes" loop line and clocked thousands of loops just clicking.
Curiously, I don't get the same problems in Angular v1.0.4. See this JSFiddle which is identical, except for Angular version
I logged this as an AngularJS bug and it was closed immediately, because I'd not shown it to be a bug in Angular per se.
Questions
Is there another way to solve this to avoid the pattern in Attempt B?
Is there a better way to implement Attempt B? There's no activity on that stackoverflow issue after hassassin offered the solution I used
Are the problems with Attempt B in my code, angular-elastic, hassassin's code, or my implementation of it all? Appreciate tips on how to debug so I can "fish for myself" in future. I've been using Chrome debug and Batarang for a half day already without much success.
Does it seem sensible to make a feature request to the AngularJS team for the pattern in Attempt A? Since you can add directives by class in Angular, and ngClass can add classes dynamically, it seems only natural to solve the problem this way.
You are severely overthinking this, and I can't think of any reason you would ever need to dynamically add / remove a directive, you could just as easily, inside the directive, check if it should do anything. All you need to do is
Use the elastic plugin you are using
Use your own directive to reset height / add ellipsis when it doesn't have focus.
So something like this will work (not pretty, but just threw it together):
http://jsfiddle.net/ss6Y5/8/
angular.module("App", ['monospaced.elastic']).directive('dynamicClass', function($compile) {
return {
scope: { ngModel: '=' },
require: "?ngModel",
link: function(scope, elt, attrs, ngModel) {
var tmpModel = false;
var origHeight = elt.css('height');
var height = elt.css('height');
var heightChangeIndex = 0;
scope.$watch('ngModel', function() {
if (elt.css('height') > origHeight && !heightChangeIndex) {
heightChangeIndex = scope.ngModel.length;
console.log(heightChangeIndex);
}
else if (elt.css('height') <= origHeight && elt.is(':focus')) {
heightChangeIndex = 0;
}
});
elt.on('blur focus', function() {
var tmp = elt.css('height');
elt.css('height', height);
height = tmp;
});
elt.on('blur', function() {
if (height > origHeight) {
tmpModel = angular.copy(scope.ngModel);
ngModel.$setViewValue(scope.ngModel.substr(0, heightChangeIndex-4) + '...');
ngModel.$render();
}
});
elt.on('focus', function() {
if (tmpModel.length) {
scope.ngModel = tmpModel;
ngModel.$setViewValue(scope.ngModel);
ngModel.$render();
tmpModel = '';
}
});
}
};
})

Angular modal dialog best practices

What is the best practice for creating modal dialogs with dynamic content, contrasted with dialogs that don't have dynamic content.
Eg..
We have some modal forms that accept a list of form elements, and have submit/cancel.
Also, there are modal dialogs that just display a confirm/ok type of operation.
I've seen a lot of people saying that dialogs should be services passed into the controller, but it seems to me that services shouldn't be rendering UI components and manipulating the DOM.
What is the best practice for assembling these two types of dialogs? Thanks.
Angular UI Boostrap provides a service - $dialog - that can be injected wherever you need to use a dialog box. That service has two main methods: dialog and messageBox. The former is used to create a dialog with dynamic content and the latter to create a message box with a title, a message and a set of buttons. Both return a promise so you can process its result, when it's available.
I think this approach works well, because it fits the somehow natural, imperative way of handling dialogs. For instance, if the user clicks on a button and you want to show a dialog and then process its result, the code could look like this:
$scope.doSomething = function() {
$dialog.dialog().open().then(function(result) {
if (result === OK) {
// Process OK
}
else {
// Process anything else
}
});
}
You can indeed use directives to do the same, and perhaps it seems the right way to do it since there is DOM manipulation involved, but I think it would be kind of awkward to handle it. The previous example would be something like this:
<dialog visible="dialogVisible" callback="dialogCallback()"></dialog>
...
$scope.doSomething = function() {
$scope.dialogVisible = true;
}
$scope.dialogCallback = function(result) {
if (result === OK) {
// Process OK
}
else {
// Process anything else
}
}
IMO, the first example looks better and it's easier to understand.
Since dialogs are DOM components, they should probably be directives. You can either build up the DOM elements of the modal inside the directive itself or put the elements on the main html page hidden and unhide them from the directive. If you don't isolate the directive's scope, you can just refer to the controller scope (unless you are in a child scope) from the directive.
Dynamic vs. static content isn't that much of a decision point IMO. Since you have access to the scope from within the directive, you can access whatever you need from the inherited scope.
One quite simple design that works well is to :
Have such a "modal dialog" div somewhere in your html. It will be typically absolute, taking all the screen width and height (typically a dark translucent div with a smaller dialog div into it) and not displayed by default (use ng-show to display it conditionally, depending on the existence of modals or not)
Declare a controller that listens to dialog events ("dialogShow", "dialogClose", etc.) and change its "currentModal" $scope value when receiving them. According to the ng-show condition setup in the previous step, the modal will accordingly display or change or disappear (if set to null/undefined)
Trigger dialog events from anywhere in your application, using broadcasts.
Improvements are:
Events parameters properties (setup when triggering and received by the controller) could include title, message, images, even html (to be sanitized), buttons, callbacks for those buttons, display durations (throught $timeout)
Remember a stack of received alerts. When one is closed, the next pending one displays

Resources