I want an efficient way to factor an Angular Directive that is written to display a chart.
After reading other answers here, I found a nice way to create a directive that displays a single chart without any problem.
How do I reuse the same directive to display different charts? Each chart needs a JSON object that has settings and data in order to render.
I don't want to pollute my Angular View by typing 100-150 lines of JSON and passing it in via the directive.
Details:-
Each chart has some common key/value pairs that I can leave in the directive.
How do I infuse chart specific key & value pairs in each directive?
Eg:- Say I want one chart to have green bars and the other chart to have red lines.
Angular Directive
(function () {
'use strict';
angular
.module("analytics")
.directive("angularDirectiveAmcharts", angularDirectiveAmcharts);
function angularDirectiveAmcharts() {
var directive = {
link: link,
restrict: 'A',
replace: true,
scope: {
chartdata: '=',
type: '=',
customstyle: '#',
chartsettings: '=',
chartid: '#'
},
template: '<div id="{{ chartid }}" style="{{ customstyle }}"></div>'
};
return directive;
function link(scope, elem, attrs) {
AmCharts.makeChart(scope.chartid, {
"type": "serial",
"categoryField": "date",
"autoMarginOffset": 10,
"marginRight": 20,
"marginTop": 20,
//I've deleted lots of keys and values for the sake of brevity
"dataProvider": scope.chartdata
});
}
}
})();
View
<div class="chartarea" ng-controller="pcController as vm">
<div angular-directive-amcharts chartid="chartdiv" chartdata="vm.chart_data"></div>
</div>
I am particular about maintainability because a lot of changes are going to made after I'm done with my internship.
Parts of the given code in this answer are based on another answer
You could use a service to provide a standard configuration to all of your chart directives. In this service you can define this standard configuration once and merge it with a specific configuration each time, a directive is created. This way you only have to declare minor changes in your controller.
Nonrequired but possible config binding into directive:
<div ng-controller="myCtrl">
<my-chart></my-chart>
<my-chart config="conf"></my-chart>
</div>
Specific configuration in controller:
myapp.controller('myCtrl', function ($scope) {
$scope.conf = {
graphs: [{ type: 'column' }]
};
});
Service for default configuration (using jQuerys way to deep merge objects):
myapp.service('chartService', function () {
this.defaultConfig = {
"type": "serial",
// reduced object for readabilty
};
this.getConfig = function (mergeObj) {
return $.extend(true, {}, this.defaultConfig, mergeObj);
}
});
The data is get through another service, and added to the configuration after the merge:
var config = chartService.getConfig(scope.config || {});
config.dataProvider = dataProvider.getData();
chart = AmCharts.makeChart(element[0], config);
I've prepared a fiddle, so you can take a look into an example.
Related
So I have been trying to make a linechart work with Echarts. I made this LineChart.vue and expect it to get props, which are arrays, from its father component as options data of Echarts.
But the props, which are proxies of arrays, doesn't seem to work well. It is shown in the console that it has the right target, but this proxy is not recognized by Echarts, so there was no data on my chart.
And to make it wierder to me, I accidently found out that if I keep my terminal open, make some changes to the code (which is nothing but comment and uncomment the same lines), and save it (which probably rerends this component), the props somehow works and the linechart actually shows up! But if I refresh the page, the data goes blank again.
Here is my code:
<template>
<div id="chart"></div>
</template>
<script>
let chart;
export default {
data() {
return {
option: {
name: "demo",
xAxis: {
type: "category",
data: [],
},
yAxis: {
// type: "value",
},
series: [
{
data: [],
type: "line",
},
],
},
};
},
props: {
xAxisData: Array,
seriesData: Array,
},
methods: {
initChart() {
chart = this.$echarts.init(document.getElementById("chart"));
// these are the four lines that I commented and uncommented to make things wierd
this.option.xAxis.data = this.xAxisData;
this.option.series[0].data = this.seriesData;
console.log(this.option.xAxis.data);
console.log(this.option.series[0].data);
chart.setOption(this.option);
},
},
mounted() {
this.initChart();
},
watch: {
xAxisData: {
handler: function (newData) {
this.option.xAxis.data = newData;
},
deep: true,
},
seriesData: {
handler: function (newData) {
this.option.series[0].data = newData;
},
deep: true,
},
},
};
</script>
<style scoped>
#chart {
height: 250px;
width: 400px;
}
</style>
And here iswhat is the proxy like before and after I made some minor changes to the code
I also tried to turn this proxy xAxisData into an object using Object.assign(), but it turns out to be empty! I am starting to think that it might have somthing to do with component life cycle, but I have no clue when and where I can get a functional proxy. Can someone tell me what is actually going on?
FYI, here are value of props in console and value of props in vue devtool.
Just figured it out. Just so you know, the info provided above was insufficient, and I made a noob move.
My vue component was fine, it was async request that caused this problem. The data for my Echarts props is requseted through a Axios request, and my child-component (the linechart) was rendered before I got the data. Some how, the proxies of the arrays donot have the data, yet they got the target shown right. By the time my child-component got the right data, the Echart was already rendered with outdated options data, which by the way was empty. And that is why re-render it can show us the data. It has nothing to do with proxy, proxy works just fine. It is me that needs to pay more attention to aysnc movement. Also, I learned that obviously Echarts was not reactive at all, so I watched the props and updated the option like this:
watch :{
xAxisData: {
handler: function (newData) {
this.option.xAxis.data = newData;
this.chart.clear();
this.chart.setOption(this.option);
},
deep: true,
},
}
It works.
I've got a directive which I want to use in different cases, but all of them will be providing a list (array) from which it will take the information.
The problem is that every array will be / can be filtered by multiple filters (and some of them are custom filters). For example:
<select-search data-list="Ctrl.players | playersByDate:Ctrl.event.datetime | orderBy: 'name'"></select-search>
Those arrays, as I said, can have one, multiple or zero filters applied, some of them custom-made and some of them not (like orderBy). The problem is that when I do this, I get a $digest cycle error with any filters (custom-made and Angular filters aswell).
First, I've read the reasons why, but I do not understand them, because I'm using a one-way binding on the list:
scope:{
property: '#',
ref: "=",
list: "<",
change: "&?"
}
Therefore, it should only be triggering once (when it's filtered the first time), because after this no other changes are made in any of the sides.
Regarding this, I would like to know how can I filter the list/array at the moment of sending it to the directive (so, not filtering and storing it via JS in another variable) without getting any $digest error.
EDIT:
As requested, I've written the code in a snippet (JsFiddle here):
(function(){
var app = angular.module("AppMd", []);
app.controller("Control", [function(){
var vm = this;
vm.players = [
{name: 'Player 1'},
{name: 'Player 2'},
{name: 'Player 3'}
];
}]);
app.directive("selectSearch", function(){
return {
restrict: 'E',
scope:{
list: "<"
},
replace: true,
template: '<ul><li data-ng-repeat="obj in list">{{obj.name}}</li></ul>',
link: function(scope, element, attrs, controller, transclude){
return false;
}
}
});
})();
<script src="https://code.angularjs.org/1.6.4/angular.min.js"></script>
<div ng-app="AppMd" ng-controller="Control as Ctrl">
<select-search data-list="Ctrl.players | orderBy: 'name'"></select-search>
</div>
As you can see, it works, but if you look at the console, you'll see the errors it triggers.
Thank you!
Update 2 added, see below
First of all, this is the starting point of the framework I am working with (and needs to fix):
// index.html
<!doctype html>
<html ng-app="myApp">
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.7/angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
<script src="index.js"></script>
<body>
<div ng-controller="outerController">
<div id="copy"></div>
<hr>
<div id="src" ng-controller="innerController">
<table>
<th>Name</th>
<th>Type</th>
<tbody>
<tr ng-repeat="poke in pokemon">
<td>{{ poke.name }}</td>
<td>{{ poke.type }}</td>
</tr>
<tr>
<td>Pikachu</td>
<td>Electric</td>
</tr>
</tbody>
</table>
</div>
</div>
</body>
</html>
// index.js
var app = angular.module("myApp", []);
app.controller("innerController", function($scope) {
$scope.pokemon = [{
name: "Bulbasaur",
type: "Grass/Poison"
}, {
name: "Charmander",
type: "Fire"
}, {
name: "Squirtle",
type: "Water"
}];
});
app.controller("outerController", function($scope) {
$("#copy").html($("#src").html());
});
So as you can see, the child controller will generate a table from its scope's data via ng-repeat. This step is successful. The next step is for the parent controller to copy-paste the inner HTML from src to copy. The intent is for copy to contain a copy of the complete table fully generated by angularJS inside src.
This step has failed. Only the table headers and the static Pikachu row is visible. After doing some research I am certain that this is because pokemon is inside the child controller's scope which is inaccessible by the parent controller. The HTML copied into the copy container includes the entire ng-repeat directive. This copied directive is inside the parent scope, where $scope.pokemon does not exist/contains no data, which is why the ng-repeat in copy generated nothing.
I cannot put the data inside the parent controller. In my actual application, the system uses a modular design. Each inner controller represents a module which pulls its own set of data from the server. There are multiple web pages (represented by the outer controller) which have a many-to-many relationship with the modules, and the composition of modules in each web page needs to be modifiable. That means the data used by a module must be contained within itself.
How can I rectify this?
Update 1: Redacted. I posted an example of using $emit and $on but Robert's example should be assumed as correct, since I'm still very new to this. Refer to his answer.
Update 2: While testing Alvaro Vazquez's & Robert's solutions, I've identified the specific root cause. When $("#copy").html($("#src").html()); is executed, either the copied ng-repeat executed before any data transfer to outerController occurred, or it was never executed. In the end, modifying what I originally did above makes it fully working:
var app = angular.module("myApp", []);
$(function() {
$("#copy").html($("#src").html());
});
app.controller("innerController", function($scope) {
$scope.pokemon = [{
name: "Bulbasaur",
type: "Grass/Poison"
}, {
name: "Charmander",
type: "Fire"
}, {
name: "Squirtle",
type: "Water"
}];
});
app.controller("outerController", function($scope) {
$scope.pokemon = [{
name: "Bulbasaur",
type: "Grass/Poison"
}, {
name: "Charmander",
type: "Fire"
}, {
name: "Squirtle",
type: "Water"
}];
});
With the location of that particular statement changed, all that is left is to transfer the data to outerController, and at this point both Alvaro's and Robert's solutions work.
As an aside, I think some have advised against using $("#copy").html($("#src").html());. As I have partly described in the comments, the actual application I'm developing consists of multiple web pages, each containing its own outerController. Each innerController is in its own separate HTML file added via an include directive into src. The outerController copies the inner HTML of src, passes it to a third party library, which pastes it into copy and controls its visual layout. $("#copy").html($("#src").html()); is actually part of the third party library's implementation, so I can't change that. Using this statement is therefore a requirement.
I'll post the above as a solution when I get home and has the convenience of a PC keyboard. In the meantime feel free to recommend better solutions to what is found if you have one, thanks!
I think you should make use of angular services.
Declaring a service
First of all, you should declare a service which would 'serve' the data to the rest of your application. For the sake of simplicity, I will only show a method which returns a predefined array, but you could get the data from the server here.
app.service('pokemonService', function () {
return {
getPokemon: function () {
return [{
name: "Bulbasaur",
type: "Grass/Poison"
}, {
name: "Charmander",
type: "Fire"
}, {
name: "Squirtle",
type: "Water"
}];
}
};
});
Using the service in your controller
Then, you can use the service on any of your controllers, injecting it as any other predefined angular service:
app.controller('innerController', function($scope, pokemonService) {
$scope.pokemon = pokemonService.getPokemon();
});
app.controller('outerController', function($scope, pokemonService) {
$scope.outerPokemon = pokemonService.getPokemon();
});
Showing the data in your view
Finally, you can list all your pokémon in any template/part of the template you want:
<!doctype html>
<html ng-app="myApp">
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.7/angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
<script src="index.js"></script>
<body>
<div ng-controller="outerController">
<div id="copy">
<!-- Here you can also list the pokémon from your outerController, maybe this time in a list -->
<ul>
<li ng-repeat="poke in pokemonOuter">
{{ poke.name }} - <span class="type">{{ poke.type }}</span>
</li>
</ul>
</div>
<hr>
<div id="src" ng-controller="innerController">
<table>
<th>Name</th>
<th>Type</th>
<tbody>
<tr ng-repeat="poke in pokemon">
<td>{{ poke.name }}</td>
<td>{{ poke.type }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</body>
</html>
Wrap up
As you can see, there is no need of messing with the DOM at all. If you use AngularJS, you should do things the Angular way, and working directly with the DOM is not the Angular way at all. Instead, you should put all your data and business logic into services, then use those services in your controllers to retrieve that data and pass it to the view.
Scopes in Angular uses prototypal inheritance, so the child scope will have access to the parent properties but the parent will not have access to the child controller scope properties.
You can use a service to share data or use $emit to send events upwards (upwards until the root scope).
I created a plnkr for you to show you how to use emit (you can find it here)
var app = angular.module("myApp", []);
app.controller("outerController", ['$scope', function($scope) {
console.log('aici');
$scope.$on('pokemonEvent', function(event, mass) { console.log(mass); });
}]);
app.controller("innerController", ['$scope', function($scope) {
$scope.pokemon = [{
name: "Bulbasaur",
type: "Grass/Poison"
}, {
name: "Charmander",
type: "Fire"
}, {
name: "Squirtle",
type: "Water"
}];
$scope.$emit('pokemonEvent', $scope.pokemon);
}]);
I would like to dynamically load select elements from an API request.
Here is my controller:
var myApp = angular.module('myApp',[]).controller('tripCtrl', function($scope){
//Call to API to get people
$scope.people = [
{
"id": 1,
"name": "Joe Hamlet"
},
{
"id": 2,
"name": "Mary Jane"
},
{
"id": 3,
"name": "Tom Lee"
}
];
//Call to API to get the element to load
$scope.selectElement =
{
"Options": "person[dynamicValue] as person[dynamicDisplayName] for person in people",
"DisplayName": "name",
"Value": "id"
};
//Dynamicly load properties
$scope.dynamicValue = $scope.selectElement.DisplayName;
$scope.dynamicDisplayName = $scope.selectElement.Value;
});
HTML:
<select ng-model="selectedPerson" ng-options="{{selectElement.Options}}">
<option value="">Select</option>
</select>
{{selectedPerson}}
I created a JSFiddle trying to accomplish this. http://jsfiddle.net/HB7LU/9493/
I found this question which I was able to implement, but when I tried to set the ng-options from the Element's Options property, it failed to load. When inspected the HTML the code looks to be set properly, but the model binding isn't working.
Edit 12/28/2014:
After updating the Angular version in the original JS Fiddle, it worked properly, however when I expanded to use an actually API, I found another issue with loading ng-options dynamically. Here is the more in depth JS Fiddle: http://jsfiddle.net/zjFp4/330/
Also here is my updated controller. The dataService.getElement() calls a hard coded string, where as the dataService.getElementFromApi() calls the same exact string, just from json-generator (which is the mock API). When inspected the objects set from the API, everything is there, so it must be an issue with the binding in Angular. Any ideas on how to fix this?
function tripCtrl($scope, dataService) {
//Call to API to get people
dataService.getPeople().then(
function (event) {
$scope.people = event;
},
function (s) {
console.log(s); }
);
//Call to API to get the element to load
$scope.selectElement = dataService.getElement();
dataService.getElementFromApi().then(
function (event) {
$scope.apiElement = event;
$scope.dynamicValue = $scope.apiElement.Value;
$scope.dynamicDisplayName = $scope.apiElement.DisplayName;
},
function (s) {
console.log(s); }
);
}
Ok, so I'm trying to get a grip on Mustache.js for rendering views in javascript. I have an API that returns a number of "events", which can be a number of different types. I want to render the events in (very) different ways, based on their type:
data : {
events: [ {title: 'hello', type: 'message'}, {title: 'world', type: 'image'} ] }
Ideally, I could do something like this:
{{#events}}
{{#message}}
<div class="message">{{title}}</div>
{{/message}}
{{#image}}
<span>{{title}}</span>
{{/image}}
{{/events}}
But that would (right?) force me to refactor my data into:
data : {
events: [ {message: {title: 'hello'}}, {image: {title: 'world'}} ] }
Is there a better way of solving this, without refactoring my data? Or should I just bite the bullet?
Mustache is logic-less so there's not much you can do with pure template code other than switching to Handlebars.
Your Mustache-friendly alternative would be to declare a helper and use it to select which template to render. It gets a little convoluted but you can avoid switching away from Mustache if that's something you can't change:
var base_template = '{{#events}}' +
'{{{event_renderer}}}' +
'{{/events}}';
var message_template = '<div>message: {{title}}</div>';
var image_template = '<div>image: {{title}}</div>';
var data = {
events: [ {title: 'hello', type: 'message'}, {title: 'world', type: 'image'} ],
event_renderer: function() {
return Mustache.render('{{> ' + this.type + '}}', this, {message: message_template, image: image_template});
}
}
Mustache.render(base_template, data);
The trick here is that you create a base template that will be the iterator, and pass in event_renderer as a helper. That helper will in turn call Mustache.render again, using partials to render each type of event you can have (that's the {{> partial}} notation).
The only ugly part here is that you need to add this event_renderer member to your JSON data, but other than that, it should all be fine (in Handlebars you could declare it as a helper and there's no need to merge it with your data).