AngularJS accessing isolated scope in child directive - angularjs

Here is my HTML part:
<div data-my-question="" data-question="Question text?">
<div data-yes="">
YES!
</div>
<div data-no="">
NO!
</div>
</div>
and here are my angular directives:
angular.module('myModule').directive('myQuestion', [
function () {
return {
restrict: 'A',
replace: true,
scope: {
question: '#'
},
transclude: true,
controller: function($scope){
$scope.answered = false;
$scope.yesActive = false;
$scope.activateYes = function (yesActive) {
$scope.answered = true;
$scope.yesActive = yesActive;
};
},
template: '<div>' +
'<div class="question-box" ng-class="{answered: answered}">' +
'<div class="question">' +
'{{question}}' +
'</div>' +
'<div class="buttons">' +
'Yes' +
'No' +
'</div>' +
'</div>' +
'<div ng-transclude></div>' +
'</div>'
};
}
])
.directive('yes', [
function () {
return {
restrict: 'A',
replace: true,
transclude: true,
template: '<p ng-show="answered && yesActive" ng-transclude></p>'
};
}
])
.directive('no', [
function () {
return {
restrict: 'A',
replace: true,
transclude: true,
template: '<p ng-show="answered && !yesActive" ng-transclude></p>'
};
}
]);
The problem is, that the child directives ('yes', 'no') can't access the 'answered' and the 'yesActive' variable from the parent directive, which has an isolated scope.
How can I achieve that the child directives react on value changes from the parent directive?

The transcluded elements won’t have access to the directive’s isolated scope.
This is how Angular works, an isolated scope (myQuestion) won’t inherit from any parent and the transcluded elements (Yes/No) will became siblings of the myQuestion’s scope.
Transcluded elements will be child of the original scope where they were defined, this way they are still able to bind instead of being isolated too.
If you check this JSBIN, in the console you will see that Yes/No scope have access to the $rootScope (where pizza is defined) while myQuestion’s isolated scope won’t.
You can get more info about this here.
Solution:
I like simple code so I'd move the Yes/No elements inside the question template if possible, but you could also communicate between directives.

Related

AngularJS directives data binding doesn't work

DEMO
Here is a simplified version of the two directives I have, my-input and another-directive:
HTML:
<body ng-controller="AppCtrl">
<another-directive>
<my-input my-input-model="data.firstName"></my-input>
</another-directive>
</body>
JS:
.directive('myInput', function() {
return {
restrict: 'E',
replace: true,
scope: {
model: '=myInputModel'
},
template: '<input type="text" ng-model="model">'
};
}).directive('anotherDirective', function($compile) {
return {
restrict: 'E',
scope: {},
compile: function(element) {
var html = element.html();
return function(scope) {
var output = angular.element(
'<div class="another-directive">' +
html +
'</div>'
);
$compile(output)(scope);
element.empty().append(output); // This line breaks the binding
};
}
};
});
As you can see in the demo, if I remove element.empty().append(output);, everything works fine, i.e. changes in the input field are reflected in controller's data. But, adding this line, breaks the binding.
Why is this happening?
PLAYGROUND HERE
The element.empty() call is destroying all child nodes of element. In this case, element is the html representation of another-directive. When you are calling .empty() on it, it is trying to destroy its child directive my-input and any scopes/data-bindings that go with it.
A somewhat unrelated note about your example. You should look into using transclusion to nest html within a directive, like you are doing with another-directive. You can find more info here: https://docs.angularjs.org/api/ng/service/$compile#transclusion
I think a little bit context as to what you are trying to do well be helpful. I am assuming you want to wrap the my-input directive in another-directive ( some sort of parent pane ). You could accomplish this using ng transclude. i.e
angular.module('App', []).controller('AppCtrl', function($scope) {
$scope.data = {
firstName: 'David'
};
$scope.test = "My test data";
}).directive('myInput', function() {
return {
restrict: 'E',
replace: true,
scope: {
model: '=myInputModel'
},
template: '<input type="text" ng-model="model">'
};
}).directive('anotherDirective', function($compile) {
return {
restrict: 'E',
transclude: true,
scope: {},
template : '<div class="another-directive"><div ng-transclude></div></div>'
};
});
It works if you require ngModel
}).directive('anotherDirective', function($compile) {
return {
restrict: 'E',
require:'ngModel',
scope: {},
...

Angular Directive Compile function: replace is ignored, and compiled content is nested

Directive:
app.directive('myCarousel', function ($compile) {
return {
restrict: 'E'
, transclude: false
, replace: true
, scope: true
, compile: function compile($element, $attr) {
var html = '<ul rn-carousel="" rn-carousel-indicator="">' + $element.html() + '</ul>'
$element.html(html)
return function ($scope) {
$compile($element.contents())($scope);
}
}
};
})
Usage:
<my-carousel>
<li>Todd</li>
<li>Andrej</li>
</my-carousel>
Output
<my-carousel class="ng-scope">
<div id="carousel-1" class="rn-carousel-container ng-scope" style="width: 1600px;">
<div id="carousel-2" class="rn-carousel-container" style="width: 1600px;">
<blah>
</div>
</div>
</my-carousel>
The problems I have (at least known problems
1 - I still have the my-carousel element, why didn't the replace remove it? Do I need to do this myself because I am writing the compile function? HOw would I go about that?
2 - for some reason it looks like the rn-carousel inner directive is getting compiled inside of itself? This could very well be my lack of understanding on this inner directive on how it works. But does it look like anything is terribly wrong with this compile function?
When you use the compile function, you are expected to do DOM manipulation by yourself. You can achieve the same effect as replace: true using replaceWith:
compile: function($element, $attr) {
var html = '<ul rn-carousel="" rn-carousel-indicator="">' + $element.html() + '</ul>'
$element.replaceWith(html);
return function(scope) {
/*probably don't need to compile element contents here*/
}
}
But... It doesn't look like you need to use compile at all. By giving the directive a template, and using transclusion, you can accomplish the same thing:
app.directive('myCarousel', function() {
return {
restrict: 'E',
transclude: true,
replace: true,
scope: true,
template: '<ul rn-carousel="" rn-carousel-indicator="" ng-transclude></ul>'
};
})
Current working solution (albeit without replacing the parent element):
eido.directive('myCarousel', function ($compile) {
return {
restrict: 'E'
, transclude: false
, replace: true
, scope: true
, compile: function compile($element, $attr) {
var html = '<ul rn-carousel="" rn-carousel-indicator="">' + $element.html() + '</ul>'
$element.html(html)
return function ($scope) {
}
}
};
})

Calling element.replaceWith from inside scope.$on throws TypeError

We have the following directive:
.directive("directiveToggleElement", function(FlagsService) {
return {
restrict: "E",
scope: {},
link: function(scope, element, attrs)
{
scope.showMe = FlagsService.isOn(attrs.toggleKey);
scope.featureText = attrs.featureText;
var htmlTag = '<div class="panel ng-scope" ng-class="{selected: selected}" ng-click="selected = !selected;"></div>';
var htmlComment = '<!-- TogglePlaceholder: ' + attrs.toggleKey +'-->';
element.replaceWith(scope.showMe ? htmlComment + htmlTag : htmlComment);
// Listen for change broadcast from flagsservice to toggle local store
scope.$on('LocalStorage.ToggleChangeEvent.' + attrs.toggleKey, function(event, parameters) {
console.log('stuff changed - ' + parameters.key + ' ' + parameters.newValue);
scope.showMe = parameters.newValue;
element.replaceWith(scope.showMe ? htmlComment + htmlTag : htmlComment);
});
}
}
})
The idea is that based on the value of a feature toggle (or feature flag) the directive will output the comment with the tag, or just the comment.
The initial element.replaceWith works as expected, but the call from inside the scope.$on generates a "TypeError: Cannot call method 'replaceChild' of null". We've inspected element before each call and can't spot an obvious difference.
Can anyone explain why the error would be thrown here, or advise of a potential workaround that will allow us to do the same thing?
We have a working directive that sets the value of ng-show:
.directive("directiveToggle", function(FlagsService) {
return {
restrict: "A",
transclude: true,
scope: {},
link: function(scope, element, attrs)
{
scope.showMe = FlagsService.isOn(attrs.toggleKey);
// Listen for change broadcast from flagsservice to toggle local store
scope.$on('LocalStorage.ToggleChangeEvent.' + attrs.toggleKey, function(event, parameters) {
console.log('stuff changed - ' + parameters.key + ' ' + parameters.newValue);
scope.showMe = parameters.newValue;
});
},
template: '<div class="panel ng-scope" ng-show="showMe" ng-class="{selected: selected}" ng-click="selected = !selected;"><span ng-transclude></span></div>',
replace: true
}
})
But we would prefer to remove the element from the DOM instead of setting display to none.
You could still use the ng-if directive within your custom "directiveToggle" without the need for a controller. To achieve this you need to use the transclude option:
.directive("directiveToggle", function (FlagsService) {
return {
template: '<div ng-transclude ng-if="isEnabled"></div>',
transclude: true,
restrict: 'A',
scope: true,
link: function ($scope, $element, $attrs) {
var feature = $attrs.featureToggle;
$scope.isEnabled = FlagsService.isOn(feature);
$scope.$on('LocalStorage.ToggleChangeEvent.' + feature, function (event, parameters) {
$scope.isEnabled = parameters.newValue;
});
}
}
});
The above is taking the mark-up from inside the "directiveToggle" and then wrapping it inside of the template, where the "ng-transclude" directive marks the insertion point. Also included on the template is an "ng-if" directive that is watching the "isEnabled" member on the current scope for the "directiveTemplate".
One caveat with this approach is that you need the directive to create it's own scope/isolated scope if you have multiple instances of this directive, otherwise the "isEnabled" member will be shared between the directives.

Multiple directives [directive#1, directive#2] asking for isolated scope on

I'm attempting to build a new directive on top of an already existing directive but I am halted in my proces. When loading the page I'm facing the following error:
Multiple directives [directive#1, directive#2] asking for isolated scope on <easymodal title="Test-Title" text="Text-Text" oncancel="show = false" onok="alert();">
The base directive looks like this:
Rohan.directive('easymodal', function () {
return {
restrict: 'E',
// priority: 200,
transclude: true,
replace: true,
scope:{
showModal: "=show",
callback: "=closeFunction",
dismissable: '&'
},
template:
'<div data-ng-show="showModal" class="modal-container">' +
'<div class="modal-body">' +
'<div class="title"><span data-translate></span><a data-ng-show="dismissable" data-ng-click="dismiss()">x</a></div>' +
'<div data-ng-transclude></div>' +
'</div>' +
'<div class="modal-backdrop" data-ng-click="dismiss()"></div>' +
'</div>'
};
});
And my wrapper directive looks like this:
Rohan.directive('easydialog', function () {
return {
restrict: 'E',
transclude: true,
scope: {title: '#',
text: '#',
onOk: '&',
onCancel: '&'},
replace: true,
template:
'<easymodal>' +
'{{text}} ' +
'<hr>' +
'<button ng-click="{{onCancel}}" value="Cancel"' +
'<button ng-click="{{onOk}}" value="Ok"' +
'</easymodal>'
};
});
My html looks like this:
<easydialog title="Test-Title" text="Text-Text" onCancel="show = false" onOk="alert();" />
At first I though my title attribute was conflicting so I removed that attribute in the html line and from my wrapper directive but it was not effective.
You need to change your easydialog template and wrap <easymodal> in a <div>.
Your problem simply is that you're adding a template argument inside the directive as well as adding a resolving template tag named <easydialog> in your actual HTML template. You have the choose either the one or the other.

How to create a directive with a dynamic template in AngularJS?

How can I create a directive with a dynamic template?
'use strict';
app.directive('ngFormField', function($compile) {
return {
transclude: true,
scope: {
label: '#'
},
template: '<label for="user_email">{{label}}</label>',
// append
replace: true,
// attribute restriction
restrict: 'E',
// linking method
link: function($scope, element, attrs) {
switch (attrs['type']) {
case "text":
// append input field to "template"
case "select":
// append select dropdown to "template"
}
}
}
});
<ng-form-field label="First Name" type="text"></ng-form-field>
This is what I have right now, and it is displaying the label correctly. However, I'm not sure on how to append additional HTML to the template. Or combining 2 templates into 1.
i've used the $templateCache to accomplish something similar. i put several ng-templates in a single html file, which i reference using the directive's templateUrl. that ensures the html is available to the template cache. then i can simply select by id to get the ng-template i want.
template.html:
<script type="text/ng-template" id=“foo”>
foo
</script>
<script type="text/ng-template" id=“bar”>
bar
</script>
directive:
myapp.directive(‘foobardirective’, ['$compile', '$templateCache', function ($compile, $templateCache) {
var getTemplate = function(data) {
// use data to determine which template to use
var templateid = 'foo';
var template = $templateCache.get(templateid);
return template;
}
return {
templateUrl: 'views/partials/template.html',
scope: {data: '='},
restrict: 'E',
link: function(scope, element) {
var template = getTemplate(scope.data);
element.html(template);
$compile(element.contents())(scope);
}
};
}]);
Had a similar need. $compile does the job. (Not completely sure if this is "THE" way to do it, still working my way through angular)
http://jsbin.com/ebuhuv/7/edit - my exploration test.
One thing to note (per my example), one of my requirements was that the template would change based on a type attribute once you clicked save, and the templates were very different. So though, you get the data binding, if need a new template in there, you will have to recompile.
You should move your switch into the template by using the 'ng-switch' directive:
module.directive('testForm', function() {
return {
restrict: 'E',
controllerAs: 'form',
controller: function ($scope) {
console.log("Form controller initialization");
var self = this;
this.fields = {};
this.addField = function(field) {
console.log("New field: ", field);
self.fields[field.name] = field;
};
}
}
});
module.directive('formField', function () {
return {
require: "^testForm",
template:
'<div ng-switch="field.fieldType">' +
' <span>{{title}}:</span>' +
' <input' +
' ng-switch-when="text"' +
' name="{{field.name}}"' +
' type="text"' +
' ng-model="field.value"' +
' />' +
' <select' +
' ng-switch-when="select"' +
' name="{{field.name}}"' +
' ng-model="field.value"' +
' ng-options="option for option in options">' +
' <option value=""></option>' +
' </select>' +
'</div>',
restrict: 'E',
replace: true,
scope: {
fieldType: "#",
title: "#",
name: "#",
value: "#",
options: "=",
},
link: function($scope, $element, $attrs, form) {
$scope.field = $scope;
form.addField($scope);
}
};
});
It can be use like this:
<test-form>
<div>
User '{{!form.fields.email.value}}' will be a {{!form.fields.role.value}}
</div>
<form-field title="Email" name="email" field-type="text" value="me#example.com"></form-field>
<form-field title="Role" name="role" field-type="select" options="['Cook', 'Eater']"></form-field>
<form-field title="Sex" name="sex" field-type="select" options="['Awesome', 'So-so', 'awful']"></form-field>
</test-form>
One way is using a template function in your directive:
...
template: function(tElem, tAttrs){
return '<div ng-include="' + tAttrs.template + '" />';
}
...
If you want to use AngularJs Directive with dynamic template, you can use those answers,But here is more professional and legal syntax of it.You can use templateUrl not only with single value.You can use it as a function,which returns a value as url.That function has some arguments,which you can use.
http://www.w3docs.com/snippets/angularjs/dynamically-change-template-url-in-angularjs-directives.html
I managed to deal with this problem. Below is the link :
https://github.com/nakosung/ng-dynamic-template-example
with the specific file being:
https://github.com/nakosung/ng-dynamic-template-example/blob/master/src/main.coffee
dynamicTemplate directive hosts dynamic template which is passed within scope and hosted element acts like other native angular elements.
scope.template = '< div ng-controller="SomeUberCtrl">rocks< /div>'
I have been in the same situation, my complete solution has been posted here
Basically I load a template in the directive in this way
var tpl = '' +
<div ng-if="maxLength"
ng-include="\'length.tpl.html\'">
</div>' +
'<div ng-if="required"
ng-include="\'required.tpl.html\'">
</div>';
then according to the value of maxLength and required I can dynamically load one of the 2 templates, only one of them at a time is shown if necessary.
I heope it helps.

Resources