Please consider the following code: it has a directive myItem with isolate scope. Each item will display a button that will call delete() on the directive controller. I'd like this to trigger a refresh in the outer controller (AppController). But of course the refresh() function can not be found, because of the isolated scope.
<html>
<body ng-app="question">
<div ng-cloak ng-controller="AppController">
<my-item ng-repeat="item in list" data="list">
</my-item>
<input type="text" maxlength="50" ng-model="new_item" />
<button ng-click="add(new_item)">+</button>
</div>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.4.5/angular.min.js"></script>
<script>
(function () {
var app;
app = angular.module('question', []);
app.controller('AppController', [
'$scope', '$http', function ($scope, $http) {
$scope.list = [];
function refresh(){
$http.get('/api/items').then(
function(response){
$scope.list = response.data;
}
);
}
$scope.add = function(item){
$http.post('/api/items', { item: item }).then(refresh);
};
refresh();
}
]);
app.directive('myItem', function() {
return {
scope: {
item: '=data',
},
// directive template with delete button
template: '{{ item }} <button ng-click="delete(item)">-</button>',
restrict: 'E',
// directive controller with delete function
controller: [ '$scope', '$http', function($scope, $http) {
$scope.delete = function (card) {
// This is where it goes wrong! refresh does not exist
$http.delete('/api/items' + card.id).then(refresh);
}
}]
};
});
})();
</script>
</body>
</html>
One thing I could do is add ng-change to the myItem directive, but that would involve requiring ngModelController which seems overkill.
Other things I can think of:
Add something like onchange: '#' to the scope attribute of the directive, then set onchange = refresh in the html. Call the onchange expression instead of refresh inside the delete function. But this feels like I'm re-implementing ng-change?
Add require: '^AppController' to the directive. Then I guess I could call refresh on the parent controller directly. That seems like it violates loose coupling.
Don't use isolate scope at all. That would mean we inherit from the parent scope and refresh is available. But then my directive implicitly assumes that the scope will hold an item. Which also violates loose coupling, but in an implicit way.
So my question is: which is the correct way to let the parent controller know it should refresh its contents?
IMO, the first way would be the best way. The directive receives a function callback from outside which is executed by the directive when necessary. Like this the two directives are loosely coupled. It's similar to ng-change which is an attribute that is used by ng-model directive.
Example: Directive
app.directive('myItem', function() {
return {
restrict: 'E',
scope: {
item: '=data',
myItemDeleteCallback: '&myItemDeleteCallback'
},
template: '{{ item }} <button ng-click="delete(item)">-</button>',
controller: [ '$scope', '$http', function($scope, $http) {
$scope.delete = function (card) {
// This is where it goes wrong! refresh does not exist
$http.delete('/api/items' + card.id).then(function () {
$scope.myItemDeleteCallback();
});
}
}]
};
});
Usage: Controller
app.controller('AppController', ['$scope', '$http', function ($scope, $http) {
$scope.list = [];
$scope.refresh = function (){
$http.get('/api/items').then(
function(response){
$scope.list = response.data;
}
);
};
$scope.add = function(item){
$http.post('/api/items', { item: item })
.then($scope.refresh);
};
refresh();
}]);
Usage: Template
<div ng-cloak ng-controller="AppController">
<my-item my-item-delete-callback="refresh()" ng-repeat="item in list" data="list">
</my-item>
<input type="text" maxlength="50" ng-model="new_item" />
<button ng-click="add(new_item)">+</button>
</div>
Related
I'm trying to create a directive which allows me to pass in an attribute string which I then use as the "name" parameter when subscribing to events using $scope.$on. Essentially, the series of events is this:
An object is broadcasted using $rootScope.$broadcast called 'validationResultMessage', in another controller for example.
I have a directive which has an attribute called "subscription" to which I pass the string 'validationResultMessage'.
That directive passes the value of the "subscription" attribute to its scope and subscribes to it with "$scope.$on".
The problem is, it looks like the value of the attribute is "undefined" at the time everything is evaluated, and so when I try to subscribe using $scope.$on, it actually subscribes me to "undefined" rather than "validationResultMessage"
Here is my directive:
app.directive('detailPane', function () {
return {
restrict: 'E',
scope: {
selectedItem: '=',
subscription: '#',
},
templateUrl: 'app/templates/DetailPane.html', //I'm also worried that this is causing my controller to get instantiated twice
controller: 'DetailPaneController'
};
});
which I then use like this:
<td class="sidebar" ng-controller="DetailPaneController" ng-style="{ 'display': sidebarDisplay }">
<detail-pane
selected-item='validationResult'
subscription='validationResultMessage'/>
</td>
And the controller that I'm trying to pass this attribute into:
app.controller('DetailPaneController', ['$scope', '$http', 'dataService', 'toastr', '$uibModal', '$rootScope', '$attrs', function ($scope, $http, dataService, toastr, $uibModal, $rootScope, $attrs) {
$scope.fetching = [];
$scope.validationResult = null;
$scope.sidebarDisplay = 'block';
console.log('subscription is ', $scope.subscription);
var thisSubscription = $scope.subscription;
//if I hardcode the param as 'validationResultMessage', this works
$scope.$on($scope.subscription, function (event, arg) {
$scope.validationResult = arg;
});
}]);
So another way that I managed to solve this particular issue is to only use the internal DetailPaneController as defined in the directive body. Part of my problem was that I was causing the controller to be instantiated twice by having it as both the parent controller using ng-controller= in my html as well as being defined in the directive body. This way I can just use the straightforward "#" binding and everything gets resolved in the right order. I can even have another directive within my template that I can pass my validationResult into.
The new setup looks like this:
DetailPaneController:
app.controller('DetailPaneController', ['$scope', '$http', function ($scope, $http) {
$scope.$on($scope.subscription, function (event, arg) {
$scope.validationResult = arg;
$scope.exception = JSON.parse(arg.Exception);
});
}]);
DetailPane Directive:
app.directive('detailPane', function () {
return {
restrict: 'E',
scope: {
subscription: '#' //notice I am no longer binding to validationResult
},
templateUrl: 'app/templates/DetailPane.html',
controller: 'DetailPaneController'
};
});
Directive as used in HTML:
<div class="sidebar" ng-style="{ 'display': sidebarDisplay }">
<detail-pane subscription='validationResultMessage' />
</div>
Directive Template (for good measure):
<div class="well sidebar-container">
<h3>Details</h3>
<div ng-show="validationResult == null" style="padding: 15px 0 0 15px;">
<h5 class=""><i class="fa fa-exclamation-triangle" aria-hidden="true" /> Select a break to view</h5>
</div>
<div ng-show="validationResult != null">
<table class="table table-striped">
<tr ng-repeat="(key, value) in validationResult">
<td class="sidebar-labels">{{key | someFilter}}</td>
<td >{{value | someOtherFilter : key}}</td>
</tr>
</table>
<another-directive selected-item="validationResult" endpoint="endpoint" />
</div>
I'm going to post my answer 1st, given that it's a bit of code, please let me know if this is the required outcome, so I can provide comments. You should be able to run the provided code snippet.
var app = angular.module('myApp', []);
app.directive('detailPane', function() {
return {
restrict: 'E',
transclude: false,
scope: {
selectedItem: '=',
subscription: '#'
},
link: function(scope, elem, attr) {
scope.$on(scope.subscription, function(e, data) {
scope.selectedItem = data.result;
elem.text(data.message);
});
},
};
});
app.controller('DetailPaneController', function($scope) {
$scope.validationResult1 = "";
$scope.validationResult2 = "";
});
app.controller('SecondController', function($rootScope, $scope, $timeout) {
$timeout(function() {
$rootScope.$broadcast('validationResultMessage1', {
message: 'You fail!',
result: 'Result from 1st fail'
})
}, 2000);
$timeout(function() {
$rootScope.$broadcast('validationResultMessage2', {
message: 'You also fail 2!',
result: 'Result from 2nd fail'
})
}, 4000);
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<body ng-app='myApp'>
<div ng-controller="DetailPaneController">
<detail-pane class='hello' selected-item='validationResult1' subscription='validationResultMessage1'></detail-pane>
<br/>
<detail-pane class='hello' selected-item='validationResult2' subscription='validationResultMessage2'></detail-pane>
<hr/>
<span>{{validationResult1}}</span>
<br/>
<span>{{validationResult2}}</span>
</div>
<div ng-controller="SecondController">
</div>
</body>
I think you should set watcher on $scope.subscription and checking if new value is set and then start subscribing passed event.
$scope.$watch('subscription', function(nv, ov){
//this makes sure it won't trigger at initialization
if(nv!==ov){
$scope.$on($scope.subscription, function (event, arg) {
$scope.validationResult = arg;
});
}
});
I have an AngularJS SPA which loads articles into the view. Some articles have code examples and I want to use highlight.js to highlight it.
In my example below I have simulated a get request, 'cause that's how I load my dynamic content in the actual app. The $scope.test is very similar to what my actual app could get returned; some regular HTML to print out which includes code examples.
My problem: it doesn't really seem to work.
Specifically, nothing gets highlighted. It seems to me like I am missing an init or something... Halp?
I've also tried <div hljs/> with the same (lack of) result. There are no console errors.
This answer provides a solution that uses ng-model in the template. However, I don't use ng-model anywhere.
EDIT: I've made some changes to my example code to further explain the problem.
Here's my app (simplified):
var app = angular.module('app', ['ngSanitize']);
app.controller('ctrl', ['$scope', '$http',
function($scope, $http) {
"use strict";
$http.get('/echo/html').then(function successCallback(response) {
$scope.title = 'Some Title';
$scope.metaStuff = 'Written by Awesome MacFluffykins';
$scope.articleBody = '<p>Here\'s an example of a simple SQL SELECT:</p><pre><code class="sql" highlight>SELECT * FROM table WHERE user = \'1\'</code></pre>';
}, function errorCallback(response) {
console.log("Error: %d %s", response.code, response.message);
});
}
]);
Here's my HTML:
<div ng-app="app" ng-controller="ctrl">
<h2>{{ title }}</h2>
<p><small>{{ metaStuff }}</small></p>
<div ng-bind-html="articleBody"></div>
</div>
And finally a jsFiddle.
In my opinion it's best to use a directive for DOM manipulations like this. Pass your sourcecode through ng-model (you could also use another attribute) and run HLJS in the directive. Since you're using a asynchronous method to supply the value to your scope, you'll need to use $watch to catch the value and then run HLJS:
HTML:
<div highlight ng-model="test"></div>
Directive:
.directive('highlight', [
function () {
return {
replace: false,
scope: {
'ngModel': '='
},
link: function (scope, element, attributes) {
scope.$watch('ngModel', function (newVal, oldVal) {
if (newVal !== oldVal) {
element.html(scope.ngModel);
var items = element[0].querySelectorAll('code,pre');
angular.forEach(items, function (item) {
hljs.highlightBlock(item);
});
}
});
}
};
}
]);
Working JSFiddle: https://jsfiddle.net/1qy0j6qk/
Fiddle
https://jsfiddle.net/vg75ux6v/
var app = angular.module('app', ['hljs', 'ngSanitize']);
app.controller('ctrl', ['$scope', '$http',
function($scope, $http) {
"use strict";
$http.get('/echo/html').then(function successCallback(response) {
$scope.test = '<h2>Here\'s some code:</h2><pre><code hljs class="sql">SELECT * FROM table WHERE user = \'1\'</code></pre>';
}, function errorCallback(response) {
console.log("Error: %d %s", response.code, response.message);
});
}
]).directive('compile', ['$compile', function ($compile) {
return function(scope, element, attrs) {
scope.$watch(
function(scope) {
return scope.$eval(attrs.compile);
},
function(value) {
element.html(value);
$compile(element.contents())(scope);
}
);
};
}]);
I have a need for a custom click directive, which executes the passed code using scope.$apply().
$(elem).on('click', function(){
scope.$apply(attrs.wdClick);
});
This works fine if I pass something like wd-click="something = !something". But when I try to call a $rootScope function it does not work, however it does work when using the default ng-click.
wd-click="$root.someFunction()" //this does not call the function but ng-click does
I have tried updating the directive to make it work
$(elem).on('click', function(){
$rootScope.$apply(attrs.wdClick);
});
But this does not work either. Any ideas?
attrs.wdClick is a string, so passing it to $apply won't do anything. To call the function you can pass the string to $eval
scope.$apply(function() {
scope.$eval(attrs.wdClick)
});
You should wrap your code in function(){}
scope.$apply(function(){
attrs.wdClick() // this is some sunction I suppose
});
Would you want to call your rootscope method in an another controller? If I understand correctly, try to use this way :
angular.module('app', [])
.controller('Ctrl', function Ctrl1($scope, $rootScope) {
$rootScope.blah = 'Hello';
$scope.yah = 'World'
})
.directive('myTemplate', function() {
return {
restrict: 'E',
templateUrl: 'my-template.html',
scope: {},
controller: ["$scope", "$rootScope", function($scope, $rootScope) {
console.log($rootScope.blah);
console.log($scope.yah);
$scope.test = function(arg) {
console.log(arg);
}
}]
};
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.22/angular.min.js"></script>
<div ng-app="app">
<div ng-controller="Ctrl">
<my-template></my-template>
</div>
<!-- my-template.html -->
<script type="text/ng-template" id="my-template.html">
<label ng-click="test($root.blah)">Click</label>
</script>
</div>
Also you can try on jsfiddle,
http://jsfiddle.net/mg74b/24/
Is there a reason you are trying to use attrs instead of a scope property? Also, you should use $timeout instead of $apply.
angular
.module('app', [])
.directive('myDirective', myDirective);
myDirective.$inject = ['$timeout'];
function myDirective($timeout) {
return {
restrict: 'E',
templateUrl: 'my-template.html',
scope: {
wdClick: '='
},
link: linkFn
};
function linkFn(scope, element) {
element.on('click', function () {
$timeout(scope.wdClick);
});
}
}
I've got a custom directive with an html template and it basically is a menu option. When a user makes a selection it updates the ng-model variable within the directive.
But I would like for the ng-model variable within the directive passed outside of the directive into the html page.
Here's the code snippets:
Directive:
(function() {
'use strict';
angular
.module('myModule')
.controller('myController', ['$scope', function($scope) {
$scope.sortByOptions = [
{ value:'red', displayText:'Redder' },
{ value:'blue', displayText:'Bluer' },
{ value:'gold', displayText:'Golder' },
{ value:'brown', displayText:'Browner' }
];
}]
)
.directive('myDirective', myDirective);
myDirective.$inject = [];
function myDirective() {
return {
restrict: 'A',
templateUrl: 'mydirective/sorting.html',
}
}
})();
HTML Template for the directive:
<select ng-model="sortBy" ng-options="sortByOption.displayText for sortByOption in sortByOptions track by sortByOption.value"> </select> {{sortBy.value}}
HTML of the Page:
<div class="col-md-8 form-inline" my-directive>
</div>
<!-- need ng-model variable from my-directive passed into sortBy --> <!-- it's being called on same page. I turned a menu options into a directive to save from copying/pasting same code everywhere. when the menu option gets selected it populates a list which is the li you see below -->
<li ng-repeat="object in objects | filter: searchTool | orderBy: (sortAscending ? '' : '-') + sortBy">
<div class="plank">
<div class="plank-header">
</div>
</div>
</li>
As you can see I'm trying to pass ng-model="sortBy" value from the directive which is chosen by the user into other parts of the page called sortBy that is within the li.
It would be awesome if someone can give an example of what they did.
I've done something similar by essentially exposing the variable in your directive to your controller. You can do this by passing a function from your controller into your directive such that that function gets called and essentially sets a variable in your controller. Something like this.
<div mydirective call-outside-function="setSortBy".....>
mycontroller function(...) {
$scope.setSortBy = function(sb) {
$scope.localSortBy = sb;
};
}
mydirective .....
link: function(scope,element,attrs) {
scope.$watch('sortBy',function(newval) {
attrs.callOutsideFunction(newval);
});
}
Probably not the most elegant solution but it works
I did the following:
1) added a scope.$watch to allow my directive to listen for a change in that variable on the DOM. Then it will set the new value in the controller
2) Added the controller into the directive. I originally forgot to add a line in the function colorSorter to return the controller
3) I did not need to modify the directive's html template or the main html template for the page to get it working.
Here is the directive:
(function() {
angular
.module('myModule')
.controller('sortController', ['$scope', function($scope) {
$scope.sortByOptions = [
{ value:'red', text:'Redder' },
{ value:'blue', text:'Bluer' },
];
$scope.sortBy = {value: undefined}
}]
)
.directive('colorSorter', colorSorter);
colorSorter.$inject = [];
function colorSorter() {
return {
restrict: 'A',
templateUrl: 'app/color-sorter/color-sorter.html',
controller: 'sortByController',
link: function (scope) {
scope.$watch('sortBy.value', function (value) {
console.log(value);
})
}
}
}
})();
I have a set of tabs which all have a directive in them:
<div class="col-md-9 maincols" id="formEditor">
<tabset>
<tab heading="New subscriber" select="isSelected('newSub')">
<new-subscriber></new-subscriber>
</tab>
<tab heading="Existing subscriber" select="isSelected('existingSub')">
<existing-subscriber></existing-subscriber>
</tab>
<tab heading="Landing page" select="isSelected('landing')">
<landing-page></landing-page>
</tab>
</tabset>
</div>
All these 3 directives have been defined similarly like this:
angular.module('myApp')
.directive('newSubscriber', function () {
return {
restrict: 'E',
scope: {},
replace: true,
templateUrl: 'scripts/newsubscriber/newsubscriber.html',
controller: 'newsubscriberCtrl'
};
}); //... and so on
I am (probably wrongly) under the impression that because I have set scope: {} for all the directives, they should now have completely isolated scopes and leave each other alone.
But that is not the case and bindings from the first directive's controller manage to stop values in the second or third controller from being binded
for example in newsubscriberCtrl I have:
app.controller('newsubscriberCtrl', ["$scope", "$routeParams", "UserMessages", "FormProvider", function ($scope, $routeParams, UserMessages, FormProvider) {
$scope.formId = $routeParams.formId;
var newSubscriberForm = new FormProvider.Form($scope);
angular.extend($scope, newSubscriberForm);
$scope.title = UserMessages.exampleText.genericPageTitle;
$scope.description = UserMessages.exampleText.genericPageDescription;
$scope.validationMessages = {
contactNotSaved: UserMessages.validationMessages.contactNotSaved,
contactCreatedOk: UserMessages.validationMessages.contactCreatedOk,
contactNotCreated: UserMessages.validationMessages.contactNotCreated,
requiredField: UserMessages.validationMessages.requiredField,
passwordMismatch: UserMessages.validationMessages.passwordMismatch,
isOpen: false
}
}]);
which is overriding the similar object in existingSubscriber controller:
app.controller('existingsubscriberCtrl', ["$scope", "$routeParams", "UserMessages", "FormProvider", function ($scope, $routeParams, UserMessages, FormProvider) {
$scope.formId = $routeParams.formId;
var existingSubscriberForm = new FormProvider.Form($scope);
angular.extend($scope, existingSubscriberForm);
$scope.title = UserMessages.exampleText.genericPageTitle;
$scope.description = UserMessages.exampleText.genericPageDescription;
$scope.validationMessages = {
contactNotSaved: UserMessages.validationMessages.contactNotSaved,
contactSavedOk: UserMessages.validationMessages.contactSavedOk,
requiredField: UserMessages.validationMessages.requiredField,
passwordMismatch: UserMessages.validationMessages.passwordMismatch,
isOpen: false
}
}]);
So in the view of both directives <pre>{{validationMessages | json }}</pre> the validationMessages object has the props of the first controller.
Why is this happening? Am I missing to understand a concept here? How Can I isolate these controllers from each other and comfortably have similar props in the controllers without them affecting each other?
Side note: I strongly want to avoid having to prefix everything on all scopes with their controller name, e.g $scope.newSubscriber.validationMessages and so on... as that would defeat the whole point pretty much as I will effectively one big controller for the whole tab section and directives would also be pointless.
Angular is on v.1.3.0-beta.11
angular-ui-bootstrap is on v.0.10.0
You have reuse the same controller newsubscriberCtrl in /app/scripts/formbanner/formbanner.js:
.directive('formBanner', function () {
return {
restrict: 'E',
replace: 'true',
templateUrl: 'scripts/formbanner/formbanner.html',
controller: 'newsubscriberCtrl'
};
});
The existingSubscriber directive have the formBanner as a child directive, plus the formBanner directive doesn't have an isolated scope.
Therefore, the $scope that get injected into the newsubscriberCtrl of formBanner is the same as the scope of the existingSubscriber!!
I've tried removing the controller property in the formBanner directive and I saw it works as expected.
Have you tried this?
angular.module('myApp')
.directive('newSubscriber', function () {
return {
restrict: 'E',
scope: { someValue = "&validationMessages" },
replace: true,
templateUrl: 'scripts/newsubscriber/newsubscriber.html',
controller: 'newsubscriberCtrl',
link: function (scope, iElm, iAttrs) {
var x = scope.someValue();
// x = your messages
}
};
});
In your controller
$scope.someValue
EDIT Disclaimer: this is sort of from memory. When I was facing something similar I felt this to be rather enlightning:
http://umur.io/angularjs-directives-using-isolated-scope-with-attributes/