How do I access data from a parent directive view? - angularjs

I'm trying to build a set of directives that handle our standard table layout for our site. For this I created two directives, one for the table and one for the columns. The goal here is to be able to dictate the columns of the table in the parent view.
Here's the parent view (index.html) that instantiates the directives. Note that col-name indicates what attribute I want to show for each row in the table.
<management-grid>
<col-data col-name="name"></col-data>
</management-grid>
Here's the relevant aspects of the managementGrid directive.
.directive('managementGrid', function() {
return {
...
template:
'<table>' +
'<tbody>' +
'<tr ng-repeat="row in collection" ng-transclude="data">' +
'<!--The specific columns go here using col-data-->' +
'</tr>' +
'</tbody>' +
'</table>',
transclude: {
'data': '?colData'
},
controller: ($scope) => {
$scope.collection = [{'name': 'Test Row 1'},{'name': 'Test Row 2'}];
}
};
});
And the child directive which is really only used to convert to a td tag so the browser doesn't discard it before the directive renders.
.directive('colData', function() {
return {
...
scope: {
colName: '#'
},
template: '<td>{{colName}}: {{row[colName]}}</td>'
};
});
In the final directive colData, the row object is not accessible. I know I can use a link function to access the parent controller, but that will only allow me to access collection and I need to access row, which is created using ng-repeat.
Here's Fiddle showing the issue.
edit:
Ideally I could simply transclude into the colData directive and not have to pass a property name in. Something like <td ng-transclude></td> and in the parent view <col-data>{{row.name}}</col-data>

I was able to solve this using $parent in the child directive. This accesses the parent scope. See the updated fiddle here.

Related

Dynamically inserting directives/components into angular table cells

I have a table directive that basically loops through an array and inserts html like this:
<tr ng-if="isComplete()" ng-repeat="row in paged.page()" ng-click="rowClick(row, $event)">
<td ng-repeat="header in headers" ng-bind-html="trustAsHtml(header.formatter(row[header.property]))"></td>
</tr>
The directive is configured like this:
$scope.headers = [
comsTable.header('id', 'ID', function(id, obj) {
return '' + id + '';
}),
comsTable.header('name', 'Name'),
comsTable.header('age', 'Age')
];
WHere the first argument is an object property of each element in the array.
And ng-bind-html is used and I can pass in html and I use trustAsHtml from the $ce service.
I am able to insert basic html like this when configuring the table:
$scope.headers = [
comsTable.header('id', 'ID', function(id, obj) {
return '' + id + '';
}),
If I wanted to insert a dynamically created directive. Can I achieve this without re-architecting the whole table. I believe I would have to use $compile and attach the node/element to the DOM but could I wait until after the table has been rendered to call $compile or what would be the best way of achieving this?
I'm new to angular so I'm not sure of what is the best way of achieving this.
Here is a plunker that shows the directive I have so far

Angular directives with a shared controller don't share the controller's $scope

If I have two different directives with isolated scopes, and they share a controller, shouldn't they share the controller's $scope?
Effectively, I have the following scenario:
I call to the server and pass it some parameters, let's call them x and y.
The server either returns some data, or it returns null which indicates that this data is not available.
If the data is returned, I need to display a button; if the user clicks the button, it will display the data in a separate div.
If the data is not returned, I do not display the button.
I've been trying to implement this as a set of linked directives. I'm trying to do it this way because this "component" is to be re-used multiple times in the application, and I'm trying to do this in two directives because I couldn't figure out any other way to make a single directive control two different elements.
In addition to there being many, they need to be linked by x and y values. So if I have a button with particular x and y, then clicking on it will only toggle the visibility of the display area with the same x and y values. Effectively a given view will have multiple of each with different x and y values.
Anyway, I have a potential working solution, but I seem to be having problems with the shared scope. When I click the button, I have logging statements which correctly show that we trigger the "show" logic. But the logging statements in the ng-if of the div consistently evaluate the same logic to false and doesn't display.
My solution is in three parts:
A directive for the button
A directive for the "display"
A controller that is shared by the two
I have a trivial working example, which I will share below. There's a Plunkr URL at the end of this post as well.
Here is the HTML. The <p> tag in the middle is just to demonstrate that the two directives are physically not adjacent.
<trigger x="apple" y="2" ></trigger>
<p>Some unrelated dom content...</p>
<display x="apple" y="2"></display>
This is the trigger directive, which is the button:
app.directive("trigger", function() {
return {
restrict: "E",
scope: {
x : "#",
y : "#"
},
transclude: false,
template: "<button ng-if='hasCalculation(x,y)' ng-click='toggle()'>Trigger x={{x}} & y={{y}}</button>",
controller: 'testController',
link: function(scope) {
scope.doSomeWork();
}
};
});
This is the display directive, which is supposed to show the data when toggled by the button:
app.directive("display", function() {
return {
restrict: 'E',
scope: {
x : '#',
y : '#'
},
require: '^trigger',
transclude: false,
controller: 'testController',
template: "<p ng-if='shouldShow(x,y)'>{{getCalculation(x,y)}}</p>"
};
});
This is the shared controller, testController:
app.controller("testController", ["$scope", function($scope) {
$scope.shouldShow = [[]];
$scope.calculatedWork = [[]];
$scope.doSomeWork = function() {
var workResult = "We called the server and calculated something asynchonously for x=" + $scope.x + " and y=" + $scope.y;
if(!$scope.calculatedWork[$scope.x]) {
$scope.calculatedWork[$scope.x] = [];
}
$scope.calculatedWork[$scope.x][$scope.y] = workResult;
};
$scope.hasCalculation = function(myX, myY) {
var xRes = $scope.calculatedWork[myX];
if(!xRes) {
return false;
}
return $scope.calculatedWork[myX][myY]
}
$scope.toggle = function() {
if(!$scope.shouldShow[$scope.x]) {
$scope.shouldShow[$scope.x] = [];
}
$scope.shouldShow[$scope.x][$scope.y] = !$scope.shouldShow[$scope.x][$scope.y];
console.debug("Showing? " + $scope.shouldShow[$scope.x][$scope.y]);
}
$scope.isVisible = function(myX, myY) {
console.debug("Checking if we should show for " + myX + " and " + myY);
var willShow;
if(!$scope.shouldShow[myX]) {
willShow = false;
} else {
willShow = $scope.shouldShow[myX][myY];
}
console.debug("Will we show? " + willShow);
return willShow;
}
$scope.getCalculation = function(myX, myY) {
if(!$scope.calculatedWork[myX]) {
return null;
}
return $scope.calculatedWork[myX][myY];
}
}]);
Here is the Plunkr.
If you go to the Plunkr, you'll see the trigger button correctly rendered. If you click the button, you'll see that the toggle() method correctly flips the value of the shouldShow to the opposite of what it was previously (there's a console debug statement in that method that shows its result). You'll also see the re-evaluation of the isVisible method which determines if the display should show -- this always returns false.
I think it has to do with the fact that I'm saving my data and visibility state relative to $scope. My understanding of this is that each individual directive has its own $scope, but since they share a controller, shouldn't they share that controller's $scope?
An isolate scope does not prototypically inherit the properties of the parent scope.
In AngularJS, a child scope normally prototypically inherits from its parent scope. One exception to this rule is a directive that uses scope: { ... } -- this creates an "isolate" scope that does not prototypically inherit.(and directive with transclusion) This construct is often used when creating a "reusable component" directive. In directives, the parent scope is used directly by default, which means that whatever you change in your directive that comes from the parent scope will also change in the parent scope. If you set scope:true (instead of scope: { ... }), then prototypical inheritance will be used for that directive.
Source: https://github.com/angular/angular.js/wiki/Understanding-Scopes
Additionally, a controller is not a singleton it is a class...so two directives can't share a controller. They each instantiate the controller.
If you want two controllers to share the same data, use a factory.

AngularJS Applying model to dynamic form elements

I'm trying to bind model data from a controller to a directive template. The directive template repeats form input types based on a form object from a controller.
The goal is to use this directive in multiple places and bind the appropriate model to the form as opposed to creating a form each time.
I've found examples using transclusion and isolated scopes, however, each of the examples I've come across seem to have the model properties hardcoded.
Here is a jsfiddle that hopefully explains better what I'm trying to achieve.
http://jsfiddle.net/N9rSa/14/
app.controller('FormCtrl',function($scope) {
$scope.form = [
{label:'First',type:'text',name:'first_name'},
{label:'Last',type:'text',name:'last_name'}
];
$scope.person = {first_name:'Jimmy',last_name:'Page'};
});
app.directive('formelements',function() {
return {
restrict: 'E',
scope:false,
template: '<div ng-repeat="elements in form"><input type="text" ng-model="field.name"></div>'
}
});
Thanks for any help.
You can use the following as template:
template: '<div ng-repeat="element in form">' +
'<input type="text" ng-model="person[element.name]"></div>'
Fiddle: http://jsfiddle.net/g5Mzs/
Essentially the name field is used to define the index inside the person object.

Apply an Angular directive only after data is loaded

I have a directive applied to a table, the outer div has a controller applied to it:
<div ng-controller="MyController">
<table ng-mydirective>
...
</table>
</div>
The controller loads some data and then uses ng-repeat to create some rows/columns in the table.
This is fine so far.
The directive needs to access columns possibly generated by the data, but the directive runs before the controller.
Is it possible to run a directive after the data is loaded/rendered? Or is the only way to achieve this by using $watch on the dom of the table?
<table ng-show="myDataSource.length>0" ng-mydirective>
...
</table>
or how about applying that within your directive's template itself, i.e hide the table until after the data source is populated. But in both cases the problem is that what if you display a table whose query returned 0 results? That means you directive won't show at all. Either way the best way to handle this would be inside the directive. I.e. hide the table unless your $http call executed successfully.
If your directive isn't using an isolate scope, it will automatically receive access to the data member in the controller scope. If you reference that data member in the template, Angular automatically sets up a $watch on it and you don't have to do anything else; otherwise you will need to add your own $watch. If you're using an isolate scope then it's a little more complicated but a similar pattern holds.
You have two ways for doing this
Easy one is using ng-if for requested data field. For example
<table ng-mydirective ng-if="nameOfTheDataSource !== undefined">
...
</table>
Also you can send an attribute to the directive and watch it until data arrives. This is more complicated but seemless to the template level:
Template
<table ng-mydirective ng-model="nameOfTheDataSource">
...
</table>
Directive
angular.module(....)
.directive('ng-mydirective', function() {
return{
scope: {
....
ngModel: '='
}
link: function(scope, element, attrs) {
var firstload = scope.$watch('ngModel', function(value) {
if (scope.ngModel !== undefined) {
// Do what is needed
}
// Deregister itself
firstload();
}
}
}
}

Binding data in a custom directive - AngularJS

I have a custom directive, and its purpose is to present a widget and bind it to a variable.
Every variable has different data type, so different widgets will be presented depending on the data type.
My problem is that I can pass the data of the variable, but I can't manage to bind the widget to it.
To simplify the problem, my widget is just a simple text input.
When I try to $compile the widget, Angular uses the value of the variable instead of binding to it.
HTML:
<body ng-app="app" ng-controller="myCtrl">
<input type="text" ng-model="resource.name"></div>
<div custom-widget widget-type="widget" bind-to="resource"></div>
</body>
Javascript:
angular.module('app', [])
.directive('customWidget', function($compile) {
return {
replace: true,
template: '<div></div>',
controller: function($scope) {
},
scope: {
bindTo: "=bindTo",
widgetType: "=widgetType"
},
link: function(scope, iElem, iAttrs) {
var html = '<div>' + scope.widgetType.label + ':<input ng-bind="' + scope.bindTo[scope.widgetType.id] + '" /></div>';
iElem.replaceWith($compile(html)(scope));
}
};
})
.controller('myCtrl', function($scope) {
$scope.widget = {
id: 'name',
label: 'Text input',
type: 'text'
};
$scope.resource = {
name: 'John'
};
});
Plunker demo: http://plnkr.co/edit/qhUdNhjSN7NlP4xRVcEA?p=preview
I'm still new to AngularJS and my approach may not be the best, so any different ideas are of course appreciated!
Since you're using an isolate scope one issue is that resource is on the parents scope and not visible within the directive. And I think you're looking for ng-model rather than ng-bind.
Also, since you want to bind to namein resource, we need to tie that in somehow.
So here's one approach to your template html (note the addition of $parent to get around the scope issue and the addition of .name(which you could add programatically using a variable if you preferred, or specify it as part of the attribute))
var html = '<div>' + scope.widgetType.label + ':<input ng-model="' + '$parent.' + iAttrs.bindTo +'.name'+ '" /></div>';
Updated plunker
Well, when you have a isolated scope within your directive and use the "=" operator you already have two-way data binding.
My suggestion would be to use the "template" more like a view so the operations are clearer.
I would change your directive to the following:
Using ng-model instead of ng-bing mainly because as the Documentation reveals:
The ngModel directive binds an input,select, textarea (or custom form control) to a property on the scope using NgModelController, which is created and exposed by this directive. [...]
Changed directive:
angular.module('app', [])
.directive('customWidget', function($compile) {
return {
replace: true,
template: '<div> {{widgetType.label}} <input ng-model="bindTo[widgetType.id]" /></div>',
scope: {
bindTo: "=bindTo",
widgetType: "=widgetType"
}
};
});
EDIT:
Ops forgot the Updated Plunker

Resources