Loading state as a modal overlay in AngularJS - javascript

I want to load a state as a modal so that I can overlay a state without effecting any other states in my application. So for example if I have a link like:
<a ui-sref="notes.add" modal>Add Note</a>
I want to then interrupt the state change using a directive:
.directive('modal', ['$rootScope', '$state', '$http', '$compile',
function($rootScope, $state, $http, $compile){
return {
priority: 0,
restrict: 'A',
link: function(scope, el, attrs) {
$rootScope.$on('$stateChangeStart', function (event, toState, toParams) {
event.preventDefault();
});
el.click(function(e){
$http
.get('URL HERE')
.then(function(resp){
$('<div class="modal">' + resp.data + '</div>').appendTo('[ui-view=app]');
setTimeout(function(){
$('.wrapper').addClass('showModal');
},1);
});
});
}
}
}
])
This successfully prevents the state change and loads the URL and appends it as a modal to the application. The problem is that it loads the entire application again...
How can I load just the state? e.g. the template files and the adjoining controller.
The state looks like:
.state('notes.add',
{
parent: 'notes',
url: '/add',
views: {
'content': {
templateUrl: 'partials/notes/add.html',
controller: 'NotesAddCtrl'
}
}
})
An example of how it should work using jQuery: http://dev.driz.co.uk/AngularModal
See how I can access StateA and StateB loading via AJAX that uses the History API to change the URL to reflect the current state change.
And regardless of whether I am on the index, StateA or StateB I can load StateA or StateB as a modal (even if I'm on that State already) and it doesn't change the url or the current content, it just overlays the state content.
This is what I want to be able to do in AngularJS.
Note. this example doesn't work with the browser back and forward buttons due to it being a quick example and not using the history api correctly.

I've seen your question a few days ago and it seemed interesting enough to try and set up something that would work.
I've taken the uiSref directive as a start, and modified the code to use angular-bootstrap's $modal to show the desired state.
angular.module('ui.router.modal', ['ui.router', 'ui.bootstrap'])
.directive('uiSrefModal', $StateRefModalDirective);
function parseStateRef(ref, current) {
var preparsed = ref.match(/^\s*({[^}]*})\s*$/), parsed;
if (preparsed) ref = current + '(' + preparsed[1] + ')';
parsed = ref.replace(/\n/g, " ").match(/^([^(]+?)\s*(\((.*)\))?$/);
if (!parsed || parsed.length !== 4) throw new Error("Invalid state ref '" + ref + "'");
return { state: parsed[1], paramExpr: parsed[3] || null };
}
function stateContext(el) {
var stateData = el.parent().inheritedData('$uiView');
if (stateData && stateData.state && stateData.state.name) {
return stateData.state;
}
}
$StateRefModalDirective.$inject = ['$state', '$timeout', '$modal'];
function $StateRefModalDirective($state, $timeout, $modal) {
var allowedOptions = ['location', 'inherit', 'reload'];
return {
restrict: 'A',
link: function(scope, element, attrs) {
var ref = parseStateRef(attrs.uiSrefModal, $state.current.name);
var params = null, url = null, base = stateContext(element) || $state.$current;
var newHref = null, isAnchor = element.prop("tagName") === "A";
var isForm = element[0].nodeName === "FORM";
var attr = isForm ? "action" : "href", nav = true;
var options = { relative: base, inherit: true };
var optionsOverride = scope.$eval(attrs.uiSrefModalOpts) || {};
angular.forEach(allowedOptions, function(option) {
if (option in optionsOverride) {
options[option] = optionsOverride[option];
}
});
var update = function(newVal) {
if (newVal) params = angular.copy(newVal);
if (!nav) return;
newHref = $state.href(ref.state, params, options);
if (newHref === null) {
nav = false;
return false;
}
attrs.$set(attr, newHref);
};
if (ref.paramExpr) {
scope.$watch(ref.paramExpr, function(newVal, oldVal) {
if (newVal !== params) update(newVal);
}, true);
params = angular.copy(scope.$eval(ref.paramExpr));
}
update();
if (isForm) return;
element.bind("click", function(e) {
var button = e.which || e.button;
if ( !(button > 1 || e.ctrlKey || e.metaKey || e.shiftKey || element.attr('target')) ) {
e.preventDefault();
var state = $state.get(ref.state);
var modalInstance = $modal.open({
template: '<div>\
<div class="modal-header">\
<h3 class="modal-title">' + ref.state + '</h3>\
</div>\
<div class="modal-body">\
<ng-include src="\'' + state.templateUrl + '\'"></ng-include>\
</div>\
</div>',
controller: state.controller,
resolve: options.resolve
});
modalInstance.result.then(function (selectedItem) {
$scope.selected = selectedItem;
}, function () {
console.log('Modal dismissed at: ' + new Date());
});
}
});
}
};
}
You can use it like this <a ui-sref-modal="notes.add">Add Note</a>
Directive requires the angular-bootstrap to resolve the modal dialog. You will need to require the ui.router.modal module in your app.

Since asked to provide an example for my comment,
Example Directive
myapp.directive('openModal', function ($modal) {
return function(scope, element, attrs) {
element[0].addEventListener('click', function() {
$modal.open({
templateUrl : attrs.openModal,
controller: attrs.controller,
size: attrs.openModalSize,
//scope: angular.element(element[0]).scope()
});
});
};
});
Example Html
<button
open-modal='views/poc/open-modal/small-modal.html'
open-modal-size='sm'
controller="MyCtrl">modal small</button>
The above directive approach is not very different from using a state, which has templateUrl and controller except that url does not change.
.state('state1.list', {
url: "/list",
templateUrl: "partials/state1.list.html",
controller: function($scope) {
$scope.items = ["A", "List", "Of", "Items"];
}
})

Apparently there is the issue Ui-sref not generating hash in URL (Angular 1.3.0-rc.3) refering to
https://github.com/angular-ui/ui-router/issues/1397
It is seems to be fixed as per comments.
I personally dislike html5mode because it requires extra work on your server for no apparent advantage (I don't regard having "more beautiful url" as tangible advantage to justify the extra work).
There is another performance problem when using routers, that the view DOM is re-created upon each route change. I mentioned a very simple solution
in this answer.
As a side remark, the example in http://dev.driz.co.uk/AngularModal/ does not behave quite well. It does not record history, so I can't go back. Further, if you click on links like Index or modals, and then reload, you don't get the same page.
UPDATE.
It seems from the comments that a route change is not wanted when opening the modal. In that case the easiest solution is not to put ui-sref on the opening button and let the modal directive along handle it.

Related

Angular auto trigger specific directive in ng-repeat

I have an interesting situation.
I have a directive with isolate scope that generate list of numbers and the user can choose numbers like in lottery.
The problem i have is that i required minimum of 1 line, if the user pick only one line so when he click play i want to auto trigger the next directive in the ng-repeat to pick for him numbers, I made this plunker so you guys can understand better and help me.
http://plnkr.co/edit/vWGmSEpinf7wxRUnqyWq?p=preview
<div ng-repeat="line in [0,1,2,3]">
<div line line-config="lineConfig">
</div>
</div>
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
$scope.lineConfig = {
guessRange: 10
}
$scope.lines = [];
$scope.$on('lineAdded', function(event, line) {
$scope.lines.push(line);
});
$scope.play = function() {
/// here i want to check if $scope.lines.length
//is less then one if yes then auto trigger the next
//line directive to auto do quick pick and continue
}
})
.directive('line', function() {
return {
restrict: 'A',
templateUrl: 'line.html',
scope: {
lineConfig: '='
},
link: function($scope, elem, attr) {
var guessRange = $scope.lineConfig.guessRange;
$scope.cells = [];
$scope.line = {
nums: []
};
$scope.$watch('line', function(lotLine) {
var finaLine = {
line: $scope.line
}
if ($scope.line.nums.length > 4) {
$scope.$emit('lineAdded', finaLine);
}
}, true);
_(_.range(1, guessRange + 1)).forEach(function(num) {
$scope.cells.push({
num: num,
isSelected: false
});
});
$scope.userPickNum = function(cell) {
if (cell.isSelected) {
cell.isSelected = false;
_.pull($scope.lotLine.nums, cell.num);
} else {
cell.isSelected = true;
$scope.lotLine.nums.push(cell.num);
}
};
$scope.quickPick = function() {
$scope.clearLot();
$scope.line.nums = _.sample(_.range(1, guessRange + 1), 5);
_($scope.line.nums).forEach(function(num) {
num = _.find($scope.cells, {
num: num
});
num.isSelected = true;
});
}
$scope.clearLot = function() {
_($scope.cells).forEach(function(num) {
num.isSelected = false;
});
$scope.line.nums = [];
}
}
}
})
You could pass the $index (exists automatically in the ng-repeat scope) - variable into the directive and cause it to broadcast an event unique for ($index + 1) which is the $index for the next instance.
The event could be broadcasted from the $rootScope or a closer scope that's above the repeat.
Then you could capture the event in there.
Probably not the best way to do it.
I can try to elaborate if anything is unclear.
EDIT
So I played around alittle and came up with this:
http://plnkr.co/edit/ChRCyF7yQcN580umVfX1?p=preview
Rather
Rather than using events or services I went with using a directive controller to act as the parent over all the line directives inside it:
.directive('lineHandler', function () {
return {
controller: function () {
this.lines = [];
}
}
})
Then requiring 'lineHandler' controller inside the 'line' directive - the controller being a singleton (same instance injected into all the line directives) - you can then setup that controller to handle communication between your directives.
I commented most of my code in the updated plnkr and setup an example of what I think you requested when clicking in one list - affecting the one beneath.
I hope this helps and if anything is unclear I will try to elaborate.

Angular Inter-Directive Communication

I have created the following simple code with several directives (I have experience with Angular only for several months).
I have used unusual way to make some basic inheritance (as You can see from the Directive class), parameters by default (using ||), possibility to insert one element into another (using transclude), have added directory-specific controllers and of course, specified the link function to support DOM events handlers.
Currently I am trying to call function specified in another directory controller through it's scope, but it fails. The error is 'Uncaught TypeError: undefined is not a function'.
My target is to call controller's function independently from the DOM structure (the target element could be sibling, parent or child). I have considered also shared service and broadcast events, but it seems not the right way to make the communication.
Why the code doesn't work properly?
Are there any other ways to make it?
Thanks in advance.
HTML
<body ng-app="app">
<component id="button" name="button">
</component>
<icon name="icon">
</icon>
</body>
JS
(function() {
var app = angular.module('app', []);
app.directive('icon', function() {
var directive = new Directive();
return directive;
});
app.directive('button', function() {
var directive = new Directive();
return directive;
});
function Directive(options) {
options = options || {};
this.scope = options.scope || {
name: '#'
};
this.restrict = options.restrict || 'E';
this.replace = (options.replace != null) ? options.replace : true;
this.transclude = (options.transclude != null) ? options.transclude : true;
this.template = options.template || '<div ng-transclude></div>';
this.controller = function($scope) {
$scope.focus = function() {
console.log('focus');
};
$scope.outFocus = function() {
console.log('outFocus');
};
};
this.link = options.link || function($scope, $element, $attrs) {
$element.click(function() {
$scope.focus();
var _scope = angular.element('#button').scope();
_scope.outFocus(); // doesn't work
});
};
}
})();

AngularJS $emit does not fire the event after added code to unregister

I just found out how to communicate between controllers using $broadcast and $emit, tried it in my POC and it worked, sort of, the original problem described in this other post is still not solved but now I have another question, the event is being registered multiple times so I am trying to unregister it the way I've seen it in multiple posts here on SO but now the event won't fire. The code is as follows:
tabsApp.controller('BasicOverviewController', function ($scope, $location, $rootScope) {
var unbind = $rootScope.$on('displayModal', function (event, data) {
if (data.displayModal) {
alert("I want to display a modal!");
var modal = $('#basicModal');
modal.modal('toggle');
}
});
$scope.$on('$destroy', function () {
unbind();
});
});
tabsApp.controller('SportsController', function SportsController($scope, $location, $rootScope) {
$scope.goToOverview = function (showModal) {
$location.path("overview/basic");
$rootScope.$emit('displayModal', { displayModal: showModal })
};
});
If I remove the
var unbind = ...
the event fires and I can see the alert. As soon as I add the code to unregister the event, the code is never fired. How can the two things work together?
Could you just pull out unbind into its own function, and use it in both like this?
tabsApp.controller('BasicOverviewController', function ($scope, $location, $rootScope) {
var unbind = function (event, data) {
if (data.displayModal) {
alert("I want to display a modal!");
var modal = $('#basicModal');
modal.modal('toggle');
}
};
$rootScope.$on('displayModal', unbind);
$scope.$on('$destroy', unbind);
});
I could be wrong but my guess would be that the BasicOverviewController isn't being persisted and it's scope is being destroyed before the SportsController gets a chance to utilize it. Without a working example, I can't deduce much more. If you want to maintain this on $rootScope then a possible pattern would be:
if (!$rootScope.displayModalDereg) {
$rootScope.displayModalDereg = $rootScope.$on('displayModal', function (event, data) {
if (data.displayModal) {
alert("I want to display a modal!");
var modal = $('#basicModal');
modal.modal('toggle');
}
});
This also allows you to check and see if there is an event registered so you can dereg it if needed.
if ($rootScope.displayModalDereg) {// this event has been registered
$rootScope.displayModalDereg();
$rootScope.dispalyModalDereg = undefined;
}
I would heavily suggested creating a displayModal directive that persists all of this instead of maintaining it on $rootScope. Obviously you would still $emit, or better yet, $broadcast from $rootScope, just not persist the dereg function there.
Here is an example of a modal directive I once wrote:
/**
*
* Modal Directive
*/
'use strict';
(function initModalDrtv(window) {
var angular = window.angular,
app = window.app;
angular.module(app.directives).directive('modalDrtv', [
'$rootScope',
function modalDrtv($rootScope) {
return {
restrict: 'A',
scope: {},
templateUrl: '/templates/modal.html',
replace: true,
compile: function modalCompileFn(tElement, tAttrs) {
return function modalLinkFn(scope, elem, attrs) {
scope.show = false;
scope.options = {
'title': '',
'message': '',
'markup': undefined,
'buttons': {
showCancel: false,
showSecondary: false,
secondaryAction: '',
primaryAction: 'Ok'
},
'responseName': ''
};
scope.respond = function(response) {
var r = '';
if (response === 1) {
r = scope.options.buttons.primaryAction;
} else if (response === 2) {
r = scope.options.buttons.secondaryAction;
} else {
r = response;
}
$rootScope.$broadcast(scope.options.responseName, r);
scope.show = false;
};
scope.$on('initIrpModal', function(event, data) {
if (angular.isUndefined(data)) throw new Error("Data missing from irp modal event");
scope.options.title = data.title;
scope.options.message = data.message;
scope.options.buttons.showCancel = data.buttons.showCancel;
scope.options.buttons.showSecondary = data.buttons.showSecondary;
scope.options.buttons.secondaryAction = data.buttons.secondaryAction;
scope.options.buttons.primaryAction = data.buttons.primaryAction;
scope.options.responseName = data.responseName;
scope.show = true;
});
}
}
}
}
]);
})(window);
This directive utilizes one modal and let's anything anywhere in the app utilize it. The registered event lives on its isolate scope and therefore is destroyed when the modal's scope is destroyed. It also is configured with a response name so that if a user response is needed it can broadcast an event, letting the portion of the app that initialized the modal hear the response.

Set Caret position in Input with AngularJS

I need to change the caret position of an input, where a given number of digits is added (Example).
app.controller('MainCtrl', function($scope, $element, $timeout, $filter) {
//$scope.val = '12';
$scope.$watch('val', function(newValue, oldValue) {
if (!isNaN(newValue)) {
if (newValue.length > 3) {
//Set Caret Position
}
}
});
});
Is it possible to do something like this example?
I need for example :
Input: 1234.
so the caret position will be 2.
New digit: 9
final: 12934
Thanks in advance.
I think that such kind of things look better in directives. For example:
app.directive('caret', function() {
function setCaretPosition(elem, caretPos) {
if (elem !== null) {
if (elem.createTextRange) {
var range = elem.createTextRange();
range.move('character', caretPos);
range.select();
} else {
if (elem.setSelectionRange) {
elem.focus();
elem.setSelectionRange(caretPos, caretPos);
} else
elem.focus();
}
}
}
return {
scope: {value: '=ngModel'},
link: function(scope, element, attrs) {
var caret = Number(attrs.caret);
scope.$watch('value', function(newValue, oldValue) {
if (newValue && newValue != oldValue && !isNaN(newValue) && newValue.length > (caret + 1)) {
setCaretPosition(element[0], caret);
}
});
}
};
});
Usage:
<input ng-model='val' caret="2" />
I used setCaretPosition function for cross browser cursor positioning from this answer.
Demo: http://plnkr.co/edit/5RSgzvyd8YOTaXPsYr8A?p=preview
I think that the best approach for this is to make a reusable directive as we are dealing with DOM manipulation.
Link to the demo: http://plnkr.co/edit/qlGi64VO1AOrNpxoKA68?p=preview
var app = angular.module('angularjs-starter', []);
app.controller('MainCtrl', function($scope, $element, $timeout, $filter) {
$scope.$watch('val', function(newValue, oldValue) {
if (!isNaN(newValue)) {
if (newValue.length > 3) {
// $element.find('input')[0].selectionEnd = 2;
}
}
});
});
app.directive('setCaret', function() {
return {
restrict: 'A',
link: function(scope,element,attrs) {
var changed = false;
element.bind('keypress', function() {
if(element[0].selectionStart > 3 && !changed) {
changed = true;
element[0].selectionEnd = parseInt(attrs.position, 10);
}
})
},
}
})
You can see in the commented out part in the controller we can have access to this by using $element, but as this is DOM and controllers are not for DOM manipulation we need to make this into a directive.
I also had the same problem.
I thought to solve it creating an appropriate directive. You can find it here. Enjoy it!
Usage
Include directive, declare it by caret-aware attribute
<script src="https://cdn.rawgit.com/leodido/ng-caret-aware/master/caretaware.min.js"></script>
<script type="text/javascript">
var app = angular.module('myModule', ['leodido.caretAware']);
</script>
...
<div data-ng-app="app">
<input type="text" name="myname" caret-aware="cursor"/>
</div>
Then on the scope you'll have a variable cursor containing the position of the caret in the input named myname.
Nevertheless, this directive's controller exposes an API
getPosition
setPosition
For other usage examples see example directory of the above linked github repository.
I believe you could do it by using .setSelectionRange() on your input. I updated your example - see if this is what you wanted: http://plnkr.co/edit/bIJAPPAzkzqLIDUxVlIy?p=preview
Note: setSelectionRange is not supported by IE8 (see https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement.setSelectionRange), so if you need to support IE < 9, you'll need to look for shims.
I jsfiddled a working solution.
So basically, you have to create a directive :
app.directive('keypressdetector', function($compile){
return {
restrict:'AEC',
link: function(scope, element, attrs){
element.bind("keypress", function (event) {
if(event.which === 13) {
var selectionStart = element[0].selectionStart;
var value = element.val();
var valueLength = value.length;
var newValue= '';
if (selectionStart == valueLength){
newValue = value;
} else {
newValue = value.substring(selectionStart, valueLength);
}
var newElement = angular.element('<input type="text" value="' + newValue +'"/>')
angular.element(document.body).append(newElement);
}
});
}
};
});
Your controller would be useless in that situation.
You can invoke the directive like this (see : keypressdetector) :
<div ng-app="myapp">
<div ng-controller="LoginController">
<div>Hello {{ user.firstName }}</div>
<input ng-model="user.firstName" keypressdetector />
<input type="submit" ng-click="login()" value="Login"/>
<div ng-repeat="login in logins">{{ login }}</div>
</div>
</div>
See demo : https://jsfiddle.net/Lt7aP/3468/

When does the directive replace the element with the template?

I'm trying to write an AngularJS directive in which I replace the element with my specified HTML tags, then attach a keypress handler that calls a function on the directive's controller. Unfortunately that is not working for me. First off, here's the code I'm working with:
.directive('amsNewEntry', function() {
return {
restrict: 'E',
replace: true,
scope: {
url: '#navigateTo'
},
compile: function (tElement, tAttrs) {
var text = tAttrs.text;
var html = '<div>'
+ ' ' + text + ''
+ ' <input data-ng-show="isInputFieldVisible" type="text" data-ng-model="inputValue" />'
+ '</div>';
tElement.replaceWith(html);
return {
post: function (scope, element, attrs) {
console.log(element);
console.log(scope);
var input = $(element).find('input');
if (input.length > 0) {
console.log('attaching handler');
input.keypress(function (e) {
alert('keypress');
var code = e.which || e.keyCode;
if (code === 13) {
alert('clicked enter');
scope.navigate();
}
});
}
}
}
},
controller: function ($scope, $location) {
$scope.isInputFieldVisible = false;
$scope.url = null;
$scope.showInput = function () {
$scope.isInputFieldVisible = true;
}
$scope.inputValue = '';
$scope.navigate = function () {
var target = $scope.url + '/' + $scope.inputValue;
console.log(target);
$location.path(target);
}
}
The thing is, in the post-linking function returned from the compile function, when I console out the element variable, it is still not being compiled to the HTML I specified. Shouldn't the tElement.replaceWith(html) line do that?
And one last thing, the input field renders visible on the screen although I'm using the ng-show directive and binding it to the isInputFieldVisible property which is initialized to false. Any ideas why this is not working?

Categories

Resources