Angular auto trigger specific directive in ng-repeat - javascript

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.

Related

how to pass data from service to directive in angular.js

I have an alert service which shows alerts on top of the page. I have written a service and a directive which feeds off of the data coming from the service.
However, when i add a service using teh alert service and pass it to the directive, it does not show up, the alert
here is my code
The template
<div class="alert alert-{{alert.type}}">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true" ng-click="close()">×</button>
<div ng-bind="::alert.message" ></div>
</div>
Alert Service and directive
angular.module('test')
.service('alertService', function() {
var alerts = [];
this.add = function(type, msg) {
var self = this;
var alert = {
type: type,
msg: msg,
close: function() {
return self.closeAlert(alert);
}
};
return alerts.push(alert);
};
this.closeAlert = function(alert) {
return this.closeAlertIdx(alerts.indexOf(alert));
};
this.closeAlertIdx = function(index) {
return alerts.splice(index, 1);
};
this.clear = function() {
alerts = [];
};
this.getAlerts = function() {
return alerts;
};
})
.directive('alertList', ['alertService', function(alertService) {
return {
restrict: 'EA',
templateUrl: 'templates/alert/alert.html',
replace: true,
link: function(scope) {
scope.alerts = alertService.getAlerts();
}
};
}]);
In the index.html , i have referenced the alert-list directive
<div>
<alert-list ng-repeat="alert in alerts">
</alert-list>
</div>
In my controller i have,
alertService.add('info', 'This is a message');
I see that the alertService adds the alert to the array, but when i put a breakpoint in the link function of the directive, it never gets called
services are function that return an object, so you had to modify your service to be more or less like this:
.service('alertService', function() {
var alerts = [];
return{
add : function(type, msg) {
var self = this;
var alert = {
type: type,
msg: msg,
close: function() {
return self.closeAlert(alert);
}
};
return alerts.push(alert);
},
closeAlert: function(alert) {
return this.closeAlertIdx(alerts.indexOf(alert));
},
closeAlertIdx : function(index) {
return alerts.splice(index, 1);
},
clear: function() {
alerts = [];
},
getAlerts: function() {
return alerts;
}
})
The link function is only called once, when the directive element is created. When your app starts up, the link function will be called, and the scope.alerts will be set to an empty list.
I think you need to move the ng-repeat to the outer div of the alert template, rather than on the alert-list element.
Since the link function is only called once, and the identity of the array can change if you call alertService.clear, you'll probably have better luck putting a watch in your alert's link statement:
link: function(scope) {
scope.$watchCollection(alertService.getAlerts, function(alerts) {
scope.alerts = alerts;
});
}
Since this method doesn't directly do any DOM manipulation, modern angular best-practice would probably be to implement this as a component instead.

Angular - removing class after time

I am building a sort of (faux) loader in Angular. Currently, I have this:
const app = angular.module('app', []);
app.controller('loaderCtrl', ($scope, $timeout) => {
let loading = $scope.loading,
loaded = $scope.loaded;
$scope.reset = () => {
$timeout(() => {
loading = false;
loaded = false;
console.log(loaded);
}, 500);
}
});
HTML:
<main ng-app="app">
<div ng-controller="loaderCtrl as loader" >
<div class="loader" ng-class="{ '-loading' : loader.loading === true, '-loaded' : loader.loaded === true }"></div>
<button ng-click="loader.loading = true;">loading</button>
<button ng-click="loader.loaded = true; reset();">loaded</button>
</div>
</main>
CodePen: http://codepen.io/tomekbuszewski/pen/WrXXdp
My problem is, both loading and loaded aren't being set up for my view, so the classes are permanently there. What can I do?
So, this is a problem of scope. Basically when you do this
let loading = $scope.loading,
loaded = $scope.loaded;
You get the "value" of the variables inside Angular scope. Therefore Angular does not know anything about changes made to those
The fix is simple, don't do that, but instead
$scope.reset = () => {
$timeout(() => {
$scope.loading = false;
$scope.loaded = false;
}, 500);
}
Why not using an object and change its content? It is possible to do that as #beaver pointed out, but then you have another problem, you need to trigger the digest cycle yourself via $apply. And somewhere in your code, you might accidentally change the content of the object and it might affect other part of the system
Having said that I do not know Babel and so I worked on the JS compiled version, I noticed that you assigned loader.loading and loader.loaded to variables and then used those "references" in $timeout function.
As in javascript
Primitives are passed by value, Objects are passed by "copy of a
reference"
you have to use $scope.loader.loading and $scope.loader.loaded
app.controller('loaderCtrl', function ($scope, $timeout) {
$scope.loader = {};
var loading = $scope.loader.loading, loaded = $scope.loader.loaded;
$scope.reset = function () {
$timeout(function () {
$scope.loader.loading = false;
$scope.loader.loaded = false;
}, 500);
};
});
Here I forked your CodePen: http://codepen.io/beaver71/pen/wMPprm

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 $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.

AngularJS - same timer for all directive instances

What is a best "Angular Way" to implement the directive that will have a shared timer for all it instances?
For example I have a directive "myComponent" and on the page it appears many times.
Inside of the component, exists some text that blink with some interval.
Because of business requirements and performance considerations, I would like that there will be single "timeout" that will toggle the blink for all instances at once (after document is ready).
I thought about the writing some code within directive definition:
//Pseudo code
angular.module("app",[]).directive("myComponent", function($timeout){
$(function() { $timeout(function(){ $(".blink").toggle(); }, 3000); } );
return {
//Directive definition
};
});
Or by using some kind of service that will receive the $element and add remove class to it:
//Pseudo code
angular.module("app",[])
.service("myService", function($timeout){
var elements = [];
this.addForBlink = function(element) { elements.push(element) };
$(function() { $timeout(function(){ $(elements).toggle(); }, 3000); } );
})
.directive("myComponent", function(myService){
return {
compile:function($element){
myService.addForBlink($element);
return function() {
//link function
}
}
};
});
In my opinion the most elegant and efficient would be to combine both these approaches by specifying the logic of the directive in the very directive initialization function. Here is a scaffold of what I actually mean:
app.directive('blinking', function($timeout){
var blinkingElements = [];
var showAll = function() {
for(var i = 0; i < blinkingElements.length; i++){
blinkingElements[i].addClass("blinking");
}
};
var hideAll = function() {
for(var i = 0; i < blinkingElements.length; i++){
blinkingElements[i].removeClass("blinking");
}
};
var blink = function () {
$timeout(showAll, 500);
$timeout(function(){
hideAll();
if (blinkingElements.length > 0) {
blink();
}
}, 1000);
};
return {
link : function(scope, element, attrs){
blinkingElements.push(element);
if (blinkingElements.length == 1) {
blink();
}
element.on("$destroy", function(){
var index = blinkingElements.indexOf(element);
blinkingElements.splice(index, 1);
});
}
}
});
And here is the working demo.
Moreover you can inject some service that will be responsible for configuration (setting the intervals and / or class) or you can provide the configuration by passing an object directly to the attribute. In the latter case you can enable applying different classes for different elements, but you should think of some policy how to deal with situation, when the interval was set more than once.

Categories

Resources