Required controller cannot be found when using $transclude in required controller - angularjs

Simplified example of the code....
HTML:
<div ng-app="app">
<m-carousel>
<m-carousel-slide>foo</m-carousel-slide>
<m-carousel-slide>bar</m-carousel-slide>
</m-carousel>
</div>
JavaScript:
class CarouselDirectiveController {
constructor($transclude) {
$transclude(clone => {
// do something
}, null, 'slide');
}
}
class CarouselSlideDirectiveController {
$onInit() {
console.log(this.carousel);
}
}
function carouselDirective() {
return {
restrict: 'E',
replace: true,
controller: CarouselDirectiveController,
controllerAs: 'carousel',
template: '<div class="m-carousel" ng-transclude="slide"></div>',
transclude: {
slide: 'mCarouselSlide'
},
scope: {},
bindToController: true
};
}
function carouselSlideDirective() {
return {
restrict: 'E',
replace: true,
transclude: true,
controller: CarouselSlideDirectiveController,
controllerAs: 'carouselSlide',
template: '<div class="m-carousel-slide" ng-transclude></div>',
scope: {},
bindToController: true,
require: {
carousel: '^^mCarousel'
}
};
}
angular.module('app.carousel', [])
.directive('mCarousel', carouselDirective)
.directive('mCarouselSlide', carouselSlideDirective);
angular.module('app', ['app.carousel']);
This throws the following error:
https://docs.angularjs.org/error/$compile/ctreq?p0=mCarousel&p1=mCarouselSlide
However, if I comment out the $transclude(clone => {}, null, 'slide'); then all is good in the world... How do I access the transcluded content in a parent controller if the controller is required by another?
CodePen: http://codepen.io/anon/pen/KzdNRE?editors=1011

I figured out what this was way back when... If you use $transclude this way you have to remember to manually append the transcluded content. So...
$transclude(clone => {
// do something then...
$element.append(clone);
}, null, 'slide');

Related

AngularJS values

How do I get the FirstName value in my directive?
<hello-World value="{{item.FirstName}}"> </hello-World>
app.directive('helloWorld', function() {
return {
restrict: 'AE',
replace: 'true',
template: '<h3>Hello ??????</h3>'
};
});
To get values you need to use scope parameter in directive. Read more from directives here https://docs.angularjs.org/guide/directive
Check this example
angular.module('myApp', [])
.directive('helloWorld', function() {
return {
restrict: 'AE',
replace: 'true',
template: '<h3>Hello {{value}}</h3>',
scope: {
value: '#'
},
};
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="myApp">
<hello-World value='World' />
</div>
app.directive('helloWorld', function() {
return {
restrict: 'AE',
replace: 'true',
template: '<h3>Hello {{value}}</h3>',
scope: {
value: '='
}
};
});

Directives, requires, angular js

Got two directives. One is a "subdirective"
Main:
.directive('materialDropdown', function() {
// Input to directive
return {
scope: {
//'personalDb': '=',
},
transclude: true,
controllerAs: 'vm',
bindToController: true,
controller: function(){
console.log('material directive fires')
},
template: '<select></select>',
restrict: 'E',
link: function(){}
};
});
sub:
.directive('materialSizeDropdown', function() {
// Input to directive
return {
require: '^^materialDropdown',
controllerAs: 'vm',
transclude: true,
bindToController: true,
controller: function(){
console.log('hello')
},
template: '<h1>fire</h1>',
restrict: 'E',
link: function(){}
};
Markup:
<material-dropdown>
<material-size-dropdown></material-size-dropdown>
</material-dropdown>
The materialSizeDropdown is not shown in the markup, and the controller is not fired either.
How do I solve this?
You're using the template option on your materialDropdown directive which will use for rendering. So your nested directive will never shown. And a <h1> in a <select> would not be valid.

Calling service method from a directive template

Let's say I have a directive:
<component>
<img ng-src='{{something}}' />
</component>
defined as:
app.directive("component", function() {
return {
scope: {},
restrict: 'E',
transclude: true,
template: "<a href='' ng-click='MyService.doThings()' ng-transclude></a>"
}
});
Despite all my efforts, I fail to understand how to accomplish two tasks:
How do I access the inner image source path?
How can I pass this path to service MyService? (think of a lightbox wrapper)
Update with solution:
app.directive("component", function(LightboxService) {
return {
restrict: 'E',
transclude: true,
replace: true,
template: "<a href='' ng-click='lb()' ng-transclude></a>",
link: function (scope, element, attrs) {
scope.lb = function () {
var src = $(element).find("img").attr("src");
LightboxService.show(src);
}
}
}
});
You can access the source path either by binding it to your controller scope or from a link method using attributes.
You can not access Service from a template. You should inject your service into a controller and define a function in $scope to call from the template.
Check your directive below:
app.directive("component", function() {
return {
scope: {
ngSrc: "#", //Text Binding
},
controller: function($scope, MyService) {
$scope.doThings = function() {
MyService.doThings();
}
},
restrict: 'E',
transclude: true,
template: "<a href='{{ng-src}}' ng-click='doThings' ng-transclude></a>"
}
});
You can learn more about directives with isolated scope here:
https://umur.io/angularjs-directives-using-isolated-scope-with-attributes/

AngularJS - accessing parent directive properties from child directives

This should not be too hard a thing to do but I cannot figure out how best to do it.
I have a parent directive, like so:
directive('editableFieldset', function () {
return {
restrict: 'E',
scope: {
model: '='
},
replace: true,
transclude: true,
template: '
<div class="editable-fieldset" ng-click="edit()">
<div ng-transclude></div>
...
</div>',
controller: ['$scope', function ($scope) {
$scope.edit = ->
$scope.editing = true
// ...
]
};
});
And a child directive:
.directive('editableString', function () {
return {
restrict: 'E',
replace: true,
template: function (element, attrs) {
'<div>
<label>' + attrs.label + '</label>
<p>{{ model.' + attrs.field + ' }}</p>
...
</div>'
},
require: '^editableFieldset'
};
});
How can I easily access the model and editing properties of the parent directive from the child directive? In my link function I have access to the parent scope - should I use $watch to watch these properties?
Put together, what I'd like to have is:
<editable-fieldset model="myModel">
<editable-string label="Some Property" field="property"></editable-string>
<editable-string label="Some Property" field="property"></editable-string>
</editable-fieldset>
The idea is to have a set of fields displayed by default. If clicked on, they become inputs and can be edited.
Taking inspiration from this SO post, I've got a working solution here in this plunker.
I had to change quite a bit. I opted to have an isolated scope on the editableString as well because it was easier to bind in the correct values to the template. Otherwise, you are going to have to use compile or another method (like $transclude service).
Here is the result:
JS:
var myApp = angular.module('myApp', []);
myApp.controller('Ctrl', function($scope) {
$scope.myModel = { property1: 'hello1', property2: 'hello2' }
});
myApp.directive('editableFieldset', function () {
return {
restrict: 'E',
scope: {
model: '='
},
transclude: true,
replace: true,
template: '<div class="editable-fieldset" ng-click="edit()"><div ng-transclude></div></div>',
link: function(scope, element) {
scope.edit = function() {
scope.editing = true;
}
},
controller: ['$scope', function($scope) {
this.getModel = function() {
return $scope.model;
}
}]
};
});
myApp.directive('editableString', function () {
return {
restrict: 'E',
replace: true,
scope: {
label: '#',
field: '#'
},
template: '<div><label>{{ label }}</label><p>{{ model[field] }}</p></div>',
require: '^editableFieldset',
link: function(scope, element, attrs, ctrl) {
scope.model = ctrl.getModel();
}
};
});
HTML:
<body ng-controller="Ctrl">
<h1>Hello Plunker!</h1>
<editable-fieldset model="myModel">
<editable-string label="Some Property1:" field="property1"></editable-string>
<editable-string label="Some Property2:" field="property2"></editable-string>
</editable-fieldset>
</body>
You can get access to parent controller by passing attribute in child directive link function
link: function (scope, element, attrs, parentCtrl) {
parentCtrl.$scope.editing = true;
}

How to add ngshow directive inside compile/link?

I want to add ngshow in the following custom element in a dynamic way... How to do that?
<toggler on-enable="main.enable()" on-disable="main.disable()">
<div style="width:100px;height:100px;background-color:#2fa">
<on>On state</on>
<off>Off state</off>
</div>
</toggler>
cf.directive('toggler', function () {
return {
restrict: 'AE',
scope: {
state: true,
onEnable: '&',
onDisable: '&'
},
compile: function (elem, attrs) {
var onElem = elem.find('on');
var offElem = elem.find('off');
// WANT TO DO THIS
// onElem.ngShow = 'state';
// offElem.ngShow = '!state';
}
};
});
You're doing it in the wrong way. Don't forget a rule of thumb in AngularJS: avoid DOM manipulation when it's not mandatory.
I guess that <on> and <off> are also custom directives, because you can't simply add tags without any defined behaviour. So, why don't put the ngShow attribute directly in this directives? Then, a directive's controller (see the documentation) will handle the communication between <on>/<off> and <toggler>:
myApp.directive('toggler', function () {
return {
restrict: 'AE',
scope: {
state: '=',
},
controller : [
'$scope',
function ($scope) {
this.isOn = function () {
return $scope.state;
};
},
],
};
});
myApp.directive('on', function () {
return {
restrict: 'AE',
require: '^toggler',
template: '<div ng-show="isOn()" ng-transclude />',
replace: true,
scope: true,
transclude: true,
link : function ($scope, element, attributes, togglerController) {
$scope.isOn = togglerController.isOn;
},
};
});
myApp.directive('off', function () {
return {
restrict: 'AE',
require: '^toggler',
template: '<div ng-hide="isOn()" ng-transclude />',
replace: true,
scope: true,
transclude: true,
link : function ($scope, element, attributes, togglerController) {
$scope.isOn = togglerController.isOn;
},
};
});
Fiddle
This way, you will be able to simply unit test your toggler, and extend his behaviour when needed.

Resources