Same AngularJS custom directive on same page - angularjs

I have a custom search directive and need to use multiple instances of it on the same page. The page makes use of bootstrap tabs and there will be an instance of this search component in each tab.
The issue is that the search directive in the second tab is overriding the callback of the search directive in the first tab. Here is a snippet of my search directive:
class SearchDirective {
constructor($timeout) {
this.require = '^ngModel';
this.restrict= "AE";
this.$timeout = $timeout;
this.scope = {
ngModel: '=',
searchTime: '=',
searchCallback: '&'
};
}
compile(tElem, tAttrs) {
return this.link.bind(this);
}
link(scope, element, attrs) {
this.scope = scope;
var timer = null;
scope.$watch('ngModel', (value, preValue) => {
if (value === preValue) return;
if (timer) {
this.$timeout.cancel(timer);
}
timer = this.$timeout(() => {
timer = null;
if (value.length === 0) {
this.scope.searchCallback();
}
}, this.scope.searchTime)
});
}
}
And here is a snippet of the HTML for the search component on the first tab:
<input search search-callback="searchWindowsController.searchPcs()" search-time="600" data-ng-model="searchWindowsController.searchQuery" type="text" class="searchBox" placeholder="Search Windows...">
And this is what i have in the second tab:
<input search search-callback="searchMacController.searchPcs()" search-time="600" data-ng-model="searchMacController.searchQuery" type="text" class="searchBox" placeholder="Search Macs...">
For some reason when you search using the Windows search, it is calling the Mac callback. Can someone point me to what I am doing wrong? I am new to custom directives.

The error due to this within the $timeout function.
See live example on jsfiddle.
'use strict'
var myApp = angular.module('myApp', []);
myApp.controller('MyCtrl', function($scope, $log) {
$scope.callback1 = function(){
console.log('callback1');
};
$scope.callback2 = function(){
console.log('callback2');
};
})
.directive('search',function($timeout){
return new SearchDirective($timeout);
});
class SearchDirective {
constructor(timeout) {
this.require = '^ngModel';
this.restrict = "AE";
this.$timeout = timeout;
this.scope = {
ngModel: '=',
searchTime: '=',
searchCallback: '&'
};
}
compile(tElem, tAttrs) {
return this.link.bind(this);
}
link(scope, element, attrs) {
this.scope = scope;
var timer = null;
scope.$watch('ngModel', (value, preValue) => {
if (value === preValue) return;
if (timer) {
this.$timeout.cancel(timer);
}
timer = this.$timeout(() => {
timer = null;
if (value.length === 0) {
scope.searchCallback();
}
}, scope.searchTime)
});
}
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
<div ng-app="myApp">
<div ng-controller="MyCtrl">
<input search search-callback="callback1()" search-time="600" data-ng-model="searchQuery1" type="text" class="searchBox" placeholder="Search Mac...">
<input search search-callback="callback2()" search-time="600" data-ng-model="searchQuery2" type="text" class="searchBox" placeholder="Search Windows...">
</div>
</div>

Related

AngularJS - view not updated when directive changes data in service

I'm using a directive to interact with a service and am encountering some trouble with the view showing the latest data.
I setup the following example. You can see that when a controller interacts with the Service, the view will update with the latest data. If you click the directive link, you can see in the console the data was changed but the view is not updated with that data.
http://jsfiddle.net/kisonay/pv8towqc/
What am I missing?
JavaScript:
var app = angular.module('myApp', []);
app.factory('Service', function() {
var Service = {};
Service.data = {};
Service.data.active = false;
Service.data.one = {};
Service.data.many = [];
Service.changeActive = function(state) {
state = state || false;
Service.data.active = state;
};
Service.setOne = function(one) {
Service.data.one = one;
};
Service.setMany = function(many) {
Service.data.many = many;
};
return Service;
});
app.directive('launcher', function(Service) {
return {
restrict: "A",
link: function(scope, elem, attrs) {
elem.bind('click', function(event) {
if (Service.data.active) {
Service.changeActive(false);
} else {
Service.changeActive(true);
}
console.log(Service.data); // shows data changed
});
}
};
});
function Ctrl1($scope, Service) {
$scope.ServiceData = Service.data;
}
function Ctrl2($scope, Service) {
$scope.active = function() {
Service.changeActive(true);
};
$scope.inactive = function() {
Service.changeActive(false);
};
}
HTML
<div ng-controller="Ctrl1">
{{ServiceData}}
</div>
<hr />
<div ng-controller="Ctrl2">
Directive: <a href="#" launcher>Change</a>
<hr /> Controller:
<button ng-click="active()">
Active
</button>
<button ng-click="inactive()">
Inactive
</button>
</div>
Your event listener executes but Angular doesn't know anything about it, so it doesn't know it has to detect changes.
Add scope.$apply(); at the evn of the click listener, and it will work as expected.
app.directive('launcher', function(Service) {
return {
restrict: "A",
link: function(scope, elem, attrs) {
elem.bind('click', function(event) {
scope.$apply(function() {
if (Service.data.active) {
Service.changeActive(false);
} else {
Service.changeActive(true);
}
});
});
}
};
});

Predefine error message with angular validation custom directive

I am doing angular validation as follows:
<form name="form" ng-submit="vm.create(vm.job)" validation="vm.errors">
<input name="vm.job.position" type="text" ng-model="vm.job.position" validator />
When the form is submitted the directive gets the name of the property, e.g. position, from the ng-model. It then checks if vm.errors has a message for that property. If yes then adds a span with the error message after the input.
However, I would also like to use the same directive in another way:
<form name="form" ng-submit="vm.create(vm.job)" validation="vm.errors">
<input name="vm.job.position" type="text" ng-model="vm.job.position" />
<span class="error" validator="position"></span>
In this case I removed the validator from the input and added the span already allowing me to control where the error will be displayed. In this case I am using validator="position" to define to which model property the error message is associated.
I am not sure how should I add this functionality to my current code ... Any help is appreciated.
The following is all the code I have on my directives:
(function () {
"use strict";
angular.module("app").directive("validation", validation);
function validation() {
var validation = {
controller: ["$scope", controller],
replace: false,
restrict: "A",
scope: {
validation: "="
}
};
return validation;
function controller($scope) {
var vm = this;
$scope.$watch(function () {
return $scope.validation;
}, function () {
vm.errors = $scope.validation;
})
}
}
angular.module("app").directive("validator", validator);
function validator() {
var validator = {
link: link,
replace: false,
require: "^validation",
restrict: "A"
};
return validator;
function link(scope, element, attributes, controller) {
scope.$watch(function () {
return controller.errors;
}, function () {
if (controller.errors) {
var result = controller.errors.filter(function (error) {
if (error.flag == null)
return false;
var position = attributes.name.lastIndexOf(".");
if (position > -1)
return attributes.name.slice(position + 1).toLowerCase() === error.flag.toLowerCase();
else
return attributes.name.toLowerCase() === error.flag.toLowerCase();
});
if (result.length > 0) {
var error = element.siblings("span.error").first();
if (error.length == 0)
element.parent().append("<span class='error'>" + result[0].info + "</span>");
else
error.text(result[0].info);
} else {
element.siblings("span.error").first().remove();
}
}
}, true);
}
}
})();

Angular modal dialog set inside list items

I'm trying to set a list of two items that open separate modal dialogs in a node.js app. I'm using Jade.
Here's the Jade:
button.login-button(type='button' ng-app='ng-modal') Login
ul
li(open-dialog='modal-to-open') Login
// JUST WORKING ON SIGN UP FOR NOW
li Sign Up
modal-dialog(show='dialogShown' dialog-title='My Dialog' height='100px' width='100px')
p Working
div.loginForm
form(name='loginForm' method='post' action='#' enctype='text/plain')
label(for='user') Username or Email
input(type='text' name='username' id='username' size="39" placeholder='Username or Email' required)
label(for='password') Password
input(type='password' name='password' id='password' size='39' placeholder='Password' required)
I'm using Adam Brecht's modal dialog plugin. I have the js file and the css files attached.
I changed the declaration of the module in the js file to this:
app = angular.module("myApp", ["ngModal"])
I have the list set as a dropdown in my CSS. I wanted the form to display in a modal dialog when the link in the list is clicked, but at the moment the form displays below dropdown box.
What am I missing?
EDIT: This is the js file:
(function() {
var app;
app = angular.module("myApp", ["ngModal"])
app.provider("ngModalDefaults", function() {
return {
options: {
closeButtonHtml: "<span class='ng-modal-close-x'>X</span>"
},
$get: function() {
return this.options;
},
set: function(keyOrHash, value) {
var k, v, _results;
if (typeof keyOrHash === 'object') {
_results = [];
for (k in keyOrHash) {
v = keyOrHash[k];
_results.push(this.options[k] = v);
}
return _results;
} else {
return this.options[keyOrHash] = value;
}
}
};
});
app.directive('modalDialog', [
'ngModalDefaults', '$sce', function(ngModalDefaults, $sce) {
return {
restrict: 'E',
scope: {
show: '=',
dialogTitle: '#',
onClose: '&?'
},
replace: true,
transclude: true,
link: function(scope, element, attrs) {
var setupCloseButton, setupStyle;
setupCloseButton = function() {
return scope.closeButtonHtml = $sce.trustAsHtml(ngModalDefaults.closeButtonHtml);
};
setupStyle = function() {
scope.dialogStyle = {};
if (attrs.width) {
scope.dialogStyle['width'] = attrs.width;
}
if (attrs.height) {
return scope.dialogStyle['height'] = attrs.height;
}
};
scope.hideModal = function() {
return scope.show = false;
};
scope.$watch('show', function(newVal, oldVal) {
if (newVal && !oldVal) {
document.getElementsByTagName("body")[0].style.overflow = "hidden";
} else {
document.getElementsByTagName("body")[0].style.overflow = "";
}
if ((!newVal && oldVal) && (scope.onClose != null)) {
return scope.onClose();
}
});
setupCloseButton();
return setupStyle();
},
template: "<div class='ng-modal' ng-show='show'>\n <div class='ng-modal-overlay' ng-click='hideModal()'></div>\n <div class='ng-modal-dialog' ng-style='dialogStyle'>\n <span class='ng-modal-title' ng-show='dialogTitle && dialogTitle.length' ng-bind='dialogTitle'></span>\n <div class='ng-modal-close' ng-click='hideModal()'>\n <div ng-bind-html='closeButtonHtml'></div>\n </div>\n <div class='ng-modal-dialog-content' ng-transclude></div>\n </div>\n</div>"
};
}
]);
}).call(this);
I just realized I haven't changed the template.

AngularJS binding issues and iteration loops

I have this factory:
.factory('Options', function () {
var getOptions = function () {
var storageData = sessionStorage.siteOptions;
if (storageData !== 'undefined')
return angular.fromJson(storageData);
return {
rotateBackground: false,
enableMetro: true
};
};
var saveOptions = function (options) {
sessionStorage.siteOptions = angular.toJson(options);
}
return {
get: getOptions,
save: saveOptions
};
});
which works fine on my profile page:
.controller('ProfileController', ['Options', function (options) {
var self = this;
self.options = options.get();
self.save = function () {
options.save(self.options);
}
}]);
The html looks like this:
<div class="row" ng-controller="ProfileController as profile">
<div class="large-4 columns">
<h2>Site options</h2>
<form name="optionsForm" ng-submit="profile.save()" role="form">
<div class="row">
<div class="large-12 columns">
<input id="enable-metro" type="checkbox" ng-model="profile.options.enableMetro"><label for="enable-metro">Enable metro design</label>
</div>
<div class="large-12 columns">
<input id="enable-background-rotate" type="checkbox" ng-model="profile.options.rotateBackground"><label for="enable-background-rotate">Enable rotating background</label>
</div>
<div class="large-12 columns">
<button class="button">Save</button>
</div>
</div>
</form>
</div>
</div>
But I have this other page that has a controller that needs to be aware if the options are ever saved. Basically, if saveOptions is ever called, then I need any page that looks at options to be notified.
The reason for this, is for example:
.controller('MetroController', ['Options', function (options) {
scope.options = options.get();
scope.$watch(function () {
return options.get();
}, function () {
scope.options = options.get();
});
}])
// ---
// DIRECTIVES.
// ---
.directive('metro', function () {
return {
restrict: 'A',
controller: 'MetroController',
controllerAs: 'metro',
link: function (scope, element, attr) {
scope.$watch(function () {
return metro.options.enableMetro;
}, function (enableMetro) {
if (enableMetro) {
element.addClass('metro');
} else {
element.removeClass('metro');
}
});
}
}
});
As you can see, this is trying to apply a class based on the enableMetro flag. But when I run this, I get an error about the amount of iterations this has had to loop through.
Can someone help me with this?
I think I have this solved.
I changed my options factory to this:
.factory('Options', function () {
var getOptions = function () {
var storageData = sessionStorage.siteOptions;
if (storageData !== 'undefined')
return angular.fromJson(storageData);
return {
rotateBackground: false,
enableMetro: true
};
};
var saveOptions = function (options) {
sessionStorage.siteOptions = angular.toJson(options);
current = getOptions();
}
var current = getOptions();
return {
current: current,
save: saveOptions
};
});
then in my controllers, I just did this:
.controller('MetroController', ['$scope', 'Options', function ($scope, options) {
var self = this;
self.options = options.current;
$scope.$watch(function () {
return options.current;
}, function () {
self.options = options.current;
});
}])
// ---
// DIRECTIVES.
// ---
.directive('metro', function () {
return {
restrict: 'A',
controller: 'MetroController',
link: function (scope, element, attr, controller) {
scope.$watch(function () {
return controller.options.enableMetro;
}, function (enableMetro) {
if (enableMetro) {
element.addClass('metro');
} else {
element.removeClass('metro');
}
});
}
}
});
and that seems to work fine.

Accessing a service or controller in my link function - Angular.js

I have a directive, but I am having a problem access the controller and my service that is injected into it. Here is my directive:
angular.module('clinicalApp').directive('chatContainer', ['encounterService', function(encounterService) {
return {
scope: {
encounter: '=',
count: '='
},
templateUrl: 'views/chat.container.html',
controller: 'EncounterCtrl',
link: function(scope, elem, attrs, controller) {
scope.addMessage = function(message) {
//RIGHT HERE
scope.resetChat();
};
scope.resetChat = function() {
scope.chatText = '';
scope.updateCount(scope.chatText);
};
}
};
}]);
You can see that I am attaching a couple of functions to my scope inside the link function. Inside those methods, like addMessage, I don't have access to my controller or the service that is injected into the directive. How do I acceess the controller or service?
UPDATE
Here is the service:
angular.module('clinicalApp').factory('encounterService', function ($resource, $rootScope) {
var EncounterService = $resource('http://localhost:port/v2/encounters/:encounterId', {encounterId:'#id', port: ':8280'}, {
search: {
method: 'GET'
}
});
var newEncounters = [];
var filterTerms = {};
EncounterService.pushNewEncounter = function(encounter) {
newEncounters.push(encounter);
$rootScope.$broadcast('newEncountersUpdated');
};
EncounterService.getNewEncounters = function() {
return newEncounters;
}
EncounterService.clearNewEncounters = function() {
newEncounters = [];
}
EncounterService.setFilterTerms = function(filterTermsObj) {
filterTerms = filterTermsObj;
$rootScope.$broadcast('filterTermsUpdated');
EncounterService.getFilterTerms(); //filter terms coming in here, must redo the search with them
}
EncounterService.getFilterTerms = function() {
return filterTerms;
}
return EncounterService;
});
and the chat.container.html
<div class="span4 chat-container">
<h5 class="chat-header">
<span class="patient-name-container">{{encounter.patient.firstName }} {{encounter.patient.lastName}}</span>
</h5>
<div class="chat-body">
<div class="message-post-container">
<form accept-charset="UTF-8" action="#" method="POST">
<div class="text-area-container">
<textarea id="chatBox" ng-model="chatText" ng-keyup="updateCount(chatText)" class="chat-box" rows="2"></textarea>
</div>
<div class="counter-container pull-right">
<span class="muted" id="counter">{{count}}</span>
</div>
<div class="button-container btn-group btn-group-chat">
<input id="comment" class="btn btn-primary btn-small btn-comment disabled" value="Comment" ng-click="addMessage(chatText)"/>
</div>
</form>
<div messages-container messages="encounter.comments">
</div>
</div>
</div>
</div>
Here is Demo Plunker I played with.
I removed scope{....} from directive and added 2 values in controller and directive to see how they change regards to action.
JS
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
$scope.name = 'World';
// listen on any change of chatText in directive
$scope.$watch(function () {return $scope.chatText;},
function (newValue, oldValue) {
if (newValue == oldValue) {return;}
$scope.chatTextFromController = newValue;
}, true);
});
app.directive('chatContainer', ['encounterService', function(encounterService) {
return {
templateUrl: 'chat.container.html',
link: function(scope, elem, attrs) {
scope.countStart = scope.count;
scope.updateCount = function(chatText) {
alert('updateCount');
scope.count = scope.countStart - chatText.length;
};
scope.addMessage = function(message) {
alert('addMessage');
encounterService.sayhello(message);
scope.resetChat();
};
scope.resetChat = function() {
alert('resetChat');
scope.chatText = 'someone reset me';
scope.name = "Hello " + scope.name;
scope.updateCount(scope.chatText);
};
}
};
}]);
app.service('encounterService', function() {
var EncounterService = {};
EncounterService.sayhello = function(message) {
alert("from Service " + message);
};
return EncounterService;
});
HTML
<body ng-controller="MainCtrl">
<div chat-container></div>
<pre>chatText from directive: {{chatText|json}}</pre>
<pre>chatText from controller: {{chatTextFromController|json}}</pre>
<pre>name: {{name|json}}</pre>
</body>

Resources