Angular modal dialog best practices - angularjs

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

Related

Opening and closing multiple Angular Material Bottom Sheets

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.

How do I return focus to an element when the entire page changes?

I have a complicated setup. My application is driven by a set of "rules" which dictate what the user interface is. The UI is rendered by looping through the rules and creating the individual dropdowns. Initially, everything renders properly. However, once a user makes a change to the UI, other rules may be affected. In my application, an api call is made, which then returns a modified set of rules. In the attached plunker, I've simplified things such that only the new set of rules is applied, which causes the page to re-render. The problem is that my users would like to be able to tab between all of the entries on the page and make changes. However, once the page is re-rendered, the currently selected page element is now gone nothing has the focus. I've tried to put the focus back on the proper element by tracking a common Id, but to no avail.
Using either of these doesn't seem to work.
var el = document.getElementById(focusId);
el.focus();
angular.element(el).focus();
I've also tried using the autofocus attribute on the dropdown that I want to have focus, but that didn't work either. I'm using angularjs 1.2. Any ideas are appreciated.
http://plnkr.co/edit/ND9PKqULIOlixWR4XChN?p=preview
If you want to assign auto focus dynamically to a element on the DOM from angular you can use this:
var myEl = angular.element(document.querySelector('select'));
myEl.attr('autofocus',"attr val");
You can also pass in an id like: angular.element(document.querySelector('#focusId'));
You can look here for a prior answer which may be of some more help!
-Cheers!
Problem here is, you are trying to focus the element before the blur event completes. So you need to execute the focus code after blur event. Async execution of your focus code would solve the problem. You can use either setTimeout or $timeout.
setTimeout(function(){
angular.element('#'+focusId).focus();
/* //or
var el = document.getElementById(focusId);
el.focus();
*/
});
or
$timeout(function(){
angular.element('#'+focusId).focus();
/* //or
var el = document.getElementById(focusId);
el.focus();
*/
});
dont forgot to inject $timeout to your controller if you are using second code. Hope this helps :)

Sharing data from multiple instances of a directive

I have a custom directive which is re-usable throughout the application. This directive uses ui-grid and I'm trying to determine the "angular way" of allowing access to the grid API from anywhere in the application.
If it was only a single instance, I'd use a service to share data across controllers:
var attachments = angular.module('attachments', ['ui.grid']);
// this would be accessible from any of my controllers
attachments.factory('Attachments', function() {
return {};
});
attachments.directive('attachments', function() {
return {
restrict: 'E',
templateUrl: 'attachments.html', // template has a ui-grid, among other elements
controller: ['$scope', 'Attachments', function($scope, Attachments) {
$scope.gridOptions = {
// ui-grid code here
onRegisterApi: function( gridApi ) {
Attachments.grid = gridApi;
}
};
}]
};
});
However, there could be multiple instances of the directive
For example, there might be a primary instance of this directive and one inside a modal, or one in a sidebar, one in a modal, etc.
I suppose I could add property namespaces to that service...
Attachments = {
libraryGrid: // ...
someModalGrid: // ...
}
etc...
I'd prefer to avoid making a service for each possible instance, i.e.:
attachments.factory('SomeModalAttachments', function() {
return {};
});
While it would work it feels inefficient. However, both choices are a lot better than digging into the modal scope and finding child scopes with the necessary API.
Is there any other method I haven't considered?
To me it depends on your usage model.
If you're going to have multiple of these and other bits of the application are going to access them, then that means one of a few things:
The access is actually initiated from the grid. So you have perhaps many list pages, and the currently active list page is the one you want to deal with. So I'd have the grid register with all the things it wants to talk to, and deregister when it closes again. The other things would all be services (singletons).
There are a specified number of these grids, and you talk to them by name - so you want to interact with the list-page-grid, or the modal-grid or whatever. So you have each grid register with somewhere central (maybe a service that everything else talks to).
The grids are subsidiary to something. So a page includes the grid directive, and then that page wants to talk to that grid. You could pass an object into the directive, then have the grid register itself on that object. So you call the directive with "myGridCommunicationObject = {}", and then the grid does "$scope.myGridCommunicationObject.gridApi = gridApi". This doesn't let other bits of the application talk to the grid, but if really you only want whatever created the grid to talk to it, then it works well.
You could broadcast. So if you don't really care which grid you talk to, you just want to talk to any grid that's currently visible (say you're resizing them or something) you could just broadcast an event to them, and all your grid directives could listen for that event. Taking this one step further, you could broadcast and include a grid id or name in the parameter, and then have each grid check whether it's me before taking action.
Having said all those options, there's something about what you're doing that has a bit of a code smell to it. Really, arbitrary bits of the application shouldn't want to talk directly to the grid Api, they should be communicating with methods on the controller that holds the grid. Perhaps some examples of what you want to do would help, but it feels to me like the model should be one of the grid registering to use other services (e.g. resize notifications), or of the controller that owns the grid interacting with the grid, and other things interacting with that controller.

Sharing buttons and logic across views and controllers

I'm working on a single page app that behaves similar to a wizard. The user lands on page 1 and needs to save and continue to get to page 2. What's the best way to share those buttons between all the views? My form names are different so I currently have to duplicate these buttons so I can use logic like:
<div class="mySaveButton" ng-disabled="page1Form.$invalid"></div>
but then on page 2:
<div class="mySaveButton" ng-disabled="page2Form.$invalid"></div>
To further complicate matters, saving on page1 posts the data to a different address than page2. I have a navigation controller which is the parent and that needs to be handled as well.
So to summarize I need my buttons (Back, Save and Save and Continue) to do all of the following without having to duplicate the buttons across all views:
Check if the current form is valid
If it's valid, the data for that form needs to post to the correct endpoint for that form
Navigation needs to be notified so that it can update and/or take action
Essentially you need to re-use a template (/controller), and somehow pass options into it. You could probably do something with ngInclude and nesting controllers, but a way that is more friendly to more complex structure later is to create a custom directive. It depends a bit on your exact use-case, and exactly what you need to pass into it, but a simple version would be something like:
app.directive('formButtons', function() {
return {
restrict: 'E',
scope: {
'localBack':'&back',
'localSave':'&save',
'localSaveAndContinue':'&saveAndContinue'
},
templateUrl: 'buttons.html'
};
});
With a template of
<div>
<button ng-click="localBack()">Back</button>
<button ng-click="localSave()">Save</button>
<button ng-click="localSaveAndContinue()">Save & Continue</button>
</div>
The scope options with & each define a function on the directive's scope, that evaluates contents of the attribute on the <form-buttons> element of that name in the parent scope. You can then use the directive as follows
<form-buttons back="back(2)" save="save(2)" save-and-continue="saveAndContinue(2)"></form-buttons>
which will get replaced by the template of the directive, and the function save, back and saveAndContinue, which are defined on the parent scope, will get called when clicking on the buttons in the template, passing the appropriate step number which can be used to customise behaviour.
You can see a working example at http://plnkr.co/edit/fqXowQNYwjQWIbl6A5Vy?p=preview

Webshims - Show invalid form fields on initial load

(Follow on questions from Placeholder Hidden)
I'd like my form to validate existing data when it is loaded. I can't seem to get that to happen
I jQuery.each of my controls and call focus() and blur(), is there a better way than this? I tried to call ctrl.checkValidity(), but it wasn't always defined yet. When it was, it still didn't mark the controls.
I seem to have a timing issue too, while the focus and blur() fire, the UI does not update. It's as if the Webshims are not fully loaded yet, even though this fires in the $.webshims.ready event.
I also tried to call $('#form').submit(), but this doesn't fire the events as I expected. The only way I could make that happen was to include an input type='submit'. How can I pragmatically case a form validation like clicking a submit button would?
Here's a jsFiddle that demonstrates the problem. When the form loads, I want the invalid email to be marked as such. If you click the add button it will be marked then, but not when initially loaded. Why?
Focus and blur in the control will cause it to be marked.
BUT, clicking ADD will too (which runs the same method that ran when it was loaded). Why does it work the 2nd time, but not when initially loaded?
updateValidation : function () {
this.$el.find('[placeholder]').each(function (index, ctrl) {
var $ctrl = $(ctrl);
if( $ctrl.val() !== "" && (ctrl.checkValidity && !ctrl.checkValidity()) ) {
// alert('Do validity check!');
$ctrl.focus();
$ctrl.blur();
}
});
}
I see this in FF 17.0.5. The problem is worse in IE9, sometimes taking 2 or 3 clicks of ADD before the fields show in error. However, I get errors on some of the js files I've liked 'due to mime type mismatch'.
This has to do with the fact, that you are trying to reuse the .user-error class, which is a "shim" for the CSS4 :user-error and shouldn't be triggered from script. The user-error scripts are loaded after onload or as soon as a user seems to interact with an invalid from.
From my point of view, you shouldn't use user-error and instead create your own class. You can simply check for validity using the ':invalid' selector:
$(this)[ $(this).is(':invalid') ? 'addClass' : 'removeClass']('invalid-value');
Simply write a function with similar code and bind them to events like change, input and so on and call it on start.
In case you still want to use user-error, you could do the following, but I would not recommend:
$.webshims.polyfill('forms');
//force webshims to load form-validation module as soon as possible
$.webshims.loader.loadList(['form-validation']);
//wait until form-validation is loaded
$.webshims.ready('DOM form-validation', function(){
$('input:invalid')
.filter(function(){
return !!$(this).val();
})
.trigger('refreshvalidityui')
;
});

Resources