Angular Expressive Directive Design - angularjs

I'm working on a open-source project for a AngularJS Data Table directive ( still WIP ). When you look at components like Angular Grid or UI Grid they all describe their columns and attributes in a object in the parent controller like:
$scope.gridOptions = {
enableSorting: true,
enableCellEditOnFocus: true,
columnDefs: [
{ name: 'field1', enableSorting: false, enableCellEdit: false },
{ name: 'field2' },
{ name: 'field3', visible: false }
]
};
which works fine, however, I don't think this is really the 'angular way'. It feels more like a jQuery widget. If you look at projects like Angular Material, they are much more expressive in the HTML template vs object driven.
For my implementation, I originally wanted to make it very expressive and expose each one of the inner directives I use, however, that ended up a mess to just create a basic table. So I did some research on other frameworks and found that react had a nice architecture where you just define the columns like:
React.render(
<Table
rowHeight={50}
rowGetter={rowGetter}
rowsCount={rows.length}
width={5000}
height={5000}
headerHeight={50}>
<Column
label="Col 1"
width={3000}
dataKey={0}
/>
<Column
label="Col 2"
width={2000}
dataKey={1}
/>
</Table>,
document.getElementById('example')
);
I feel in love with this approach, its simple and is expressive all at the same time. 90% of the time you only want to customize the column template anyways. So instead of this:
$scope.gridOptions = {
enableFiltering: true,
rowTemplate: rowTemplate(),
data: 'data',
columnDefs: [
{ name: 'name' },
{ name: 'gender' },
{ name: 'company' },
{ name: 'widgets' },
{
name: 'cumulativeWidgets',
field: 'widgets',
cellTemplate: '<div class="ui-grid-cell-contents" title="TOOLTIP">{{grid.appScope.cumulative(grid, row)}}</div>'
}
]
};
with the cell template, you could do something like this:
<dt options="options" rows="data" class="material">
<Column name="name" width="300"></Column>
<Column name="Gender">
<strong>{{value}}</strong>
</Column>
<Column name="Company"></Column>
</dt>
notice how I took the React concept and paired it with a more Angular concept where I would include the ability to have a template inside the column.
Ok now for the problem. I want the columns on init, but not after that. I want to replace it with the actual table. Problem is I can never get that HTML when I need it.
So on this line I tried to do something like:
compile: function(tElem, tAttrs){
var tags = z.getElementsByTagName('column');
console.log(tags) // equals = []
return {
pre: function($scope, $elm, $attrs, ctrl){
}
};
}
but the columns are never there til later when I try to transclude them ( not really what I wanna do ). I need a way to get ahold of them before the controller is initialized and the template replaces the inner contents. Here is a plunkr!
Additionally, since my directive is scoped ( which I def wanna do for perf reasons ) I have no way to get access to the parent scope to compile the inner template with the outer contents.
Also, any suggestions / thoughts on this design paradigm? Thanks!

Try to use the template as a function, which returns the actual template string. The first parameter is the original html:
var tags;
return {
template: function(element) {
tags = element[0].getElementsByTagName('column');
return "<div>table</div>"
},
compile: ...
}

I only found a weird way to achieve what you want.
Before someone give a better solution, here is mine working in this plunker.
Adding the $transclude service to the controller and transclude:'element' to the directive
app.directive("simple", function(){
return {
restrict: "EA",
replace:true,
transclude:'element',
template:"<div>table</div>",
compile: function(element, attributes, transclude){
var tags = element[0].getElementsByTagName('column');
console.log("compile", tags);
console.log("transclude", transclude);
return {
pre: function(scope, element, attributes, controller, transcludeFn){
var tags = element[0].getElementsByTagName('column');
console.log("pre", tags);
},
post: function(scope, element, attributes, controller, transcludeFn){
var tags = element[0].getElementsByTagName('column');
console.log("post", tags);
}
}
},
controller: function($scope, $element, $transclude){
$transclude(function(clone,scope){
/* for demo am converting to html string*/
console.log("From controller",angular.element(clone).find('column'));
});
}
};
});
I tested some other things. But i'm only able to get the column with this and only into the controller.

Related

Angular directive template unknown scope

I know there is a lot of questions and posts about AngularJS and how directives are supposed to be used. And I got mine working just fine until I got another problem which I don't know how to resolve.
I use a directive on a custom HTML element. Directive transforms this element into a regular html tree as defined in a template. The HTML element has some attributes which are used when building the template. Data for one of the elements is received with HTTP request and is successfully loaded. This is the part which I got working fine.
Now I want to do something more. I've created a plunker which is an example of what I want to achieve. It's a fake one, but illustrates my problem well.
index.html:
<body ng-controller="MainCtrl">
<div id="phones">
<phone brand="SmartBrand" model="xx" comment="blah"></phone>
<phone brand="SmarterBrand" model="abc" comment="other {{dynamic.c1}}"></phone>
</div>
</body>
Angular directive:
app.directive('phone', function() {
return {
restrict: 'E',
replace: true,
scope: {
'comment': '#',
'brand': '#'
},
templateUrl: 'customTpl.html',
controller: function($scope) {
fakeResponse = {
"data": {
"success": true,
"data": "X300",
"dynamic": {
"c1": "12",
"c2": "1"
}
}
}
$scope.model = fakeResponse.data.data;
$scope.dynamic = fakeResponse.data.dynamic;
}
}
});
Template:
<div class="phone">
<header>
<h2>{{brand}} <strong>{{model}}</strong></h2>
</header>
<p>Comment: <strong>{{comment}}</strong></p>
</div>
So I would like to be able to customize one of the tags in the element (phone comment in this example). The trick is that the number of additional info that is going to be in the tag may vary. The only thing I can be sure of is that the names will match the ones received from AJAX request. I can make the entire comment be received with AJAX and that will solve my problem. But I want to separate template from the variables it is built with. Is it possible?
Ok, I got it working. It may not be the state of the art solution (I think #xelilof suggestion to do it with another directive may be more correct), but I'm out of ideas on how to do it (so feel free to help me out).
I've turned the {{comment}} part into a microtemplate which is analysed by a service. I've made a plunk to show you a working sample.
The JS part looks like this now:
app.directive('phone', ['dynamic', function(dynamic) {
return {
restrict: 'E',
replace: true,
scope: {
'comment': '#',
'brand': '#',
'color': '#',
'photo': '#'
},
templateUrl: 'customTpl.html',
controller: function($scope) {
fakeResponse = {
"data": {
"success": true,
"data": "X300",
"dynamic": {
"c1": "12",
"c2": "2"
}
}
}
$scope.model = fakeResponse.data.data;
$scope.comment2 = dynamic($scope.comment, fakeResponse.data.dynamic);
console.log("Comment after 'dynamic' service is: " + $scope.comment);
}
}
}]);
app.factory('dynamic', function() {
return function(template, vars) {
for (var v in vars) {
console.log("Parsing variable " + v + " which value is " + vars[v]);
template = template.replace("::" + v + "::", vars[v]);
}
return template;
}
});

I want to write a custom directive for ui-grid with different input columnDefs

This is my Controller
$scope.usersList = {};
$scope.usersList = {
paginationPageSizes: [10,15, 20],
paginationPageSize: 10,
columnDefs: [
{ name: 'userId', cellTemplate: '<div class="ui-grid-cell-contents"><a ui-sref="appSetting.userSelected({userId: row.entity.userId})">{{ row.entity.userId }}</a></div>' },
{ name: 'firstName' },
{ name: 'lastName' },
{ name: 'emailId' },
{
name: 'action',
cellTemplate: '<div>' +
' <button ng-click="grid.appScope.sampledetails()">Delete</button>' +
'</div>',
enableSorting: false,
enableColumnMenu: false
}
]
};
and this is my .cshtml
<div id="grid1" ui-grid="gridOptions" class="grid"></div>
I want to write this in such a way that it should be used in other .cshmtls, but the columnDefs varies depending on table column name. How should I write in such a way that ths user should give the columnsDefs in directive along with pagination?
Your question is hard to understand, hopefully I got it right. You want to define default-settings for your grid but enable the user to input some special settings if needed?
Warp ui-grid in your own directive. Pass your wanted arguments into that directive and create default settings in your directive.
Your .cshtml. You pass your settings variable into that.
<my-grid options="usersList" />
Your Directive. Grab the settings there (see options: '=') and bind that to controller or scope.
angular.module('app').directive('myGrid', myGrid);
function myGrid() {
return {
templateUrl : 'grid.html',
controller : 'GridCtrl',
controllerAs : 'grid',
restrict: 'E',
scope: {
options : '=',
},
bindToController: true,
};
}
Your Controller. Now you can access your settings in that controller.
There you could combine the default settings with your inserted settings and pass that into the directive template.
angular.module('app').controller('GridCtrl', GridCtrl);
function GridCtrl() {
var grid = this;
console.log(grid.options); // containts your settings
grid.gridOptions = {
paginationPageSize: grid.options.paginationPageSize,
...,
columnDefs: grid.options.columnDefs
etc
}
}
And your grid.html, you pass the combined settings into the grid-API.
<div id="grid1" ui-grid="grid.gridOptions" class="grid"></div>
There are many more details to watch out for, but thats a possible setup.
e: I made a Plunkr for another similar question. For future reference.

Kendo grid editable template from directive

I am trying to create a kendo grid (angularjs) and attached a personalized editor <div my-directive-editor></div> via grid options editable.template. On my directive editor (angularjs directive), i specify the structure of HTML from remote file and link it via templateUrl. Upon running the application, everything works great when i first click the Add New Entry but when i cancel the popup dialog and click again the Add New Entry an error will show $digest already in progress in angular format.
I tried instead using templateUrl I used template and formatting the whole HTML structure as string and passed it there and it goes well without the error but as i can see, it is hard for the next developer to manage the very long HTML string so it would be great if i can separate it to remote file and just link it to templateUrl. I prepared a dojo to play with CLICK HERE the content of TestTemplate.html is the HTML string from template.
This is my directive
app.directive('grdEditor',
[
function () {
return {
restrict: 'A',
replace: true,
scope: {
dataItem: '=ngModel'
},
//template: '<div><table><tr><td>Name</td><td><input ng-model="dataItem.Name" class="k-input k-textbox" /></td></tr><tr><td>Birthdate</td><td><input kendo-date-picker k-ng-model="dataItem.Birthdate" /></td></tr><tr><td>Gender</td><td><input kendo-combo-box k-ng-model="dataItem.Gender" k-options="optGender" /></td></tr></table></div>',
templateUrl: 'http://localhost/Angular/TestTemplate.html',
/*template: function(){
return '<div><table><tr><td>Name</td><td><input ng-model="dataItem.Name" class="k-input k-textbox" /></td></tr><tr><td>Birthdate</td><td><input kendo-date-picker k-ng-model="dataItem.Birthdate" /></td></tr><tr><td>Gender</td><td><input kendo-combo-box k-ng-model="dataItem.Gender" k-options="optGender" /></td></tr></table></div>';
},*/
controller: function ($scope, $attrs, $timeout) {
$scope.optGender = {
dataTextField: 'Text',
dataValueField: 'Value',
dataSource:
{
data: [
{
Text: 'Male',
Value: 1
},
{
Text: 'Female',
Value: 2
}]
}
};
}
};
}
]);
and this is my kendo grid options (partial)
$scope.optGrid = {
editable: {
mode: "popup",
window: {
minHeight: '320px',
minWidth: '365px',
},
template: '<div grd-editor ng-model="dataItem"></div>',
},
toolbar: ['create', 'excel'],
excel: {
allPages: true
},
.....................
Any help would be appreciated.
TIA
i think a there is problem with templateUrl. you don't need to give http://
you just need to give path from your base directory or directory of your index.html

How to get jquery.Datatable's data to respect angular 2-way binding

I have a json object that may be modified by local conditions (not async ajax calls). I want this json object to be output on a datatables grid.
I have the table displaying the data just fine, however if the data changes, the grid does not. Do I have to call the constructor datatables every time? I would prefer to just use 2-way binding, but I am not sure. I have gotten it to update using a $watch, but I'd like to avoid using a $watch on this large object if I can. It could have 1000's of rows.
Here is a fiddle showing the datatable load, and the data it runs on changing after 3000 millis (simple $timeout). Is there a way to get the table to detect the change in the json object and refresh itself? Or do I need to do something different in the Angular code?
I am using a directive to load the table:
.directive('myTable', function() {
return {
restrict: 'E',
scope: {
data: '=',
options: '=',
columns: '='
},
template:
'<table id="balancesTable"></table>',
replace: true,
link: function(scope, elem, attrs) {
scope.options["aaData"] = scope.data;
scope.options["aoColumnDefs"] = scope.columns;
elem.dataTable(scope.options);
}
};
})
My Controller (edited for brevity... see fiddle for full code):
var myApp = angular.module('myApp', [])
.controller('MyAppCtrl', function ($scope, $timeout) {
$("#data-preview").css("color","green");
$scope.dataObj = [ { "balanceType": "Available Funds", ... } ];
$timeout(function(){
$scope.dataObj = [ { "balanceType": "Available Funds", ... different data ...} ];
$("#data-preview").css("color","red");
},
3000);
// columns settings for data table
$scope.columnDefs = [
{
"mData": "balanceType",
"sTitle": "Balance Type",
"aTargets":[0],
"sWidth": "200px"
},
{
...
}
];
// data table settings for table to not show search, pagination and allow column resize
$scope.overrideOptions = {
"bSort": true,
"bPaginate": false,
"sDom": "Rlfrtip",
"bFilter": false,
"bAutoWidth": false,
"bInfo": false
};
})
HTML:
<pre id="data-preview">{{dataObj}} | json}}</pre>
<my-table options="overrideOptions" data="dataObj" columns="columnDefs"><my-table>
Thanks in Advance!!

Nesting element directives can't access parent directive scope

I've been struggling with this for quite some time and I can't figure out how to resolve this issue.
I'm trying to make a grid directive which contains column directives to describe the grid but the columns won't be elements and would just add the columns to the array that is declared on the scope of the grid directive.
I think the best way to explain this issue is to view view the code:
var myApp = angular.module('myApp', [])
.controller('myCtrl', function ($scope, $http) {
})
.directive('mygrid', function () {
return {
restrict: "E",
scope: true,
compile: function ($scope) {
debugger;
$scope.Data = {};
$scope.Data.currentPage = 1;
$scope.Data.rowsPerPage = 10;
$scope.Data.startPage = 1;
$scope.Data.endPage = 5;
$scope.Data.totalRecords = 0;
$scope.Data.tableData = {};
$scope.Data.columns = [];
},
replace: true,
templateUrl: 'mygrid.html',
transclude: true
};
})
.directive('column', function () {
return {
restrict: "E",
scope: true,
controller: function ($scope) {
debugger;
$scope.Data.columns.push({
name: attrs.name
});
}
};
});
And here is the HTML markup:
<body ng-app="myApp">
<div ng-controller="myCtrl">
<input type="text" ng-model="filterGrid" />
<mygrid>
<column name="id">ID</column>
<column name="name">Name</column>
<column name="type">Type</column>
<column name="created">Created</column>
<column name="updated">Updated</column>
</mygrid>
</div>
In addition, you can test the actual code in jsfiddle: http://jsfiddle.net/BarrCode/aNU5h/
I tried using compile, controller and link but for some reason the columns of the parent grid are undefined.
How can I fix that?
Edit:
When I remove the replace, templateUrl, transclude from the mygrid directive, I can get the scope from the column directive.
Thanks
In the later versions of AngularJS I found that $scope.$$childHead does what I wanted.
It's still new but it works very well also with directive with isolated scopes.
So in the Columns directive you can just do:
$scope.$$childHead.Data.columns.push({
name: attrs.name
});
Just make sure that this command is executed after the compile of the grid. You can do that but switching between compile, link and controller since each one of them has a different loading priority.
I see what you're trying to do, but using a column directive is probably not the best way to tackle problem.
You're trying to define a grid directive with customizable columns. The columns each have 2 relevant pieces of information: the key with which to access the value in the row data, and the title to display.
Ignoring for the moment all the pagination-related stuff, here's a different way to approach the problem.
First, let's use attributes to define the column information, so our HTML would look like:
<body ng-app='app' ng-controller='Main'>
<grid col-keys='id,name,type'
col-titles='ID,Name,Type'
rows='rows'>
</grid>
</body>
For the JS, we obviously need the app module:
var app = angular.module('app', []);
And here's the grid directive. It uses an isolate scope, but uses the = 2-way binding to get the row data from its parent scope. Notice how the link function pulls the column info from the attrs object.
The template becomes very simple: loop over the column titles to define the headings, then loop over rows, and in each row, loop over the column keys.
app.directive('grid', function() {
return {
restrict: 'E',
scope: {
rows: '='
},
link: function(scope, element, attrs) {
scope.colKeys = attrs.colKeys.split(',');
scope.colTitles = attrs.colTitles.split(',');
},
replace: true,
template:
'<table>' +
' <thead>' +
' <tr>' +
' <th ng-repeat="title in colTitles">{{title}}</th>' +
' </tr>' +
' </thead>' +
' <tbody>' +
' <tr ng-repeat="row in rows">' +
' <td ng-repeat="key in colKeys">{{row[key]}}</td>' +
' </tr>' +
' </tbody>' +
'</table>'
};
});
And some sample data to get started.
app.controller('Main', function($scope) {
$scope.rows = [
{id: 1, name: 'First', type: 'adjective'},
{id: 2, name: 'Secondly', type: 'adverb'},
{id: 3, name: 'Three', type: 'noun'}
];
});
Here it is in fiddle form.
As Imri Commented:
In the later versions of AngularJS you can get your parent directive by using $scope.$$childHead
I have not tested it.

Resources