I am building a search application.I am using the highlighter function from Johann Burkard's JavaScript text higlighting jQuery plugin.
After an angularJS $Http call all the data is bound. I created a directive to call the Highlighter function.
searchApplication.directive('highlighter', function () {
return {
restrict: 'A',
link: function (scope, element) {
element.highlight(scope.searchText);
}
}
});
here is the controller
`searchApplication.controller('searchController', function ($scope, $http, searchService) {
$scope.myObject= {
searchResults: null,
searchText: "",
};
$scope.search = function () {
searchService.doSearch($scope.luceneSearch.searchText).then(
function (data) {
$scope.myObject.searchResults = data;
},
function (data, status, headers, configs) {
$log(status);
});
}
});`
here is the service
searchApplication.factory('searchService', function ($http, $q) {
return {
doSearch: function (_searchText) {
var deferred = $q.defer();
var searchURL = '/Search';
$http({
method: 'POST',
url: searchURL,
params: { searchText: _searchText }
})
.success(function (data, status, headers, configs) {
deferred.resolve(data);
})
.error(function (data, status, headers, configs) {
deferred.reject(data, status, headers, configs);
});
return deferred.promise;
}
}
});
In the html I have the following
<td ng-repeat="col in row.fields" highlighter>
{{col.Value}}
</td>
The directive is not getting the value to be searched, rather it gets {{col.Value}} and hence it is not able to highlight.
What am I doing wrong? How can I get the actual bound values so that I can manipulate it? Is there a better way to do this?
Updated: with controller and service code
From the code given, it should work fine if your controller has $scope.searchText properly set. I defined your directive in my app and debugged the link() function in Chrome, and scope.searchText is found as expected. If from browser debugging you find scope.searchText to be undefined you probably need to also post your controller code here.
UPDATE: From your comment, it seems the problem here is the execution order within Angular. Apparently the linking function is getting called before text interpolation is finished, so the solution is to wait for that process before proceeding with the highlighting part.
The trick here is to $watch for an update in col.Value and invoke the highlight logic afterward. The code below should do the trick:
app.directive('highlighter', function ($log) {
return {
restrict: 'A',
compile: function compile(element, attributes) {
// at this point, we still get {{col.Value}}
var preinterpolated = element.text().trim();
// removing the enclosing {{ and }}, so that we have just the property
var watched = preinterpolated.substring(2, preinterpolated.length - 2);
$log.info('Model property being watched: ' + watched);
return {
post: function (scope, element) {
// we watch for the model property here
scope.$watch(watched, function(newVal, oldVal) {
// when we reach here, text is already interpolated
$log.info(element.text());
element.highlight(scope.searchText);
});
}
};
}
};
});
This time around, $log logic should print out the interpolated value instead of just col.Value.
UPDATE2: I'm not quite sure how to go from there with directive, but if you don't mind using Angular filter, you can try this Angular UI solution. After attaching js file to your page, just include 'ui.highlight' module in your app and the filter will be available for use. It's also small like your jquery lib as well:
https://github.com/angular-ui/ui-utils/blob/master/modules/highlight/highlight.js
You can try live example here:
http://angular-ui.github.io/ui-utils/
Your HTML should now look like this (directive is no longer needed):
<td ng-repeat="col in row.fields">
{{col.Value | highlight:searchText}}
</td>
Also noted that now the CSS class for hightlighted text is ui-match instead of highlight.
UPDATE3: If you're set on using a directive, this person seems to do something very similar, except that he's adding a timeout:
http://dotnetspeak.com/2013/07/implementing-a-highlighting-directive-for-angular
setTimeout(function () {
element.highlight(scope.searchText);
}, 300);
Related
I have a directive for users to like (or "fave") posts in my application. Throughout my controllers I use $rootScope.$emit('name-of-function', some-id) to update user data when they like a new post, as this is reflected throughout my application. But I can't seem to use $rootScope.$emit in the directive. I receive an error
$rootScope.$emit is not a function
Presumably the $rootScope.$on event which corresponds with this command has not been called yet, so this function does not yet exist? What can be done about this? Is there a better way to arrange this?
var module = angular.module('directives.module');
module.directive('postFave', function (contentService, $rootScope) {
return {
restrict: 'E',
templateUrl: 'directives/post-fave.html',
scope: {
contentId: '#',
slug: '#'
},
link: function ($scope, $rootScope, element) {
$scope.contentFavToggle = function (ev) {
contentId = $scope.contentId;
contentService.contentFavToggle(contentId, ev).then(function (response) {
$rootScope.$emit('dataUpdated', $scope.slug);
if (response) {
$scope.favourite[contentId] = response;
} else {
$scope.favourite[contentId] = null;
}
});
};
console.log("track fave directive called");
}
};
});
from controller:
var dataUpdatedListener = $rootScope.$on('dataUpdated', function (event, slug) {
dataService.clearData(slug);
dataControllerInit();
});
How can I access this rootscope function from within the directive? Thanks.
FYI - "link" has been used in the directive because this is related to an instance of an HTML element which will be used a number of times on the page
link has the following signature, there is no need to add $rootScope injection into link function:
function link(scope, element, attrs, controller, transcludeFn) { ... }
Remove it from link and it will work.
I'm trying to write a directive for fancytree. The source is loaded through ajax and almost everything looks like a charm. The tree is correctly shown, events are firing nice, but the parameters get undefined at the controller side.
It looks strange, because when I set a function(event, data){ ... } for the events (like activate or beforeSelect as seen in the docs) both event and data are nicely set.
Where I'm doing it wrong?
Thank you in advance!
Directive
angular.module('MyAppModule', [])
.provider('MyAppModuleConfig', function () {
this.$get = function () {
return this;
};
})
.directive('fancytree', function () {
return {
restrict: 'E',
transclude: true,
replace: true,
scope: {
activateFn: '&',
//long list of events, all stated with "<sth>Fn : '&'"
selectFn: '&',
selectedNode: '=',
treeviewSource: '=',
enabledExtensions: '=',
filterOptions: '='
},
template: '<div id="treeview-container"></div>',
link: function (scope, element) {
element.fancytree({
source: scope.treeviewSource,
activate: function (event, data) {
console.log(event, data); // ok, parameters are all set
scope.activateFn(event, data);
// function fires right, but all parameters
// are logged as undefined
}
});
}
};
});
HTML
<fancytree ng-if="tvSource" treeview-source="tvSource"
activate-fn="genericEvt(event, data)"/>
Controller
TreeViewSvc.query()
.success(function (response) {
$timeout(function ()
{
$scope.tvSource = response;
});
});
$scope.genericEvt = function (event, data) {
console.log('event', event);
console.log('data', data);
// function is firing, but all parameters come undefined
};
You are missing one important piece in the function binding of directive. They need to be passed in as object with property name same as that of the argument names. i.e
scope.activateFn(event, data);
should be
scope.activateFn({event: event,data: data});
Or in otherwords, the properties of the object passed in through the bound function ({event: e,data: d}) needs to be specified as argument of the function being bound (genericEvt(event, data)) at the consumer side.
Though the syntax can be confusing at the beginning, you can as well use = binding instead of & though & is to be used specifically for function binding. Ex:
....
activateFn: '=',
....
and
activate-fn="genericEvt"
I am struggling with data binding in AngularJs.
I have the following piece of markup in .html file that includes the custom directive:
<my-directive ng-repeat="i in object" attr-1="{{i.some_variable}}"></my-directive>
Note: 'some-variable' is being updated every 10 seconds(based on the associate collection and passed to template through controller).
The directive's code includes:
myApp.directive('myDirective', function () {
scope: {
'attr-1': '=attr1'
which throws this exception because of the brackets in attr-1(see html code above).
It works though if I use read-only access(note at sign below):
myApp.directive('myDirective', function () {
scope: {
'attr-1': '#attr1'
I use scope.attr-1 in directive's HTML to show its value.
The problem is that with read-only access UI is not reflecting the change in attribute change.
I've found solution with $parse or $eval(couldn't make them work tho). Is there a better one there?
You'll need only two-way binding and I think $parse or $eval is not needed.
Please have a look at the demo below or in this fiddle.
It uses $interval to simulate your updating but the update can also come from other sources e.g. web socket or ajax request.
I'm using controllerAs and bindToController syntax (AngularJs version 1.4 or newer required) but the same is also possible with just an isolated scope. See guide in angular docs.
The $watch in the controller of the directive is only to show how the directive can detect that the data have changed.
angular.module('demoApp', [])
.controller('MainController', MainController)
.directive('myDirective', myDirective);
function MainController($interval) {
var self = this,
refreshTime = 1000; //interval time in ms
activate();
function activate() {
this.data = 0;
$interval(updateView, refreshTime);
}
function updateView() {
self.data = Math.round(Math.random()*100, 0);
}
}
function myDirective() {
return {
restrict: 'E',
scope: {
},
bindToController: {
data: '='
},
template: '<div><p>directive data: {{directiveCtrl.data}}</p></div>',
controller: function($scope) {
$scope.$watch('directiveCtrl.data', function(newValue) {
console.log('data changed', newValue);
});
},
controllerAs: 'directiveCtrl'
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.7/angular.js"></script>
<div ng-app="demoApp" ng-controller="MainController as ctrl">
model value in ctrl. {{ctrl.data}}
<my-directive data="ctrl.data"></my-directive>
</div>
I've come to the following solution(in case somebody runs into the the same problem):
// Directive's code
myApp.directive('myDir', function () { return {
restrict: 'E',
templateUrl: function () {
return 'my-dir.html';
},
scope: {
'id': '#arId',
'x': '#arX',
'y': '#arY',
//....
},
link: function ($scope, element, attrs) {
// *** SOLUTION ***
attrs.$observe('arId', function (id) {
$scope.id = id;
});
//...
}
Update: somebody sent me this answer, they have the same problem and came up with a very similar if not exact same solution:
Using a directive inside an ng-repeat, and a mysterious power of scope '#'
It is useful to read because they explain what's the idea behind it.
I'm relatively new to AngularJs, I have a problem using a custom directive when data comes from an HTTP request.
I have a service with an HTTP get request.
app.service('someService', function($http, $q){
this.getData = function(){
var deferred = $q.defer();
$http({
method: 'GET',
url: 'theUrl'
})
.success(function(data, status){
deferred.resolve(data);
})
.error(function(data, status){
deferred.reject;
})
return deferred.promise;
}
})
and a controller that calls the service.
app.controller('someConroller', function($scope, someService){
someService.getData().then(function(response){
$scope.data = response;
})
$scope.someArrayData = [
{.....}, {.....}, ...
]
}
Here is a very simple custom directive.
app.directive('customDirective', function(){
return {
link: function(scope, element, attrs){
console.log(scope[attrs['customDirective']]);
}
}
})
The problem is when I get an instance of the directive using someArrayData it works fine. But when I get an instance of the directive using data (the data that I get from the http service) console.log(data) gives me undefined.
<div custom-directive="someArrayData"></div><!-- console.log(scope[attrs['customDirective']]) gives the expected result -->
<div custom-directive="data"></div><!-- console.log(scope[attrs['customDirective']]) undefined -->
Thanks for helping.
You'll need a $watch to "listen" for that new value inside your directive once resolved by your service. There are various ways to do this, but this will be the most straightforward for understanding the concept. Also, you can likely clean this up a bit if you bind your value to that directives scope - essentially your call to scope[attrs[... can be streamlined. Observe the following...
angular.module('app', [])
.controller('ctrl', function($scope, $timeout) {
// -- simulate ajax call
$timeout(function() {
$scope.data = ['A', 'B', 'C'];
}, 500)
})
.directive('customDirective', function() {
return {
scope: {
data: '=customDirective'
},
link: function(scope, elem, attrs) {
scope.$watch('data', function(newVal, oldValue) {
console.log(newVal) // -- or console.log(scope.data)
});
}
}
});
JSFiddle Link - demo
That's because the data is not yet retrieved when the directive is linked.
You can simply wrap the html element with ng-if:
<div ng-if="data">
<div custom-directive="data"></div>
</div>
The controller and the directive have different scopes, so when you assign $scope.data in your controller, you aren't doing it for your directive. So you should inject your service in your directive and request the data there.
If you are having trouble understanding scope heirarchies, read up on them in the Angular documentation for scope.
I would suggest downloading the Angular JS Batarang extension for Chrome - it allows you to inspect all the different scopes on your page.
I'm using Angular 1.08, hence I need to use responseInterceptors.
First the code.
Interpreter:
app.factory('errorInterceptor', ['$q', 'NotificationService', function ($q, NotificationService) {
return function (promise) {
return promise.then(function (response) {
// do something on success
return response;
}, function (response) {
// do something on error
alert('whoops.. error');
NotificationService.setError("Error occured!");
return $q.reject(response);
});
}
}]);
app.config(function ($httpProvider) {
$httpProvider.responseInterceptors.push('errorInterceptor');
});
NotificationService:
app.service("NotificationService", function () {
var error = '';
this.setError = function (value) {
error = value;
}
this.getError = function () {
return error;
}
this.hasError = function () {
return error.length > 0;
}
});
Directive error-box:
app.directive("errorBox", function (NotificationService) {
return {
restrict: 'E',
replace: true,
template: '<div data-ng-show="hasError">{{ errorMessage }}</div>',
link: function (scope) {
scope.$watch(NotificationService.getError, function (newVal, oldVal) {
if (newVal != oldVal) {
scope.errorMessage = newVal;
scope.hasError = NotificationService.hasError();
}
});
}
}
});
The problem: When I use <error-box> at multiple places, all these boxes will display the error message. This is not my intent. I'd like to show only the error-box where the exception occurs.
For example, I have a directive which shows a list of transactions. When fetching the transactions fails, I want to show the error-box which is declared in that part.
I also have a directive where I can edit a customer. This directive also contains the error-box tag.
What happens is when saving a customer fails, both error-boxes are displayed, however, I only want the error-box of the customer to be displayed.
Does someone has an idea to implement this?
Angular services are Singleton objects, as described in the angular docs here. This means that Angular only creates one single "global" instance of a service and uses that same instance whenever the given service is requested. That means that Angular only ever creates one single instance of your NotificationService services and then will supply that one instance to every instance of your errorBox directive. So if one directive updates the NotificationService's error value, then all of the <error-box directives will get that value.
So, you're going to have to either create multiple notification services for each type of error (ie TransactionNotification and CustomerNotification, etc) or add different methods to your main NotificationService that would allow you to set only specific alerts (such as NotificationService.setCustomerError() or NotificationService.setTransactionError()).
None of those options are particularly user-friendly nor clean, but I believe (given the way you've set up your service), that's the only way to do it.
UPDATE: After thinking about it, I might suggest just dropping your whole NotificationService class and just using $scope events to notify your <error-box> elements when an error occurs:
In your 'errorInterceptor':
app.factory('errorInterceptor', ['$q', '$rootScope', function ($q, $rootScope) {
return function (promise) {
return promise.then(function (response) {
// do something on success
return response;
}, function (response) {
// do something on error
alert('whoops.. error');
var errorType = ...; // do something to determine the type of error
switch(errorType){
case 'TransactionError':
$rootScope.$emit('transaction-error', 'An error occurred!');
break;
case 'CustomerError':
$rootScope.$emit('customer-error', 'An error occurred!');
break;
...
}
return $q.reject(response);
});
}
}]);
And then in your errorBox directive:
link: function (scope, element, attrs) {
var typeOfError = attrs.errorType;
scope.$on(typeOfError, function (newVal, oldVal) {
if (newVal != oldVal) {
scope.errorMessage = newVal;
}
});
}
And then in your view:
<error-box error-type="transaction-error"></error-box>
<error-box error-type="customer-error"></error-box>
Does that make sense?