Angular 1.6 component transclusion scope - javascript

I'm trying to figure out how to get data into a component transclusion in Angular 1.6.4. The scenario has a component, a directive (not re-written as a component yet) and a service for inter-component communication.
angular.module('app')
.service('svc', function() {
this.connector = {};
})
.directive('first', ['svc', function($svc) { return {
restrict: 'E',
scope: { 'id': '#' },
template: '<button ng-click="GetData()">get data</button>',
controller: ['$scope', 'svc', function($scope, $svc) {
$scope.connector = { data: [] };
$svc.connector[$scope.id] = $scope.connector;
$scope.GetData = function() {
// This is a mock-up; I'm really doing a REST call.
$scope.connector.data = [
{id: 0, name: 'one'},
{id: 1, name: 'two'}
];
};
}]
}; }])
.component('second', {
bindings: { parent: '#firstid' },
transclude: true,
template: '<ng-transclude></ng-transclude>',
controller: ['svc', function($svc) {
this.data = $svc.connector[this.parent];
// Not sure what to do here
}]
})
;
My HTML looks something like this:
<first id="first-thing"></first>
<second firstid="first-thing">
Where I expect my data to be: {{$ctrl | json}}<br/>
... but maybe here: {{$ctrl.$parent | json}}<br/>
... or even here: {{$parent | json}}<br/>
<div ng-repeat="item in $ctrl.data">
<p>Output: {{item.id}}/{{item.name}}</p>
</div>
</second>
These may not be nested with a require, which is why I'm using a service to store my data; <first><second></second></first> is not an option. I can manage getting data from the service inside the component controller using some $onInit workarounds where necessary. I've checked and the service contains the correct data at the correct times. In the interest of component reuse, I need the controller to transclude content.
Batarang lists all my scopes. The directive has a scope, $id 6 (there are other things on the page), as expected. The component has a scope, $id 7, as expected. These scopes contain the correct data based on what I've put in them and what I'd expect.
My problem is that I have an additional scope, $id 8. It appears to be the transcluded scope and it is a sibling of 6 and 7 (these are peers on $id 5, my page controller). As noted in my HTML snark, I expected the component transclusion to live in 7. I would be fine if 8 was a child scope of 7, but it's a disconnected sibling. I tried additional bindings but I can't get them to populate so they just throw. I'm clearly doing something wrong because what I'm getting is the model that pre-1.3 used for transclusion scope inheritance.
Can someone tell me where I've gone astray or at least point me towards the correct solution?

I've figured it out. In passing, I should note that according to the literature on the Internet, I'm doing something that I probably shouldn't do. I understand where the authors of Angular are coming from with trying to isolate scopes down the chain but I don't agree with that model, at least for transclusion.
angular.module('app')
.service('svc', function() {
this.connector = {};
})
.directive('first', ['svc', function($svc) { return {
restrict: 'E',
scope: { 'id': '#' },
template: '<button ng-click="GetData()">get data</button>',
controller: ['$scope', 'svc', function($scope, $svc) {
$scope.connector = { data: [] };
$svc.connector[$scope.id] = $scope.connector;
$scope.GetData = function() {
// This is a mock-up; I'm really doing a REST call.
$scope.connector.data = [
{id: 0, name: 'one'},
{id: 1, name: 'two'}
];
$scope.connector.data.Update($scope.connector.data);
};
}]
}; }])
.component('second', {
bindings: { parent: '#firstid' },
transclude: true,
template: '<ng-transclude></ng-transclude>',
controller: ['$element', '$transclude', '$compile', 'svc', function($element, $transclude, $compile, $svc) {
this.$onInit = () => { angular.extend(this, $svc.connector[this.parent]; };
var parentid = $element.attr('firstid');
$transclude((clone, scope) => {
$svc.connector[parentid].Update = (data) => {
angular.extend(scope, data);
$element.append($compile(clone)(scope));
};
});
}]
})
;
How it works
This is essentially manual transclusion. There are too many examples on the Internet about manual transclusion where people modify the DOM manually. I don't completely understand why some people think this is a good idea. We jump through so many hoops to separate our markup (HTML) from our formatting (CSS) from our code (Angular directives/components) from our business logic (Angular services/factories/providers), so I'm not going to go back to putting markup inside my code.
I found this article and a comment on an Angular issue by Gustavo Henke that used the scope inside $transclude to register a callback. With that key bit of information, I figured I could do much more scope manipulation.
The code in $transclude seems to be outside the digest cycle. This means that anything touched inside it will not receive automatic updates. Luckily, I have control of my data's change events so I pushed through this callback. On the callback, the data are changed and the element is recompiled. The key to locate the callback in the service hasn't been bound from the controller tag yet so it has to be retrieved from the attributes manually.
Why this is bad
Components are not supposed to modify data outside their own scope. I am specifically doing exactly not-that. Angular doesn't seem to have a more appropriate primitive for doing this without breaking some other concern that's more important to leave intact, in my mind.
I think there's a, "memory leak," in this, which is to say that my element and scope aren't being disposed of correctly with each update cycle. Mine uses fairly little data, it is updated only directly by the user with a throttle and it's on an administration interface; I'm okay with leaking a little memory and I don't expect the user will stay on the page long enough for it to make a difference.
My code all expects things to be in the right place and named the right things in the markup. My real code has about four times as many lines as this and I'm checking for errors or omissions. This is not the Angular way which means I'm probably doing something wrong.
Credits
Without the Telerik article, I would have been sitting next to an even bloodier mark on my wall right now.
Thanks to Ben Lesh for his comprehensive post about $compile with appropriate disclaimers about how one shouldn't use it.
Todd Motto helped a bunch with how to write a decent Angular 1.x component in his post on upgrading to 1.6. As one may expect, the Angular documentation on components doesn't offer much more than specific pointers to exactly what things are called.
There's a little information at the bottom of AngularJS issue 7842 that does something similar and may even have a better method for managing scoped data more appropriately than I did.

Related

What can controllers do that directives can't? [duplicate]

This question already has an answer here:
Difference between and when to use controllers vs directives?
(1 answer)
Closed 4 years ago.
I'm just beginning AngularJS. When I first started reading about it, it seemed from the beginner tutorials that the controller is the basic building block of an angular app. However, since learning about directives, I've been creating my own little Angular app with only directives, literally not a single controller. I can't see why I would ever need a controller.
The only thing I've ever seen done with a controller is add variables to scope:
angular.controller("myController",
function($scope)
{
$scope.x = 5;
$scope.y = 6;
}
)
But I can do that with a directive too, by using the scope argument passed to the link function.
Is there something else that can be done with controllers, which can't be done with directives? Or at least something which is easier to do with controllers than with directives?
For example, if I just needed to populate scope with some variables x an y, I can just do:
angular.directive(
"myDirective",
function()
{
return {
link: function(scope, element, attributes)
{
scope.x = 5;
scope.y = 6;
}
};
}
);
You can probably write pretty much everything your app needs in a link callback, sure. Note that I'm not even calling it a directive, I'm saying a link callback. A directive is something that defines a custom HTML tag and its associated functionality, a link callback is merely a specific part of that.
The thing is that this is little more than working with jQuery, or using addEventListener to attach behaviour to HTML elements. On the other hand, you can write controllers as classes instead of procedural code manipulating the scope object. Here's my preferred style to write angularjs in typescript:
export default class WidgetController {
error: string;
static $inject = ['$state', 'FooService', 'BarService'];
constructor(
protected $state: angular.ui.IStateService,
protected foo: FooService,
protected bar: BarService
) {}
get fooValue() {
return this.foo.baz;
}
doSomething() {
this.error = null;
this.bar.getSomething().then(data => {
if (data.error) {
this.error = data.error;
} else {
this.$state.go('success');
}
});
}
}
A template for this might look like this:
<h1>{{ $ctrl.fooValue }}</h1>
<button ng-click="$ctrl.doSomething()">Do!</button>
<p>{{ $ctrl.error }}</p>
The controller may be attached to the template using the ui-router:
import WidgetController from 'widget-controller';
module.config(['$stateProvider', ($state: angular.ui.IStateProvider) => {
$state.state('widget', {
controller: WidgetController,
controllerAs: '$ctrl',
templateUrl: '/templates/widget.html',
});
}]);
Or as a component:
module.component('widget', {
templateUrl: '/templates/widget.html',
controller: WidgetController,
bindings: {
error: '#'
}
});
Or using ng-controller or in a number of other ways.
It gives you more flexibility. It allows you to test the controller pretty easily in isolation, since it's just a regular class. It allows you to reuse the controller for different templates, and the same template for different controllers (yes, this can actually be really useful). It's IMO more readable and easier to understand. Specifically using $ctrl. in the template prevents you from building too interdependent nested scopes and explicitly binds the template to use only its controller, instead of some implicit scope.
There are many ways to do things, but the one thing I have figured out over time is that dealing with the scope object is both verbose and annoying, and can easily lead to spaghetti code. So, moving away from that, you're soon arriving at controllers as objects.

Why does ng-click not fire in components template?

I am picking up Angular for a project of mine and am having trouble getting my first steps right.
Specifically, I can get a list of items to display via a component and appropriate template, but I can not figure out how to trigger ng-click events using the component model. Many similar problems to this have been answered on SO but I have followed the many corrections and suggestions without progress and need some advice.
file: customerList.js
function CustomerListController($scope, $element, $attrs, $http) {
this.customerList = [
{ name: 'Arya' },
{ name: 'No One' },
];
this.yell = function(customer) {
console.log("customer customer, we've got a click");
};
}
angular.module('myApp').component('customerList', {
templateUrl: 'customerList.html',
controller: CustomerListController,
});
And its template:
file: customerList.html
<div class="customer"
ng-repeat="customer in $ctrl.customerList"
customer="customer"
ng-click="$ctrl.yell(customer);">
Welcome home, {{customer.name}}!
</div>
Even when I set ng-click="console.log('click detected');", I get no console log.
I believe this is sufficient information to diagnose but please let me know if you need more.
Thanks!
First of all, console.log will not work directly in an angular expression. You can't use window functions directly in expressions.
Second, I would recommend using controllerAs syntax as it's a newer school way of doing things. Try accessing the controller with your controllerAs alias in the ng-click() expression.

Prevent controller from creating new scope object

I am passing a custom scope object to the $compile and creating a custom template. If I apply a directive on the elements inside the template, scope that is changing is the one that is passed to the $compile, and that's really what I wanted.
However, I just thought that it might be good to also have a controller on some elements inside the template,
<div ng-controller="controllerName" >
</div>
but ng-controller doesn't set data on the passed scope but creates its own and uses that one. Is there a way to make ngController to use existing scope and not create a new one ?
We create our controllers and wrap them in factories to make them accessible. We apply or controllers through directives (also going away). This gives you a controller that is scoped to the directive, which has better control for scope, this works for us as the directives where we do this for are usually components.
I don't know if this will be an option given the road you are down now. I would suggest trying to stop using ng-controller. You may want to look at angular 2 now just to keep it in mind as a migration path, it is coming in the fairly near future. They have removed ng-controller, a lot of what they are doing in angular 2 can be done now.
This is a good resource on why these things are a bad idea
https://www.youtube.com/watch?v=gNmWybAyBHI&t=9m10s
If you look at the source code for ng-controller, you will see it is very simple:
var ngControllerDirective = [function() {
return {
restrict: 'A',
scope: true,
controller: '#',
priority: 500
};
}];
You can actually create an almost identical alternate directive that just defines scope: false (or omits the scope key altogether, same thing):
app.directive('controllerNoScope', function () {
return {
restrict: 'A',
scope: false,
controller: '#',
priority: 500 // same as ng-controller
}
});
(You may want to give it a better name).
See this Plunkr for a demo that shows the scope has the same $id as the outer one, meaning it is the same scope.

How to find a directive within a controller and update it's data?

I'm very new to AngularJS and probably because of that I couldn't google the answer to my question but I really want to refactor my app in an MVC way until it will totally become a big piece of spaghetti :)
I have a directive that should indicate the number of users in groups that are logged in right now. Say it looks like this:
angular.module('dashboard').directive('indicator',
function($scope) {
var directive = {
restrict: 'E',
scope: { free: '=' },
template: '<div><h3>Free<h3><span>{{free}}</span></div>'
};
return directive;
});
This number updates instantly when it is changed for some group (I'm using SignalR for that). The JSON I'm getting from the server when something changes looks like this
{
groupId: 123,
loggedIn: 12,
onPause: 2,
total: 20
}
So I need somehow (as I think) to find the directive that is displaying data for a group with the given ID, and update it's scope object. What is the best way to do that?
<indicator free="freeVal"></indicator>
in your view with
$scope.freeVal = 25;
in your controller will show you Free25
Every time you change $scope.freeVal - shown value will also change.
So you don't need to change scope value from directive - just pass scope value to your directive (free="freeVal") and change value in scope when new data arrives - value in directive and in view will change automatically.

ui-select2 inside directive isn't updating controller model

I have a directive that takes in a collection and builds out a dropdown.
.directive("lookupdropdown", function () {
return {
restrict: 'E',
scope: {
collectionset: '=',
collectionchoice: '='
},
replace: true,
template: '<select class="input-large" ui-select2 ng-model="collectionchoice" data-placeholder="">' +
' <option ng-repeat="collection in repeatedCollection" value="{{collection.id}}">{{collection.description}}</option>' +
'</select>',
controller: ["$scope", function ($scope) {
$scope.repeatedCollection = new Array(); //declare our ng-repeat for the template
$scope.$watch('collectionset', function () {
if ($scope.collectionset.length > 0) {
angular.forEach($scope.collectionset, function (value, key) { //need to 'copy' these objects to our repeated collection array so we can template it out
$scope.repeatedCollection.push({ id: value[Object.keys(value)[0]], description: value[Object.keys(value)[1]] });
});
}
});
$scope.$watch('collectionchoice', function (newValue, oldValue) {
debugger;
$scope.collectionchoice;
});
} ]
}
});
This works fine. It builds out the drop down no problem. When I change the dropdown value, the second watch function gets called and I can see that it sets the value of collection choice to what I want. However, the collectionchoice that I have put into the directive doesn't bind to the new choice.
<lookupDropdown collectionset="SecurityLevels" collectionchoice="AddedSecurityLevel"></lookupDropdown>
That is the HTML markup.
This is the javascript:
$scope.SecurityLevels = new Array();
$scope.GetSecurityLevelData = function () {
genericResource.setupResource('/SecurityLevel/:action/:id', { action: "#action", id: "#id" });
genericResource.getResourecsList({ action: "GetAllSecurityLevels" }).then(function (data) {
$scope.AddedSecurityLevel = data[0].SCRTY_LVL_CD;
$scope.SecurityLevels = data;
//have to get security levels first, then we can manipulate the rest of the page
genericResource.setupResource('/UserRole/:action/:id', { action: "#action", id: "#id" });
$scope.GetUserRoles(1, "");
});
}
$scope.GetSecurityLevelData();
Then when I go to post my new user role, I set the user role field like this:
NewUserRole.SCRTY_LVL_CD = $scope.AddedSecurityLevel;
but this remains to be the first item EVEN though I have updated the dropdown, which according the watch function, it has changed to the correct value. What am I missing here?
You faced this issue because of the prototypical nature inheritance in Javascript. Let me try and explain. Everything is an object in Javascript and once you create an object, it inherits all the Object.Prototype(s), which eventually leads to the ultimate object i.e. Object. That is why we are able to .toString() every object in javascript (even functions) because they are all inherited from Object.
This particular issue on directives arises due to the misunderstanding of the $scope in Angular JS. $scope is not the model but it is a container of the models. See below for the correct and incorrect way of defining models on the $scope:
...
$scope.Username = "khan#gmail.com"; //Incorrect approach
$scope.Password = "thisisapassword";//Incorrect approach
...
$scope.Credentials = {
Username: "khan#gmail.com", //Correct approach
Password: "thisisapassword" //Correct approach
}
...
The two declarations make a lot of difference. When your directive updated its scope (isolated scope of directive), it actually over-rid the reference completely with the new value rather then updating the actual reference to the parent scope hence it disconnected the scope of the directive and the controller.
Your approach is as follows:
<lookupDropdown collectionset="SecurityLevels" collectionchoice="$parent.AddedSecurityLevel"></lookupDropdown>
The problem with this approach is that although it works, but it not the recommended solution and here is why. What if your directive is placed inside another directive with another isolated scope between scope of your directive and the actual controller, in that case you would have to do $parent.$parent.AddedSecurityLevel and this could go on forever. Hence NOT a recommended solution.
Conclusion:
Always make sure there is a object which defines the model on the scope and whenever you make use of isolate scopes or use ng directives which make use of isolate scopes i.e. ng-model just see if there is a dot(.) somewhere, if it is missing, you are probably doing things wrong.
The issue here was that my directive was being transcluded into another directive. Making the scope im passing in a child of the directive it was in. So something like $parent -> $child -> $child. This of course was making changes to the third layer and second layer. But the first layer had no idea what was going on. This fixed it:
<lookupDropdown collectionset="SecurityLevels" collectionchoice="$parent.AddedSecurityLevel"></lookupDropdown>

Categories

Resources