I have angular directive that displays few elements defined in compile like this(only one button in this example):
compile: function(element, attrs) {
var htmlText = '<button ng-click='myFunction()'></button>';
element.replaceWith(htmlText);
return linkF;
},
where linkF is a link function:
var linkF = function($scope, $element, $attrs) {
$scope.$element = $element;
};
This way I can access element in controller anf modify it.
I have my controller defined in controller parameter of directive.
controller: function ($scope) {
$scope.myFunction = function() {
//some code to mofify $scope.$element
}
Now my question is how to use isolated scope with this solution(need to reuse component on page few times), because if I define
scope: {},
then myFunction stops firing on button click.
I know I am doing some basic thing wrong but I cant figure it out, please help.
Related
I am currently working on extending the UX of Angular Typeahead. I made it possible for the results list not to get cleared on input .blur and remain in the DOM until the query changes.
But this way the results do not disappear. And I want to bind the input.blur() event to the variable, that is later on passed to the scope that has control over the typeahead-popup.html module.
Here's the adapted relevant-code of what I currently have:
angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position'])
.directive('typeahead', ['$compile', '$parse', '$q', '$timeout', '$document', '$window', '$rootScope', '$position', 'typeaheadParser',
function($compile, $parse, $q, $timeout, $document, $window, $rootScope, $position, typeaheadParser) {
return {
link: function(originalScope, element, attrs, ctrls) {
var isBlurred;
//some irrelevant library code
//on this event I change isBlurred;
element.bind('blur', function() {
//irrelevant functionality
isBlurred = true;
});
element.bind('focus', function() {
isBlurred = false;
});
}
}
}
}])
.directive('typeaheadPopup', function() {
return {
scope {
//<irrelevant other variables>
isBlurred: '='
}
},
replace: true,
//<irrelevant code>
link: function(scope, element, attrs) {
//<irrelevant other functions that have access to the scope and apply params at the DOM element>
scope.$watch('isBlurred', function() {
console.log(isBlurred)
});
});
})
});
Directive typeaheadPopup has control over the relevant DOM elements. Therefore the variable change from the .blur and .focus events on the input. But this doesn't happen. I just get wasBlurred is not defined.
Question:
How do I adjust the code in a way so that variable change that happens in typeahead directive is reflected properly within typeaheadPopup directive?
Here is a working Plunker with ui-bootstrap.
Here is an integration with ui-bootstrap and a close button control. Plunker
Here is a working Plunker with the code you gave and the question about how to communicate between directives.
Your isBlurred variable needs to be assigned to the scope :
originalScope.isBlurred;
Blur and focus are not part of the ngModelOptions that will trigger the scope to automaticaly change. You need to trigger it manualy.
element.on('blur', function() {
originalScope.$apply(function(){
originalScope.isBlurred = true;
});
});
Now your isBlurred is linked to the scope and that it will update with your custom events, you can forward it to your typeaheadPopup directive this way :
<input type="text" ng-model="name" typeahead typeahead-popup="isBlurred">
Define a isolated scope in your typeaheadPopup :
scope : {
isBlurred: '=typeaheadPopup'
}
There it is : your isBlurred variable is accessible and will automatically be updated in your directive. No need for a $watch. But you'll need one if you want to trigger an event when it changes :
scope.$watch('isBlurred', function() {
alert("inside typeaheadPopup directive" + scope.isBlurred)
});
The general answer is for all non-Angular event-handlers (in your case it's element.bind) you should call scope.$apply (or scope.$digest) to run the digest cycle and synchronise the values.
So for your case you either do manual call:
element.on('blur', function() {
originalScope.isBlurred = true;
originalScope.$apply();
});
or to run Angular method that will do it for you, for example $timeout:
element.on('blur', function() {
$timeout(function(){
originalScope.isBlurred = true;
});
});
$timeout sometimes is needed like a hack to integrate 3rd party non-Angular libraries.
If you can nest your directives, or use one as an attribute on the other, then they are able to communicate more cleanly. If they exist on the same level, then I believe some injected share state is you best bet.
If possible, nest the directives and make them communicate as described under "Creating Directives that Communicate" (at the bottom) of the documentation.
Example:
angular.module('foo', [])
.directive('parent', function() {
return {
restrict: 'E',
transclude: true,
controller: function() {
this.someFunction = function(pane) {
};
}
};
})
.directive('child', function() {
return {
require: '^^parent',
restrict: 'E',
transclude: true,
link: function(scope, element, attrs, parentCtrl) {
parentCtrl.someFunction();
}
};
});
I guess You need something like this demo right? So you need to assign a two-way bounded scope in each directive like this
scope: {
model: "="
},
then set both of them with the same model
<body ng-controller='MainController as vm'>
<foo model='vm.foobar'></foo>
<bar model='vm.foobar'></bar>
</body>
so MainController's foobar is set two-way bounded to foo and bar's scope, so technically, they share the same model and have every changes the model have.
please see the demo for the full code
I have my directive and I want to change the controller and html of a small section of the page onclick of a submit button. Changeing the HTML is working but changing the controller is not.
The line: attrs.ngController = "wordlistsPageController";
is not working. Please let me know what I can do to dynamically set the controller since this does not work.
It is a little web app game, so I do not want to change the whole page, just the game section between 3 game pages.
Here is my directive:
myApp.directive('gamePanel', function ($compile) {
return {
restrict: 'E',
templateUrl: "templates/wordlistPage.ejs",
link : function(scope, elem, attrs) {
attrs.ngController = "wordlistsPageController";//NOT WORKING
scope.submitWordlist = function() {
//change html
elem.html('<ng-include src="\'templates/gameOptionsDialogPage.ejs\'"></ng-include>');
$compile(elem.contents())(scope);
//change controller
attrs.ngController = "optionsPageController";///NOT WORKING
};
scope.backToWordlist = function() {
elem.html('<ng-include src="\'templates/wordlistPage.ejs\'"></ng-include>');
$compile(elem.contents())(scope);
attrs.ngController = "wordlistsPageController";///NOT WORKING
};
}//end link
}
});//end directive
How to Dynamically Change a Directive Controller
To instantiate different controllers inside a directive linking function, use the $controller service.
angular.module("myApp").directive('gamePanel', function ($controller) {
return {
restrict: 'E',
template: "<p>Game Panel Template</p>",
//controller: "pageController as vm",
link : function(scope, elem, attrs, ctrl, transclude) {
var locals = { $scope: scope,
$element: elem,
$attrs: attrs,
$transclude, transclude
};
scope.vm = $controller("pageController", locals);
}//end link
}
});
The above example instantiates a controller and puts it on scope as vm.
Best practice
The injected locals should follow the conventions of the locals provided by the $compile service. For more information on $compile service injected locals, see AngularJS $compile service API Reference API -- controller.
Beware: memory leaks
When destroying scopes, all watchers created on those scopes should be de-registered. This can be done by invoking scope.$destroy().
When destroying controllers, all watchers created by the controller should be de-registered. This can be done by invoking the de-register function returned by each $watch.
I am trying to take the innerHTML of an Angular directive, and pass that as an attribute of another directive. So, let's say that I have:
<js-code>This is some text</js-code>
my jsCode directive looks like this:
prettifyModule.directive('jsCode', function() {
return {
restrict: 'E',
compile: function($element, $scope) {
$scope.codeText = $element.html();
$element.replaceWith("<code-mirror model='codeText'></code-mirror>");
}
};
});
The goal being to pass a variable containing the string, "This is some text" as the model attribute of the code-mirror directive. For the most part, this seems to work. I can see in the elements that a directive appears that looks like:
<code-mirror model='codeText'></code-mirror>
However, the controller for the code-mirror directive does not, at that point, initialize.
If anyone could point out what I am doing wrong, or if there is a better way to do this entirely, it would be appreciated.
My limitations are:
I cannot alter the code-mirror directive.
I cannot statically manipulate the text that is being sent to the jsCode directive.
I have changed the directive code to
myApp.directive('myDirective', function($compile) {
return {
restrict: 'E',
link: function($scope, $element){
$scope.codeText = $element.html();
var template = "<second-directive model='codeText'></second-directive>";
var linkFn = $compile(template);
var content = linkFn($scope);
$element.replaceWith(content);
},
controller: function($scope) {
$scope.test = "Text from controller";
}
};
});
Here is the updated fiddle http://jsfiddle.net/xw7ms0xd/2
This is the first time, I used $compile service. Got the reference from here
http://odetocode.com/blogs/scott/archive/2014/05/07/using-compile-in-angular.aspx
I have couple of links in the header. But the pop-up nested under the other controller. I am using a service call for the pop-up here.
clicking on the header link, how can i open the pop-up which nested in the other controller.
And i need to update the content of the pop-up according to the link what the user clicks. for that, i included the html (using ng-include)
here is my code and demo :
var app = angular.module('myApp', []);
app.service('modalService', function() {
this.width = 100;
this.height = 100;
});
app.directive('modalDialog', function(modalService) {
return {
restrict: 'E',
scope: {
show: '='
},
replace: true, // Replace with the template below
transclude: true,
link: function(scope, element, attrs) {
},
templateUrl: "modal.html"
};
});
app.controller('MyCtrl', ['$scope', function($scope, modalService) {
$scope.modalShown = false;
}]);
app.controller('header', ['$scope', 'modalService', function($scope, modalService) {
$scope.modalShown = false;
$scope.toggleModal = function(link) {
console.log(link);
$scope.linkInfo = link+'.html';
$scope.modalShown = !$scope.modalShown;
};
}]);
Live demo
In case the way what i do is wrong, please correct me. at present the fuction is calls and gettting console. but pop-up not opening with appropriate content.
You can use the modal service of angular-UI library angular-ui#modal
If you want to just create something simple, you have to change your code in following ways:
I would recommend using ngIf for showing/hiding your modalDialog directive. With it, it will be rendered when the expression becomes truthy and you can get the right template each time.
Do not transclude since you don't have static templates. You have to compile the template on runtime (using $compile service). Also create a new scope for that template.
app.directive('modalDialog', function(modalService, $rootScope, $compile, $templateCache) {
return {
restrict: 'E',
scope: {
show: '=',
templateUrl: '='
},
link: function(scope, element, attrs) {
var template = $templateCache.get(scope.templateUrl);
var modalScope = $rootScope.$new();
var modalElement = angular.element(template);
element.append(modalElement);
$compile(modalElement)(modalScope);
},
templateUrl: "modal.html"
};
});
Note that there are still issues. Your placing of modal into arbitrary location into a DOM may not work (not all browsers behave correctly when playing with positioning). So I would recommend creating a service that would append and render the modal just below the element.
I am trying to pass the controller scope of parent controller and parent directive into a child directive but facing an error saying that the controller is not available. Here is a plunk for that
http://plnkr.co/edit/aahgOK9oFFjcP2y5VkVa?p=preview
HTML:
<div ng-controller="MainCtrl as mc">
<new-dir>
<data-customer-details customer="mc.customers[0]" logger="mc.logger()" version-age="{{mc.age}}"></data-customer-details>
</new-dir>
</div>
OK, so I tinkered with your plunker a bit. I couldn't get it working using Controller As...I had to change it over to $scope injection on the main controller. Then I created a new scope on newDir by setting scope: true.
You don't actually need to require the MainCtrl because these directives are automatically children of that scope anyway.
I changed your 'MainCtrl' to this:
angular.module('plunker').controller('MainCtrl', function ($scope) {
$scope.name = 'World';
$scope.customers = [{
"name": "angularjs 1.4",
"version": "1.4"
}, {
"name": "angularjs 1.3",
"version": "1.3"
}, {
"name": "angularjs 1.2",
"version": "1.2"
}];
$scope.age = 30;
$scope.logger = function() {
console.log('clicked');
}
$scope.ctrlScopeVariable = 'im in controller scope';
})
Minor change to newDir:
function newDir (){
return {
scope: true, //you need this
controller: function($scope){
$scope.val= 'someval';
console.log($scope.$parent.ctrlScopeVariable)
},
link: function(scope, el, attr, ctrl) {
console.log(scope.$parent.name)
}
}
}
And the last directive:
function CustomerDetails() {
var directive = {
scope: {
customer: '=',
logger: '&',
myNewAge: '#versionAge'
},
restrict: 'EA',
require: ['^newDir'],
controllerAs: 'cd',
templateUrl: 'customer-details.html',
link: linkFunction,
controller: function($scope){
console.log($scope.$parent.$parent.ctrlScopeVariable);
var cd = this;
cd.newval = 'new val';
}
};
function linkFunction(scope, elem, attributes, controllers, transclude) {
console.dir(controllers);
scope.fromMainCtrl = scope.$parent.$parent.ctrlScopeVariable
}
return directive;
}
The Plunker
I added a binding to the customer details template that passes in the $scope.ctrlScopeVariable from the main controller, so you can see the MainCtrl scope is accessible form the child directive.
In regards to require, the relevant documentation is here, I think:
If it is necessary to reference the controller or any functions bound
to the controller's scope in the template, you can use the option
controllerAs to specify the name of the controller as an alias. The
directive needs to define a scope for this configuration to be used.
This is particularly useful in the case when the directive is used as
a component.
Looking back at myPane's definition, notice the last argument in its
link function: tabsCtrl. When a directive requires a controller, it
receives that controller as the fourth argument of its link function.
Taking advantage of this, myPane can call the addPane function of
myTabs.
Essentially, you can use it to reference a parent controller on which you need to access some functions or something. Notably, it becomes available under whatever alias you give it as the fourth argument of your link function.
EDIT:
In this Plunker I added a function to the controller of newDir, required newDir in the CustomerDetail directive, and then called that function in the CustomerDetail link function:
CustomerDetails directive:
//some stuff
require: '^newDir',
//some stuff
link: function(scope, el, attr, newDirCtrl) {
console.log(newDirCtrl.doubleNum(100));
}
newDir controller:
controller: function($scope){
this.doubleNum = function(num) {
return num*2
}
// some stuff
}
First you need to declare a variable as callback function:
var MainCtrlFn = function() { .... }
Then, you can set it as parameter to angularJS:
angular.module('plunker').controller('MainCtrl', MainCtrlFn);