When does the directive replace the element with the template? - javascript

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?

Related

Lazy loading on scrolling drop down options in angularjs

I am working on MEAN stack application in which I am defining a drop down using following customize directive. This drop down shows available variables from an array(around 70K options).
.directive('inputDropdown', function($parse) {
var template =
'<input class="form-control" ng-model="ngModel" ng-disabled="disabled" ng-focus="setDirectiveList()" ng-blur="removeDirectiveList()">' +
'<div class="dropdown dropdown1" input-dropdown="increaseLimit()" ng-init="limit=20;" ng-click="setDirectiveList()">' +
'<div class="form-control" ng-repeat="value in selectedList | filter:ngModel | limitTo:limit">' +
'<div ng-mousedown="select($event, value)">{{value}}</div>' +
'</div>' +
'</div>';
return {
restrict: 'EA',
require: '^form',
scope: {
ngModel: '=',
list: '=',
onSelect: '&',
disabled:'=ngDisabled'
},
template: template,
link: function(scope, element, attrs,mapController) {
element.addClass('input-dropdown');
var handler = $parse(attrs.inputDropdown);
element.scroll(function (evt) {
var scrollTop = element[0].scrollTop,
scrollHeight = element[0].scrollHeight,
offsetHeight = element[0].offsetHeight;
if (scrollTop === (scrollHeight - offsetHeight)) {
$scope.$apply(function () {
handler($scope);
});
}
});
if(scope.$parent.setDirty)
{
scope.makeFormDirty = mapController.$setDirty();
}
scope.select = function(e, value) {
scope.ngModel = value;
// scope.onSelect({$event: e, value: value});
scope.makeFormDirty = mapController.$setDirty();
};
scope.setDirectiveList = function() {
// debugger;
scope.selectedList = scope.list;//scope.$parent.variables;
}
scope.removeDirectiveList = function() {
scope.selectedList = [];
}
}
};
})
What I am trying to do is using limitTo option, increasing the the display variables count by some value when the scroll bar of drop down hits bottom. The increaseLimit function is defined in my controller as:
$scope.increaseLimit = function () {
if ($scope.limit < $scope.variables.length) {
$scope.limit += 20;
}
};
where $scope.variables contains 70K entries. I am not sure whether the I am adding increaseLimit to correct div or scrolling function is wrong.
I want to achieve this type of loading in my drop down. How to add this in my customize directive input-dropdown. I tried but no success.
Please Help.

AngularJS directive changing ability to modify scope variable

Here's a plunkr with my problem: http://plnkr.co/edit/Sx830ekQyP7YBqmRB4Nd?p=preview
Click "Open", then click on "5". Notice how it changes to "test"? Now, type something into Body. It'll either say "Say a little more..." or "Now for the title". Either way, click the button again, and notice how it doesn't change to "test"? Why not? If I remove the directive, the button changes to "test" with or without text in the body.
I know this has to do with the scope in the directive, but I don't understand what exactly is wrong. Can you explain? Thanks.
angular.module('plunker', ['ngDialog']).controller('MainCtrl', function($scope, ngDialog) {
//$scope.submitPostValue = "OK";
$scope.submitPost = function() {
$scope.submitPostValue = 'test';
};
$scope.open = function () {
console.log('open');
$scope.submitPostValue = '5';
ngDialog.openConfirm({
template: 'postModal',
showClose: true,
trapFocus: false,
scope: $scope,
}).then(function (success) {
}, function (error) {
});
};
}).directive('bodyValidator', function () {
return {
require: 'ngModel',
link: function (scope, element, attr, ctrl) {
function customValidator(ngModelValue) {
if(ngModelValue.length > 0){
if(ngModelValue.length < 10) {
scope.submitPostValue = "Say a little more...";
scope.bodyValid = false;
}
else {
scope.bodyValid = true;
if(scope.titleValid)
scope.submitPostValue = "Submit";
else
scope.submitPostValue = "Now for the title..."
}
}
else {
scope.submitPostValue = "Enter a body...";
scope.bodyValid = false;
}
return ngModelValue;
}
ctrl.$parsers.push(customValidator);
}
};
});
Try to wrap all your variables into an object.
Define $scope.obj = {}; first and change all your scope.submitPostValue to $scope.obj.submitPostValue. In your HTML, change ng-value='submitPostValue' to ng-value=obj.submitPostValue.

Loading state as a modal overlay in AngularJS

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.

AngularJS Masonry for Dynamically changing heights

I have divs that expand and contract when clicked on. The Masonry library has worked great for initializing the page. The problem I am experiencing is that with the absolute positioning in place from Masonry and the directive below, when divs expand they overlap with the divs below. I need to have the divs below the expanding div move down to deal with the expansion.
My sources are:
http://masonry.desandro.com/
and
https://github.com/passy/angular-masonry/blob/master/src/angular-masonry.js
/*!
* angular-masonry <%= pkg.version %>
* Pascal Hartig, weluse GmbH, http://weluse.de/
* License: MIT
*/
(function () {
'use strict';
angular.module('wu.masonry', [])
.controller('MasonryCtrl', function controller($scope, $element, $timeout) {
var bricks = {};
var schedule = [];
var destroyed = false;
var self = this;
var timeout = null;
this.preserveOrder = false;
this.loadImages = true;
this.scheduleMasonryOnce = function scheduleMasonryOnce() {
var args = arguments;
var found = schedule.filter(function filterFn(item) {
return item[0] === args[0];
}).length > 0;
if (!found) {
this.scheduleMasonry.apply(null, arguments);
}
};
// Make sure it's only executed once within a reasonable time-frame in
// case multiple elements are removed or added at once.
this.scheduleMasonry = function scheduleMasonry() {
if (timeout) {
$timeout.cancel(timeout);
}
schedule.push([].slice.call(arguments));
timeout = $timeout(function runMasonry() {
if (destroyed) {
return;
}
schedule.forEach(function scheduleForEach(args) {
$element.masonry.apply($element, args);
});
schedule = [];
}, 30);
};
function defaultLoaded($element) {
$element.addClass('loaded');
}
this.appendBrick = function appendBrick(element, id) {
if (destroyed) {
return;
}
function _append() {
if (Object.keys(bricks).length === 0) {
$element.masonry('resize');
}
if (bricks[id] === undefined) {
// Keep track of added elements.
bricks[id] = true;
defaultLoaded(element);
$element.masonry('appended', element, true);
}
}
function _layout() {
// I wanted to make this dynamic but ran into huuuge memory leaks
// that I couldn't fix. If you know how to dynamically add a
// callback so one could say <masonry loaded="callback($element)">
// please submit a pull request!
self.scheduleMasonryOnce('layout');
}
if (!self.loadImages){
_append();
_layout();
} else if (self.preserveOrder) {
_append();
element.imagesLoaded(_layout);
} else {
element.imagesLoaded(function imagesLoaded() {
_append();
_layout();
});
}
};
this.removeBrick = function removeBrick(id, element) {
if (destroyed) {
return;
}
delete bricks[id];
$element.masonry('remove', element);
this.scheduleMasonryOnce('layout');
};
this.destroy = function destroy() {
destroyed = true;
if ($element.data('masonry')) {
// Gently uninitialize if still present
$element.masonry('destroy');
}
$scope.$emit('masonry.destroyed');
bricks = [];
};
this.reload = function reload() {
$element.masonry();
$scope.$emit('masonry.reloaded');
};
}).directive('masonry', function masonryDirective() {
return {
restrict: 'AE',
controller: 'MasonryCtrl',
link: {
pre: function preLink(scope, element, attrs, ctrl) {
var attrOptions = scope.$eval(attrs.masonry || attrs.masonryOptions);
var options = angular.extend({
itemSelector: attrs.itemSelector || '.masonry-brick',
columnWidth: parseInt(attrs.columnWidth, 10) || attrs.columnWidth
}, attrOptions || {});
element.masonry(options);
var loadImages = scope.$eval(attrs.loadImages);
ctrl.loadImages = loadImages !== false;
var preserveOrder = scope.$eval(attrs.preserveOrder);
ctrl.preserveOrder = (preserveOrder !== false && attrs.preserveOrder !== undefined);
scope.$emit('masonry.created', element);
scope.$on('$destroy', ctrl.destroy);
}
}
};
}).directive('masonryBrick', function masonryBrickDirective() {
return {
restrict: 'AC',
require: '^masonry',
scope: true,
link: {
pre: function preLink(scope, element, attrs, ctrl) {
var id = scope.$id, index;
ctrl.appendBrick(element, id);
element.on('$destroy', function () {
ctrl.removeBrick(id, element);
});
scope.$on('masonry.reload', function () {
ctrl.scheduleMasonryOnce('reloadItems');
ctrl.scheduleMasonryOnce('layout');
});
scope.$watch('$index', function () {
if (index !== undefined && index !== scope.$index) {
ctrl.scheduleMasonryOnce('reloadItems');
ctrl.scheduleMasonryOnce('layout');
}
index = scope.$index;
});
}
}
};
});
}());
Like with many non-Angular libraries, it appears the answer lies in wrapping the library in an Angular directive.
I haven't tried it out but it appears that is what this person did
You can use angular's $emit, $broadcast, and $on functionality.
Inside your masonry directive link function:
scope.$on('$resizeMasonry', ctrl.scheduleMasonryOnce('layout'));
Inside your masonryBrick directive link function or any other child element:
scope.$emit('$resizeMasonry');
Use $emit to send an event up the scope tree and $broadcast to send an event down the scope tree.

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/

Categories

Resources