AngularJS - huge directive - angularjs

I have a website that has to display different set of data on a map. The map will always be the same - Some areas with 'onhover' effect and tooltip.
There is about 10 different sets of data.
I created a directive to display the map
Directive (only draw the map)
angular.module('PluvioradApp.controllers')
.directive('map', function() {
return {
restrict: 'E',
link: function(scope, element, attrs) {
var svg = d3.select(element[0])
.append("div")
.classed("svg-container", true) //container class to make it responsive
.append("svg")
//responsive SVG needs these 2 attributes and no width and height attr
.attr("preserveAspectRatio", "xMinYMin meet")
.attr("viewBox", "0 0 1000 500")
//class to make it responsive
.classed("svg-content-responsive", true);
var g = svg.append("g");
//Scale / translate / ...
var lineFunction = d3.svg.line()
.x(function(d) { return (d[0]+50000)/500; })
.y(function(d) { return (-d[1]+170000)/500; })
.interpolate("linear");
//Draw map
var path = g.selectAll('path')
.data(data.areas)
.enter().append("path")
.attr("d", function(d){ return lineFunction(d.borders)})
.attr("fill", "#D5708B")
.on('mouseover', function(d) {
d3.select(this).style('fill', 'orange');
d3.select(this).text(function(d){return "yeah";})
})
.on('mouseout', function(d) {
g.selectAll('path').style('fill', '#D5708B');
});
// zoom and pan
var zoom = d3.behavior.zoom()
.on("zoom", function() {
g.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
g.selectAll("path")
.attr("d", function(d){ return lineFunction(d.borders)});
});
svg.call(zoom);
}
}
});
My idea was to get the data to display from the controller (it comes from an API) and send it to the directive. Inside the above directive I will add a big switch or multiple if to display the correct data with the correct colors, size,...
I am sure that there is another way to split the work.
For example :
1st directive to display the map. It can be reuse multiple time
2nd directive to display set 1
3rd directive to display set 2
If this is the correct way, how can I achieve this ?
Additional information
I have a menu with a dropdown to select which data will be displayed. For the moment, the items redirect to a page containing the map directive above

Have a folder where you will have a bunch of service, where each service will be one of your data set.
Set1Service, Set2Service. etc.
Each of this will have own logic.
Have a factory service which will return one of your service.
for example:
(new FactoryService())->get('dataSetItem'); //this will return one of services from point 1.
Inject FactoryService in you directive use it.
In factory you will have the logic how to parse your data set, to determine what DataSetService you will have to return
This is extensible an easy to use.
All that I described are more related to Strategy and Factory pattern, you can read more about those and this will help you to have more abstract implementation.
angular.module('PluvioradApp.controllers')
.directive('map', function(factoryService) {
return {
restrict: 'E',
scope: {
dataSet: '='
}
link: function(scope, element, attrs) {
//all your code
var dataSetService = factoryService.get(scope.dataset);
var result = dataSetService.seed(d3);
}
}).service('factoryService', function() {
this.get = function(name) {
var service = null;
switch (name) {
case 'set1'
service = new DataSet1();
break;
case 'set2'
service = new DataSet2();
break;
}
return service;
}
});
function DataSet1() {
}
DataSet1.prototype.seed = function(d3) {
//d3 logic you have access here;
}
function DataSet2() {
}
DataSet2.prototype.seed = function(d3) {
//d3 logic you have access here;
}

Related

Angular eventListener from Directive

I have directive, essentially a button, that should only show on certain conditions. It uses addEventListener('scroll') to capture the scroll of a <ion-content> element in the containing page. This works fine for one page/view but when navigating to another page/view the scroll event is not fired?
'use strict';
angular.module('Fmpclient')
.directive('fmpRefreshButton', ['$window', '$ionicScrollDelegate', 'RefreshButtonService', function ($window, $ionicScrollDelegate, RefreshButtonService) {
var hideDelayTime = 3500;
var isButtonVisible = false;
var lastYPos = 0; // store the last Y Position
var timerId = null; // store a setTimeout id for later use
var $scrollEl = null;
var refreshButton = null;
/* __ code snipped for brevity __ */
/**
Work out the direction of scroll so we can
either hide or show the refresh button according to AC
#method onScroll
#public
*/
function onScroll () {
var scrollTop = $ionicScrollDelegate.getScrollPosition().top;
if(scrollTop > lastYPos) {
// Scrolling DOWN - hide refresh button
hideRefreshButton();
} else {
// Scrolling UP - show refresh button
shouldRefreshFeed();
}
// Store the last Y position to determine
// direction of scroll on next iteration
lastYPos = scrollTop;
}
/**
Setup directive elements on init
#method init
#public
*/
function init (){
console.log('directive::init()');
RefreshButtonService.setRefreshFeed(false);
$scrollEl = document.getElementById('ionContentScroll');
if($scrollEl){
$scrollEl.addEventListener('scroll', onScroll);
}
}
function _link(scope, element) {
refreshButton = element[0];
init();
}
return {
templateUrl: 'app/app/refresh-button/refresh-button.directive.html',
restrict: 'AE',
scope: true,
link: _link,
};
}
]);
As suggested by Scott, in the comment above, the solution to my rather trivial problem was to pass in the id as a parameter to the directive to ensure the id can be unique for each usage:
In the Directive scope:
scope: {
scrollElement: '='
},
and passed in on the Directive template:
<my-directive scroll-element="ionFeedScroll">

Implement the general update pattern for an svg path

I have a custom chart that is loaded with some initial values on page load. When an event happens, I update the chart with some indicative arrows that show some trend. The d3 part resides inside an angular directive that gets its updated data when there is a change to the dataset from various parts of the page. What I have currently seems to work, but few path elements are not showing up. I wonder if I messed up something in the general update pattern
A snippet of the part where I have the general update pattern
ngApp.directive("customChart", function ($window) {
return {
restrict : "A",
template : "<svg width='320' height='200'></svg>",
scope : {
data : "=chartData"
},
link : function (scope, elem, attrs) {
scope.line = {
/*line function*/
};
scope.setChartDimensions = function () {
// . . .
}
scope.render = function (dataset) {
aData = dataset.arrows ? dataset.arrows : undefined;
// . . .
if (arrowData) {
// . . .
var path = svg
.selectAll("path")
.data(pathCoordinates, function (d) {
return d;
});
path.enter()
.append("path")
.attr("d", function (d) {
return scope.line(d) + "Z";
})
.attr("class", "enter");
path.exit().remove();
}
}
scope.$watchCollection('data', function (newValue, oldValue) {
scope.render(newValue);
});
scope.setChartDimensions();
}
}
}
When an event happens some of the arrow data is not being recognized as new or something and those arrows are not being rendered despite that data being available at the time.
Fiddle for the entire code: https://jsfiddle.net/animeshb/tbpnx0xd

angularjs - scope changes but function is not fired

I'm working on an angular app which needs to display some data as a chart.
The library i'm using in nvd3.js.
The chart is a directive, and the code that creates the chart is the following:
angular.module('charts', []).factory('d3', [
function() {
return d3;
}
]).factory('nv', [
function() {
return nv;
}
]).directive('hgCharts', [
'$timeout', 'd3', 'nv', function($timeout, d3, nv) {
var linkFunction;
linkFunction = function(scope, element, attr) {
var chart, chartCreated, containerHeight, containerWidth, createChart, date, svg, xScale;
containerWidth = element[0].offsetWidth;
containerHeight = element[0].offsetHeight;
date = new Date;
svg = element.find('svg');
chart = void 0;
xScale = d3.time.scale().domain([date, date]).nice(d3.time.day).range([0, containerWidth - 50]);
createChart = function() {
debugger;
chart = nv.models.multiChart();
chart.options({
showLegend: true,
duration: 0,
margin: {
'left': 20,
'right': 30,
'top': 50,
'bottom': 50
},
yDomain1: [0, 30],
showXAxis: false,
showMinMax: false,
useVoronoi: true
});
chart.xAxis.scale(xScale).orient('bottom').ticks(3).duration(0).tickFormat(d3.time.format('%H:%M'));
chart.yAxis1.orient('left').ticks(7).duration(0);
chart.tooltip.enabled(true);
nv.utils.windowResize(function() {
containerWidth = element[0].offsetWidth;
containerHeight = element[0].offsetHeight;
chart.xAxis.range([0, containerWidth - 50]);
chart.update();
return d3.selectAll(".nv-axisMaxMin-x")[0][0].remove();
});
return chart;
};
chartCreated = function() {
console.log("Scope.data is: ", scope.data);
d3.select(svg[0]).attr('width', '100%').attr('height', '100%').datum(scope.data).call(chart);
return d3.selectAll(".nv-axisMaxMin-x")[0][0].remove();
};
scope.$watch(function() {
return scope.data;
}, function() {
if (scope.data) {
console.log("Scope data changed...");
return nv.addGraph(createChart, chartCreated);
}
});
};
return {
restrict: 'E',
scope: {
data: '='
},
template: '<svg></svg>',
link: linkFunction
};
}
]);
In my UI, I have a prev and next button.
Each of the buttons above have a function in the controller to get the right start and end time period using moment.js, and once they have the right times, the call a function.
This it the code for the funciton they call:
var loadChartData;
loadChartData = function(start, end) {
if (!start) {
start = 1467331200000;
}
if (!end) {
end = 1467417599000;
}
return hgChartData.loadData(start, end).then(function(res) {
return $scope.chartData = res;
});
};
The function calls a service to load my data, and returns them as a promise (res), and then creates (or modify) $scope.chartData with the new data.
Debugging this process a lot, I noticed that the problem I've got, is that even if $scope.chartData changes (I have the right result printed on my console), the debugger inside createChart function is never called, and so the chart with my new data is not created.
Am I doing something wrong with the watcher?
Strangely, it works only the first time I click on a button (it doesn't matter if it prev or next), but then it stops (as I said above, scope.chartData changes everytime according to the console messages I print.) The code for next and prev buttons are below:
var requiredDay;
requiredDay = '';
$scope.prevDay = function() {
if (!requiredDay) {
requiredDay = moment(1467377134000);
}
requiredDay = requiredDay.clone().subtract(1, 'day');
console.log("Required data period is: ", requiredDay);
displayRequestedData(requiredDay);
return requiredDay;
};
$scope.nextDay = function() {
if (!requiredDay) {
requiredDay = moment(1467377134000);
}
requiredDay = requiredDay.clone().add(1, 'day');
console.log("Required data period is: ", requiredDay);
displayRequestedData(requiredDay);
return requiredDay;
};
I think the problem is with my watcher, but I can't figure out what i'm doing wrong.
Thanks for any help
EDIT
After debugging this a bit more, I found out that the scope is actually watched, but as soon as I changes to empty array, it's not watched anymore.
I'll explain it a bit better:
scope.data (the scope i'm watching in my directive) is equal to an array of object, and each object has this structure (taken directly from console):
Object{
color:"red"
duration:0
key:"Temperature"
originalKey:"Temperature"
type:"line"
values:Array[10]
yAxis:1
}
Each array inside the previous object look like this:
Object{
series:0
x:1467330307000
y:20
}
what I found is that if my array changes its values (array values are the only thing that changes inside that scope), the scope is updated and the chart re-rendered, but as soon as I have no data for the requested time period, the number of arrays inside my object goes to 0 and then the watcher doesn't work anymore.
I hope this helps a bit more. I'm still having issues try to understand why and how to solve it.
Thanks again

ngMessages with custom validator in parent controller

I'm trying to figure out how to wire up a custom validator with ngMessages that has access to the parent scope. I have an input field for an address and onBlur I want to do a geolocate of the address and update the position of a marker on a map (by setting two variables on the controllers this).
Custom validator examples use directives (and I have followed those for a basic example) but can't move from their to geolocation, as I'm struggling with Directive to parent Controller communication in the context of ES6, ControllerAs,...:
I've started off trying to access the parent controller from the link function with scope.$parent (How to access parent scope from within a custom directive *with own scope* in AngularJS?) but I'm using ES6 classes and it just did not seem to work.
now I'm thinking about passing in the parent function to the Directive, but the function is complaining that it cannot find elements of the controller's normal scope, so that means it cannot update the marker's position.
Grateful for advice on wiring this up.
Here's where I got to in the second instance
var checkAddressDir = function($timeout) {
return {
require : 'ngModel',
scope: {
geolookup: '&'
},
link : function(scope, element, attrs, ngModel) {
function setAsLoading(bool) {
ngModel.$setValidity('recordLoading', !bool);
}
ngModel.$parsers.push(function(value) {
if(!value || value.length == 0) return;
setAsLoading(true);
scope.geolookup()(value)
// scope.$parent.findAddress()
.then( res => {
// I'll use res to update ngModel.$setValidity
console.log(res);
setAsLoading(false);
});
// THE FOLLOWING SERVED TO GET ME STARTED
// $timeout(function() {
// console.log("timeout");
// setAsLoading(false);
// }, 1000);
return value;
})
}
}
}
And this is the controller function I need to be able to use with the controller's scope
findAddress(address) {
return this.$q( (resolve, reject) => {
mygeocoder.geocode(myOptions, (res, stat) => {
if (stat === google.maps.GeocoderStatus.OK) {
if (res.length > 1) {
console.log(`Need unique result, but got ${res.length}`);
return "notUnique";
}
var loc = res[0].geometry.location;
this.resto.lat = loc.lat();
this.resto.lng = loc.lng();
this.zoom = 16;
this.$scope.$apply();
return "good";
} else {
console.log(" not found - try again?");
return "notFound";
}
});
});

How to factor scope dependent plugins into a service for ng-grid

I am developing a project that uses ngGrid to enable the user to view and select data.
I am using a couple plugins I have pick up from the community.
a double click listener taken from ngGrid double click row to open pop-up for editing a row and a filter bar taken from http://plnkr.co/edit/c8mHmAXattallFRzXSaG?p=preview
var ngGridDoubleClick = function() {
var self = this;
self.$scope = null;
self.myGrid = null;
// The init method gets called during the ng-grid directive execution.
self.init = function(scope, grid, services) {
// The directive passes in the grid scope and the grid object which
// we will want to save for manipulation later.
self.$scope = scope;
self.myGrid = grid;
// In this example we want to assign grid events.
self.assignEvents();
};
self.assignEvents = function() {
// Here we set the double-click event handler to the header container.
self.myGrid.$viewport.on('dblclick', self.onDoubleClick);
};
// double-click function
self.onDoubleClick = function(event) {
self.myGrid.config.dblClickFn(self.$scope.selectedItems[0]);
};
}
var filterBarPlugin = {
init: function(scope, grid) {
filterBarPlugin.scope = scope;
filterBarPlugin.grid = grid;
$scope.$watch(function() {
var searchQuery = "";
angular.forEach(filterBarPlugin.scope.columns, function(col) {
if (col.visible && col.filterText) {
var filterText = (col.filterText.indexOf('*') == 0 ? col.filterText.replace('*', '') : "^" + col.filterText) + ";";
searchQuery += col.displayName + ": " + filterText;
}
});
return searchQuery;
}, function(searchQuery) {
filterBarPlugin.scope.$parent.filterText = searchQuery;
filterBarPlugin.grid.searchProvider.evalFilter();
});
},
scope: undefined,
grid: undefined
};
I am loading them like this:
$scope.gridOptions = {
data : 'parts.partlist',
columnDefs : 'parts_fields',
dblClickFn : $scope.addToConstruct,
multiSelect : false,
showGroupPanel : true,
jqueryUIDraggable: true,
plugins: [ filterBarPlugin, ngGridDoubleClick ],
headerRowHeight : 60,
sortInfo : SortOpts,
selectedItems : $scope.selection
}
So the problem is is that these plugin definitions exist in a controller and I would like to pull them out to a service so that I can use them anywhere without copy/pasting ~50 lines of code.
I have tried to put them in a factory but they need access to the $scope object and I was unsuccessful in injecting $scope into my factory.
Any ideas on how to best turn these plugins into reusable components?
You don't need to inject $scope into your factory, the init() function of the plugin does that for you in this line of each plugin (ref ngGrid Wiki):
self.init = function(scope, grid) {
All you have to do is stick them in a factory, and return each function from the factory. I reconfigured filterBarPlugin into a function instead of a JSON object in this case... See sample code here: http://plnkr.co/edit/Uw28KUbIOtDps2GaJahb?p=preview

Resources