How to avoid `require` and Access the controller of the parent component in transclusion - angularjs

I'm trying to build a form component that receives an object as input and use the template defined into the object to ng-include the right template to render the form defined in the model.
The problem I have is the object might be defined in the above component. For example this:
<somecomponent>
<formx object="$ctrl.settings"></formx>
</somecomponent>
Unfortunately, it doesn't seem to work. From what I read the transcluded block should be using the scope of the above controller. Is there a way to access the scope of the component somecomponent?
By the way, what I'm looking for is to do the same as:
<div ng-controller="SomeController as ctrl">
<formx object="ctrl.settings"></formx>
</div>
But instead of using a plain controller I'd like to use a component without using an explicit require as the parent component might be different from time to time.

With components the ng-include directive adds a child scope to the isolate scope. Transcluded components need to reference $parent:
<somecomponent settings="'ss'">
̶<̶f̶o̶r̶m̶x̶ ̶o̶b̶j̶e̶c̶t̶=̶"̶$̶c̶t̶r̶l̶.̶s̶e̶t̶t̶i̶n̶g̶s̶"̶>̶<̶/̶f̶o̶r̶m̶x̶>̶
<formx object="$parent.$ctrl.settings"></formx>
</somecomponent>
The DEMO
angular.module("app",[])
.component("somecomponent",{
transclude: true,
bindings: {settings:"<"},
template: `
<fieldset>
somecomponent scope-{{$id}}
<ng-transclude>
</ng-transclude>
</fieldset>
`
})
.component("formx",{
bindings: {object:"<"},
template: `
<fieldset>
formx scope-{{$id}}<br>
object={{$ctrl.object}}
</fieldset>
`
})
<script src="//unpkg.com/angular/angular.js"></script>
<body ng-app="app">
<somecomponent settings="'ss'">
<formx object="$parent.$ctrl.settings"></formx>
</somecomponent>
</body>

Related

Ng-if inside ng-template does not work

I created a component and I am trying to make the component's template dynamic, that is, for some condition the parent tag should be a div, otherwise it should be an anchor tag.
I have been trying to use ng-if but somehow it wont work. Here is a code snippet. For some reason, even if the ng-if is true, the nested div (.testDiv .testThumbnail) will be undefined and this will break my component.
I cannot understand why it doesn't find the component even if the ng-if is true. I am new to Angular JS, so maybe I am missing something here? Or there is a better way to dynamically create the component's parent tags according to some condition.
function myCardController($window) {
var element = angular.element(document.querySelector('.testDiv .testThumbnail'));//is undefined
}
angular.module('myApp').component('myCard', {
templateUrl: 'testTemplate', ,
controller: ["$window", myCardController],
});
<script type="text/ng-template" id="testTemplate">
<div ng-if="true"
class="testDiv">
<div role="img" class="testThumbnail"></div>
</div>
<a ng-if="false" class="tesstDiv">same content</a>
</script>
You probably miss to add ng-app or ng-controller directive. Use following HTML as your template:
<div ng-controller = "myCardController">
<div ng-if="show"
class="testDiv">
<div role="img" class="testThumbnail"></div>
</div>
<a ng-if="!show" class="tesstDiv">same content</a>
</div>
Update your JS code as like:
function myCardController($window) {
var element = angular.element(document.querySelector('.testDiv .testThumbnail'));//is undefined
$scope.show = true;
}
angular.module('myApp').component('myCard', {
templateUrl: 'testTemplate.html', ,
controller: ["$window", myCardController],
});
You also can use ng-app="myApp" in your body tag.

Can a CSS class selector be applied to a component?

I have the following Plunker that uses ui-router and Angular's 1.6x new component feature.
The state 'userRegister' becomes active then initialises the 'userRegister' component. This component injects a new <user-register/> into the <ui-view> then injects the HTML contents of the ng-template script block, which is all working fine.
The final DOM ends up being:
<ui-view class="ng-scope">
<user-register class="ng-scope ng-isolate-scope">
<h1 class="header">Create account</h1>
</user-register>
</ui-view>
However, I cannot find a way to add a CSS class selector to the <user-register/> tag.
e.g. using a class selector called .example I'd like to achieve the following:
<user-register class="example ng-scope ng-isolate-scope">...<user-register/>
Any ideas please?
Sure you could always wrap the template on a div and put the class there.
If don't want to do it, you can inject the $element and use the $postLink function to add the class you need:
.component('userRegister', {
templateUrl: '/views/user-register',
controller: function($element) {
this.$postLink = function() {
$element.addClass('example');
}
}
})
Here is the working plunker:
https://plnkr.co/edit/VuWu8L9VqrgJRGnxItY2?p=preview
Final DOM:
<user-register class="ng-scope ng-isolate-scope example">
<h1 class="header">Create account</h1>
</user-register>

Angular component with transcluded markup

I am trying to create an Angular component and transclude the inner HTML of the component, but the markup of the inner HTML does not seem to be compiling. My use case for this is that the component has an attribute binding that I want to use in multiple ways, so the template will never be exactly the same.
For example, say I have the following simple controller:
class ComponentCtrl {
$onInit() {
this.variable = 'hello world';
}
}
let MyComponent = {
controller: ComponentCtrl
};
app.component('myComponent', MyComponent);
I want the following HTML:
<my-component>
<div style="color: green;">{{ $ctrl.variable }}</div>
</my-component>
<my-component>
<div style="color: red;">{{ $ctrl.variable }}</div>
</my-component>
to render as:
<div style="color: green;">hello world</div>
<div style="color: red;">hello world</div>
However, right now it is only rendering as:
<div style="color: green;"></div>
<div style="color: red;"></div>
without the markup being evaluated.
Is there something I'm doing wrong?
did you write the right name of controller inside your {{ }} in html? you wrote controller: ComponentCtrl and then {{ $ctrl.variable }}. it looks like they must have the same names
I think the problem come from {{ $ctrl.variable }}. In fact $ctrl try to link with a parent controller not with the controller of your component.
If you want interact with the controller of your component you need to use some parameter.
Transclusion is not made by default, you have to especify on your component that it has to be transcluded. Also, you didn't especify on your template where it should be trasncluded. Therefore, your component should look like:
let MyComponent = {
transclude: true, // tell angular to transclude it
template: '<ng-transclude></ng-transclude>', // tell where it will be transcluded
controller: ComponentCtrl
};
app.component('myComponent', MyComponent);
However, how was told on comments, component scopes are always isolated. Therefore, ou won't be able to access {{ $ctrl.variable }} from outside the component.
The transcluded content's scope has a $parent property that always points to the host component's scope.
So you could do something like this -
<my-component>
<div style="color: green;">{{ $parent.$ctrl.variable }}</div>
</my-component>
<my-component>
<div style="color: red;">{{ $parent.$ctrl.variable }}</div>
</my-component>
Plunk link that uses $parent property - http://run.plnkr.co/preview/ckdwiuzlb00073b661a7blt3f/

Directive doesn't work when I which the version of Angular to 1.0.1 to 1.2.27

The following could be run in demo here.
this is html:
<div ng-controller="MyCtrl">
<h2>Parent Scope</h2>
<input ng-model="foo"> <i>// Update to see how parent scope interacts with component scope</i>
<br><br>
<!-- attribute-foo binds to a DOM attribute which is always
a string. That is why we are wrapping it in curly braces so
that it can be interpolated.
-->
<my-component attribute-foo="{{foo}}" binding-foo="foo"
isolated-expression-foo="updateFoo(newFoo)" >
<h2>Attribute</h2>
<div>
<strong>get:</strong> {{isolatedAttributeFoo}}
</div>
<div>
<strong>set:</strong> <input ng-model="isolatedAttributeFoo">
<i>// This does not update the parent scope.</i>
</div>
<h2>Binding</h2>
<div>
<strong>get:</strong> {{isolatedBindingFoo}}
</div>
<div>
<strong>set:</strong> <input ng-model="isolatedBindingFoo">
<i>// This does update the parent scope.</i>
</div>
<h2>Expression</h2>
<div>
<input ng-model="isolatedFoo">
<button class="btn" ng-click="isolatedExpressionFoo({newFoo:isolatedFoo})">Submit</button>
<i>// And this calls a function on the parent scope.</i>
</div>
</my-component>
</div>
And this is js:
var myModule = angular.module('myModule', [])
.directive('myComponent', function () {
return {
restrict:'E',
scope:{
/* NOTE: Normally I would set my attributes and bindings
to be the same name but I wanted to delineate between
parent and isolated scope. */
isolatedAttributeFoo:'#attributeFoo',
isolatedBindingFoo:'=bindingFoo',
isolatedExpressionFoo:'&'
}
};
})
.controller('MyCtrl', ['$scope', function ($scope) {
$scope.foo = 'Hello!';
$scope.updateFoo = function (newFoo) {
$scope.foo = newFoo;
}
}]);
This should be a good example for three kinds of scope binding in directives.However, it just doesn't work when I try to switch a higher angular version - (1.2.27). I suspect the shadow of the inherited scope within the directive, but I'm not sure of it.
This isn't going to work the way you expect. Isolated Scopes are created and provided to the Link, Compile, and Template portions of a Directive. However, the HTML within the Element itself is not actually part of the Directive. Those HTML portions are still bound to the parent $scope. If you have a tendancy to name your isolated scope objects the same, you may have just been working against the $scope unintentionally and not noticed any ill effect. If your HTML was in a Template rather than inside the Element, it would access the isolate scope.
As an example, in the HTML that is inline in the Element, you can call updateFoo(), but that would not be possible from inside a Template

$scope.$emit not working while $rootScope.$broadcast does

Here's my parent controller where I listen for the event
app.controller("SectionLayoutController", function($scope) {
$scope.$on("sectionLayout.doAction", function(e, options) {
// do some stuff
});
});
And in my child controller I do this
$scope.$emit("sectionLayout.doAction", { someOption: "something" });
The event should be triggered in the parent controller but it isn't. I did check what the contents of my child controller's scope was (with console.log) right before emitting the event, and the listener for the event does exist in one of its parents.
Maybe it's because the child controller's scope is not a direct child of the scope where the event is being listened on? This is not really a critical issue because I can just replace the line with
$rootScope.$broadcast("sectionLayout.doAction", { someOption: "something" });
and it works fine, but I'm still curious as to why $scope.$emit isn't working (it did work at some point, it just stopped randomly).
(Angular version 1.2.16)
EDIT
The problem seems to be a bug in angular.js, not ui-router (it has already been reported and is scheduled for a fix in version 1.3 https://github.com/angular/angular.js/issues/5489). Using ng-transclude inside a directive seems to create a sibling scope for the directive, which makes this kind of hierarchy (with my first plunker example) : http://i.imgur.com/4OUxJSP.png.
So yeah, I guess for now i'll be using $rootScope.$broadcast
Ok, so from what I've found, this is a bug with ui-router (forgot to mention I was using that, oops).
If the ui-view of a child route is inside a transcluded directive, events will get stuck there. Here's a plunkr of that bug in action http://plnkr.co/edit/zgqAPiDbB2LUtwJaeHhN?p=preview. The Dummy controller uses the sectionLayout directive and the ui-view is transcluded (as you can see in dummy.html).
<!-- dummy.html -->
<div section-layout="layout">
<!-- transcluded stuff -->
<div ui-view></div>
</div>
<!-- sectionlayout.html -->
<div>
<p>Section Layout for {{layout.title}}</p>
<p ng-repeat="r in recieves">{{r.message}}</p>
<div ng-transclude style="background-color: #EEE;"></div>
</div>
Here's another plunkr where $scope.$emit does work, and the only difference is that the ui-view is directly inside sectionlayout.html instead of transcluded in the directive http://plnkr.co/edit/mVftwkZrynkF6KanE4zV?p=preview.
<!-- dummy.html -->
<div section-layout="layout"></div>
<!-- sectionlayout.html -->
<div>
<p>Section Layout for {{layout.title}}</p>
<p ng-repeat="r in recieves">{{r.message}}</p>
<div ui-view style="background-color: #EEE;"></div>
</div>
And here's a completely different plunkr where I don't use ui-router, but it's still the same thing. A directive is used in a parent where events are listened, and the child controller is transcluded in the directive. In this one, both $scope.$emit and $rootScope.$broadcast work fine, so it seems to be a bug with transcluded ui-views in ui-router. http://plnkr.co/edit/Iz5YcbMiTzrXQ6bJMskK?p=preview
<div ng-controller="ParentController">
<p>Parent Controller</p>
<p ng-repeat="r in recieves">{{r.message}}</p>
<div my-directive>
<div ng-controller="ChildController" style="background-color: #EEE;">
<p>Child Controller</p>
<button ng-click="tryEmit()">$scope.$emit</button>
<button ng-click="tryBroadcast()">$rootScope.$broadcast</button>
</div>
</div>
</div>
I'll report this bug on their github page

Resources