I have the following angular app to create a menu of sections/products.
at present when rendered and hitting the 'add' button that is rendered within each li I want to add a section/product as a sub of that section however multiple new children are created.
ultimately I wish to display a form which when submitted will create the child but that is the next step. Right now I need to limit the scope to the current section and not have multiple bound clicks.
If you need more information please state and I will post in an edit.
Some sample data data.
{
"sections":[
{
"name":"Flags",
"sections":[
{
"name":"Europe",
"sections":[],
"products":[
{ "name": "France" },
{ "name": "Germany" },
{ "name": "Ireland" },
{ "name": "England" }
]
},
{
"name": "Africa",
"sections":[],
"products":[
{ "name": "Egypt" },
{ "name": "Nigeria" },
{ "name": "Chad" }
]
},
{
"name": "South America",
"sections":[],
"products": [
{ "name": "Brasil" },
{ "name": "Argentina" },
{ "name": "Peru" }
]
}
],
"products":[]
},
{
"name": "Maps",
"sections":[
{
"name": "Africa",
"sections":[],
"products":[
{ "name": "Egypt" },
{ "name": "Nigeria" },
{ "name": "Chad" }
]
},
{
"name": "South America",
"sections":[],
"products": [
{ "name": "Brasil" },
{ "name": "Argentina" },
{ "name": "Peru" }
]
}
],
"products":[]
}
],
"products":[]
}
The app.
'use strict';
var menuApp = angular.module('menuApp', []);
menuApp
.directive('sections', function () {
return {
restrict: "E",
replace: true,
scope: {
sections: '='
},
template: '<ul><section ng-repeat="section in sections" section="section" /></ul>'
};
})
.directive('section', function ($compile) {
return {
restrict: "E",
replace: true,
scope: {
section: '=section'
},
template: '<li class="section">{{section.name}} <button ng-click="addSub(section)">Add</button></li>',
link: function (scope, element, attrs, controller) {
if (angular.isArray(scope.section.sections)) {
element.append("<sections sections='section.sections'></sections>");
$compile(element.contents())(scope);
}
if(angular.isArray(scope.section.products)){
element.append("<products products='section.products'></products>");
$compile(element.contents())(scope);
};
},
controller : function($scope){
console.log($scope);
$scope.addSub = function (section){
//console.log(section,'Adding Sub');
section.sections.push({"name":"Section","sections":[],"products":[]});
};
}
};
})
.directive('products', function () {
return {
restrict: "E",
replace: true,
scope: {
products: '='
},
template: '<ul><product ng-repeat="product in products" product="product"></product></ul>'
};
})
.directive('product', function ($compile) {
return {
restrict: "E",
replace: true,
scope: {
product: '='
},
template: '<li class="product">{{product.name}}</li>'
};
});
menuApp.controller('menuCtrl', function menuCtrl($scope,$http) {
$http.get('/ajax/getvenuesmenu?venueID='+venueMenu.venueId).success(function(resp) {
$scope.sections = resp;
});
$scope.add = function(data){
data.push({"name":"Section","sections":[]});
};
});
Took me a bit to figure it out but here's the basic problem, you are compiling the full contents of section 2 extra times and each compile seems to add a new event handler.
Instead of compiling the contents of element each time you make an append of new template, compile the template itself (outside of the DOM) and then append the compiled template. This way the ng-click handler doesn't get compiled again other than initial scope creation
Here's an abbreviated version with one template appended:
link: function (scope, element, attrs, controller) {
if (angular.isArray(scope.section.sections)) {
/* compile outside of the DOM*/
var subsections = $compile("<sections sections='section.sections'></sections>")(scope);
/* append compilation*/
element.append(subsections);
}
DEMO
Another approach would be to create a complete template string in link by checking for subsections and products, then compiling everything all at once....instead of using template option
Code for alternate approach compiling complete section at once:
.directive('section', function ($compile, $timeout) {
return {
restrict: "E",
scope: {
section: '=section'
},
link: function (scope, element, attrs, controller) {
var template = '<li class="section">{{section.name}} <button ng-click="addSub(section)">Add</button>';
if (angular.isArray(scope.section.sections)) {
template += "<sections sections='section.sections'></sections>";
}
if (angular.isArray(scope.section.products)) {
template += "<products products='section.products'></products>";
};
template += '</li>';
var compiledTemplate = $compile(template)(scope);
element.replaceWith(compiledTemplate);
scope.addSub = function (section) {
section.sections.push({ "name": "Section", "sections": [], "products": []
});
};
}
};
})
DEMO-Alt
Related
I am using amcharts as a custom directive. Now I want to fetch the dataProvider of this AmChart from the output of the $http.get service using webservices.
But I am unable to get this dynamically assigned to the dataprovider of the amChart.
var app=angular.module('mainApp',[]);
app.service('dataProviderService',function($http){
this.getData= function(){
return $http.get('jaxrs/WebService/getJSONData');
};
});
app.directive('myChart',['dataProviderService',function (dataProviderService) {
return {
restrict: 'E',
replace:true,
template: '<div id="chartdiv" style="min-width: 310px; height: 400px; margin: 0 auto"></div>',
link: function (scope, element, attrs) {
scope.data={};
dataProviderService.getData().then(function(response){
scope.data=response.data;
},function(error){
scope.status="Show error";
});
var chart = false;
var initChart = function() {
if (chart) chart.destroy();
var config = scope.config || {};
chart = AmCharts.makeChart( "chartdiv", {
"theme": "none",
"type": "serial",
"dataProvider": dataProviderService.newData(),
"categoryField": "OT",
"depth3D": 20,
"angle": 30,
"categoryAxis": {
"labelRotation": 90,
"gridPosition": "start"
},
"valueAxes": [ {
"title": "availability"
} ],
"graphs": [ {
"valueField": "availability",
"colorField": "color",
"type": "column",
"lineAlpha": 0.1,
"fillAlphas": 1
} ],
"chartCursor": {
"cursorAlpha": 0,
"zoomable": false,
"categoryBalloonEnabled": false
},
"export": {
"enabled": true
}
} );
};
initChart();
}//end watch
}
}]) ;
Assuming your newData() method is an asynchronous $http.get() request like your getData() method, you need to either call makeChart after the request resolves itself or tell the chart to redraw itself by calling validateData() when the request resolves itself. Here's an example of doing this inside the chart's init event:
chart = AmCharts.makeChart( "chartdiv", {
// ... omitted ...
"dataProvider": [],
// ... omitted ...
"listeners": [{
"event": "init",
"method": function(e) {
dataProviderService.newData().then(function(response) {
e.chart.dataProvider = response;
e.chart.validateData();
}, function(error) {
// do something else
});
}
}]
} );
I am new to Angular and just getting used to how everything fits together. I want to display a chart through a directive. This chart will be static data so will neither be dynamically loaded nor updated, it is taken from a JSON.
I have referenced here but the main difference is that I am not loading Highcharts in the view. I am loading it globally in the .js file from my bower_components directory. This is my code:
.js
/* global Highcharts */
angular.module("fusoDataLoggerChart", [])
.controller("fusoDataLoggerChartController", ["$http", "$scope", function($http, $scope) {
"use strict";
$scope.data = {
"activities": [
{},
{},
{
"title": "Engine speed",
"type": "DATA_LOGGER",
"result": {
"Engine speed": {
"data": [
{
"timestamp": 0,
"value": {
"type": "QUANTITY",
"unit": "rpm",
"value": 900
}
},
{
"timestamp": 1000,
"value": {
"type": "QUANTITY",
"unit": "rpm",
"value": 1000
}
},
{
"timestamp": 2000,
"value": {
"type": "QUANTITY",
"unit": "rpm",
"value": 2000
}
}
]
},
"Accelerator pedal position": {
"data": [
{
"timestamp": 0,
"value": {
"type": "QUANTITY",
"unit": "%",
"value": 0
}
},
{
"timestamp": 1000,
"value": {
"type": "QUANTITY",
"unit": "%",
"value": 10.6
}
},
{
"timestamp": 2000,
"value": {
"type": "QUANTITY",
"unit": "%",
"value": 11
}
}
]
}
}
}
]
}
}])
.directive("fusoDataLoggerChart", function() {
"use strict";
return {
scope: {},
restrict: 'E',
link: function(scope) {
scope.dataLoggerData = scope.data["activities"][2]["result"];
scope.timestamps = getData()[0];
scope.engineSpeeds = getData()[1];
scope.pedalPositions = getData()[2];
var chart = new Highcharts.Chart({
chart: {
type: 'line',
animation: false,
renderTo: "DataLoggerChartContainer",
zoomType: 'x'
},
credits: {
enabled: false
},
title: {
text: null
},
xAxis: {
type: "linear",
title: {
text: "Timestamp"
},
min: 0,
categories: scope.timestamps
},
yAxis: [{ //Engine Speed
title: {
text: 'Engine Speed'
},
labels: {
format: '{value} RPM'
},
opposite: true
}, { //Accelerator Pedal Position
title: {
text: 'Accelerator Pedal Position'
},
labels: {
format: '{value} %'
},
min: 0,
max: 100,
// FIXME: 'allowDecimal' may not work
allowDecimal: true
}],
series: [{
name: 'Engine Speed',
type: 'spline',
yAxis: 1,
tooltip: {
valueSuffix: ' RPM'
},
data: scope.engineSpeeds
}, {
name: 'Accelerator Pedal Position',
type: 'spline',
yAxis: 2,
tooltip: {
valueSuffix: ' %'
},
data: scope.pedalPositions
}]
});
function getData() {
var timestamps = [],
engineSpeeds = [],
pedalPositions = [];
var engineSpeedData = scope.dataLoggerData["Engine Speed"]["data"],
pedalPosData = scope.dataLoggerData["Accelerator Pedal Position"]["data"];
for (var i in engineSpeedData) {
timestamps.push(engineSpeedData[i].timestamp);
}
for (var j in engineSpeedData) {
engineSpeeds.push(engineSpeedData[j].value.value);
}
for (var k in pedalPosData) {
pedalPositions.push(pedalPosData[k].value.value);
}
return [timestamps, engineSpeeds, pedalPositions];
}
}
}
});
HTML
<fuso-data-logger-chart>
<div id="DataLoggerChartContainer"></div>
</fuso-data-logger-chart>
When I go to where the chart is to be rendered, nothing is shown (and only the HTML code set manually is displayed with nothing inside when I check in DevTools).
N.B: I realize I do not need the controller yet but it is there as in future the JSON will be got from a rest call from within the controller
Any help appreciated.
This isn't a full answer to your question, but I noticed a way you can make your HTML directive a little cleaner so that you could instead use this format:
<fuso-data-logger-chart></fuso-data-logger-chart>
You don't need the <div id="DataLoggerChartContainer">. You can embed that in the directive definition as a template:
.directive("fusoDataLoggerChart", function() {
"use strict";
return {
scope: {},
restrict: 'E',
transclude: true,
template: '<div id="DataLoggerChartContainer"></div>'
link: function(scope) {
... your other code ...
}
}
});
I have figured out the problem. There were multiple errors but I'll leave this here so maybe someone else can find it useful:
Highcharts and my FusoDataLoggerChart.js were not loaded into index.html (<script src="modules/readVehicleData/FusoDataLoggerChart.js"></script>) meaning that they couldn't be seen by Angular.
In my directive, I was trying to access the local scope (scope.data) in the link() function instead of trying to access the parent scope in the controller where the data is set. To access the parent scope, scope.$parent.data worked for me (N.B: was only 'data' for me as that's the value I set)
Here i created some Example.. In this Example 1 I put directly
//{{arr.Description}}
like this its working but via directive its not working what misstake i did
can please explain me?
Thanks friends
var app = angular.module('components', []);
app.directive('subpane', function() {
return {
restrict: 'E',
scope: {
array: '#'
},
template:'<li ng-repeat="arr in array">{{arr.Description}}</li>',
link: function(scope) {
}
};
})
app.controller('tabController', ['$scope', function ($scope) {
$scope.array =[{
"title": 0,
"Description": "Select your option"
},
{
"title": 1,
"Description": "Male"
},
{
"title": 2,
"Description": "Female"
},
{
"title": 3,
"Description": "Unknown"
}];
}])
<script data-require="angular.js#~1.3.15" data-semver="1.3.15" src="https://code.angularjs.org/1.3.15/angular.js"></script>
<body ng-app="components" ng-controller="tabController">
<span>Example 1 Working fine</span>
<li ng-repeat="arr in array">{{arr.Description}}</li>
<span>Example 2 Not Working.. Here also same thing only i did via directive.. why this is not working?</span>
<subpane array={{array}}></subpane>
</body>
Use '=' for the scope param and remove '{{}}' in the markup.
var app = angular.module('components', []);
app.directive('subpane', function() {
return {
restrict: 'E',
scope: {
array: '='
},
template:'<li ng-repeat="arr in array">{{arr.Description}}</li>',
link: function(scope) {
}
};
})
app.controller('tabController', ['$scope', function ($scope) {
$scope.array =[{
"title": 0,
"Description": "Select your option"
},
{
"title": 1,
"Description": "Male"
},
{
"title": 2,
"Description": "Female"
},
{
"title": 3,
"Description": "Unknown"
}];
}])
<script data-require="angular.js#~1.3.15" data-semver="1.3.15" src="https://code.angularjs.org/1.3.15/angular.js"></script>
<body ng-app="components" ng-controller="tabController">
<span>Example 1 Working fine</span>
<li ng-repeat="arr in array">{{arr.Description}}</li>
<span>Example 2 Not Working.. Here also same thing only i did via directive.. why this is not working?</span>
<subpane array=array></subpane>
</body>
You need to add "plain: true" in the directive if you don't use templateUrl
app.directive('subpane', function() {
return {
restrict: 'E',
scope: {
array: '=' // = not #, = is for object/array ; = is for text/number.
},
template:'<li ng-repeat="arr in array">{{arr.Description}}</li>',
plain: true,
link: function(scope) {
}
};
})
In the html dont put {{ }} in the parameter array
<subpane array="array"></subpane>
camden_kid has posted an acceptable answer but not probably the best one. In your template:
<subpane array={{array}}></subpane>
Must have the quotes:
<subpane array="{{array}}"></subpane>
Please also note that camden_kid suggested to change
scope: {
array: '#'
}
To
scope: {
array: '='
}
This is what is needed for double binding and it is only necessary if you have to edit in any way the original value (in the parent scope) from within the directive's isolated scope.
You probably should read more carefully the docs: https://docs.angularjs.org/guide/directive
I have a hierarchical menu system with nested categories that allows users to drill down the hierarchy to lower levels.
I have a click event that is used to drill down in to the menu when there are children.
When they reach the final level (known as Leaf), I want to fire a methods(data) on the page controller with information about the node they just selected.
Directive
angular.module(APP)
.directive('wkProductCategoryMenu', [function () {
return {
restrict: 'E',
require: "?ngModel",
scope: {
heading: '=',
onMenuLeafSelect: '&'
},
templateUrl: COMPONENTS_PATH + '/productCategoryMenu/productCategoryMenu.html',
controller: 'ProductCategoryMenuController',
link: function (scope, element, attrs, ngModel) {
if (!ngModel) {
return;
}
scope.parentCategories = [];
ngModel.$render = function () {
scope.focusCategories = ngModel.$modelValue;
};
}
};
}
])
;
Directive Controller
angular.module(APP)
.controller('ProductCategoryMenuController',
['$scope', '$log', function
($scope, $log)
{
$scope.selectMenu = function (menu) {
var name = menu.name;
$log.info('Select: ' + name);
for (var i = 0; i < $scope.focusCategories.length; i++) {
if (_.isEqual($scope.focusCategories[i].name, name)) {
if (menu.isLeaf) {
$log.warn('Trigger for menu: ' + name + " : " + menu.hierarchy);
if ($scope.onMenuLeafSelect) {
$scope.onMenuLeafSelect(menu);
}
} else {
$scope.parentCategories.push($scope.focusCategories);
$scope.focusCategories = $scope.focusCategories[i].categories;
}
}
}
}
$scope.back = function () {
$log.info('Back');
$scope.focusCategories = $scope.parentCategories.pop();
}
}]);
Use the Directive in Page
<div class="row">
<div class="col-lg-12">
<wk-product-category-menu
heading="model.filter.supplier.supplier"
ng-model="model.entity.categories"
on-menu-leaf-select="getProducts(productCategory)">
</wk-product-category-menu>
</div>
</div>
Page Events
// -------------------------------------------------------------------------------------------------------
// Page Events
// -------------------------------------------------------------------------------------------------------
$scope.getProducts = function (productCategory) {
$log.info("ProductCategory");
// THIS is UNDEFINED
$log.error(JSON.stringify(productCategory));
}
Sample Data
{
"_id": "561f6304512106cfa84920b0",
"active": true,
"supplier": "All",
"productCount": 6891,
"categoryCount": 13,
"categories": [
{
"isLeaf": false,
"isRoot": true,
"categories": [
{
"isLeaf": false,
"isRoot": false,
"categories": [
{
"isLeaf": true,
"isRoot": false,
"categories": [
],
"productCount": 15,
"name": "Premium"
},
{
"isLeaf": true,
"isRoot": false,
"categories": [
],
"productCount": 38,
"name": "Craft"
}
],
"productCount": 53,
"name": "Domestic Beer"
},
{
"isLeaf": false,
"isRoot": false,
"categories": [
{
"isLeaf": true,
"isRoot": false,
"categories": [
],
"productCount": 6,
"name": "Craft"
}
],
"productCount": 28,
"name": "International Beer"
},
{
"isLeaf": true,
"isRoot": false,
"categories": [
],
"productCount": 18,
"name": "Cider"
},
{
"isLeaf": true,
"isRoot": false,
"categories": [
],
"productCount": 4,
"name": "Bitters"
}
]
}
]
}
When you trigger the event pass the data with an object as below:
$scope.onMenuLeafSelect({productCategory: menu});
How can I pass a JSON data object to dataProvider in my directive instead of hard-coding actual data to render a graph with Amcharts and Angularjs?
If JSON data is provided instead of a JavaScript object the graph is rendered otherwise the graph is not rendered at all. Although chart title is visible but axis are not created and also the data-points are not displayed in the graph.
angular.module('myApp').directive('activityChart',
function ( $timeout) {
return {
restrict: 'EA',
replace:true,
//scope :true,
template: '<div id="{{chartId}}" style="width: 100%; height: 400px; overflow: hidden; text-align: left;"></div>' ,
link: function ($scope, $element, $attrs) {
var chart = false;
var initChart = function() {
if (chart) chart.destroy();
$scope.chartId = $attrs.chartId;
$scope.chartUnit = $attrs.chartUnit;
$scope.chartData = $attrs.chartData;
console.log($scope.chartData);
$timeout(function(){var chart = AmCharts.makeChart($scope.chartId, {
"type": "serial",
"pathToImages": "/assets/amcharts/images/",
"categoryField": "time",
"dataDateFormat": "YYYY-MM-DD HH:NN:SS",
"categoryAxis": {
"minPeriod": "mm",
"parseDates": true
},
"chartCursor": {
"categoryBalloonDateFormat": "JJ:NN:SS"
},
"chartScrollbar": {},
"trendLines": [],
"graphs": [
{
"bullet": "round",
"bulletSize": 4,
"id": $scope.chartId,
"title": $scope.chartId,
"valueField": "value",
"type": "smoothedLine",
"lineThickness": 2,
"lineColor": "#637bb6"
}
],
"guides": [],
"valueAxes": [
{
"id": "ValueAxis-1",
"title": $scope.chartId + " (" + $scope.chartUnit + ")"
}
],
"allLabels": [],
"balloon": {},
"legend": {
"useGraphSettings": true
},
"titles": [
{
"id": "Title-1",
"size": 15,
"text": $scope.chartId
}
],
"dataProvider": $scope.chartData,
});
});
};
initChart();
}
}
}) ;
I made a generalized demo for your use case. Take a look at this fiddle.The key is to introduce an isolated scope for your directive. This allows you to use the directive multiple times.
If you do it like this:
scope: {
// bi directional binding will pass data array to isolated scope
data: '=',
title: '#'
},
you can assign the data array from the parent controller:
<activity-chart data="data" title="This is the title"></activity-chart>
Feel free to ask, if you don't understand how it works.