I'm using highmaps to create a vector map that can change based on the data values loaded into the series[0].data array. The initial chart is created with my default values, and inserted into the page using a custom directive. When a select dropdown changes value, I fire off a function using ng-change to update the chart values.
My ng-change function fires and runs as expected, until I attempt to access the series containing the data used to populate highmaps. It seems that after the initial chart is created, the series array is given a value of null. This persists even when I attempt to load in a fresh chartOptions object. I've noticed that when the $scope object containing the chartOptions isn't bound to the directive, then the series is populated with the correct values. The array is only empty when it is bound to the directive.
This only happens with my highmaps. All of the other highcharts on the page have their series arrays visible for editing after being two-way bound to the directive. Also all other elements of my highmap chartOptions object can be viewed and edited, just not the part with the actual data.
My directive
.directive('mapChart', function(){
return {
restrict: "E",
replace: true,
template: "<div id='map' class='div_content-holder'></div>",
scope:{
options:"=",
},
link: function(scope, element){
Highcharts.mapChart(element[0], scope.options);
});
}
};
});
My html
<map-chart options="region_options"></map-chart>
my JavaScript
$scope.region_options = {};
var setRegionMap = function(jsonData){
var chartOptions = selectChart('map'); // grabs my premade options object
// grabs data from somewhere else and adds it to chart options
chartOptions.series[0].data.forEach(function (region, index) {
region["value"] = $scope.region_map.one_month[index];
console.log(chartOptions.series[0].data); //says that series exists
});
$scope.region_options = chartOptions;
console.log($scope.region_options); //says that .series is null, everything else is still there
//$scope.$apply(); //i do this elsewhere in my ajax call
};
Is this an issue with two-way binding the array with highmaps, or angular, or both?
You can change your directive in the following way, adding a new parameter for the current data.
Adding the two watches you will ensure that every time you change data from outside, the chart will be refreshed setting up the correct data and with the first watch on the option you will ensure to instantiate the chart when your options have really been loaded:
.directive('mapChart', function(){
return {
restrict: "E",
replace: true,
template: "<div id='map' class='div_content-holder'></div>",
scope:{
options:"=",
current: "=" // your actual data
},
link: function(scope, element){
var chartexample = {};
scope.$watch('options', function() {
chartexample = Highcharts.mapChart(element[0], scope.options);
});
scope.$watch('current', function(){
chartexample.series[0].setData(scope.current);
});
}
}
});
You can get rid of both the watchers.
About the init, you just have to be sure to init your directive once you have already all your options, so you don't need anymore the watch on the options.
About the current data, you can add a callback parameter & in the directive which will update the values of the chart, and call this callback of the directive inside the function associated to the ngChange of the select inside the controller.
This will make the code better and cleaner.
But for now you can enjoy your chart in this way :)
My best luck for your app!
Cheers :)
Related
I'm using Highcharts with AngularJS. I have a directive which generates the chart. In the link function I specify a default object chartOptions. After setting the default options, I append that object to another object passed to the directive which persists between routes and refreshes, passedObject. Thus any changes to chartOptions after being appended successfully persist. Then the chart generates with the call to Highcharts.chart();
return {
restrict: 'E',
scope: {
passedObject: "="
},
link: function (scope, element) {
var chartOptions = {
title : {
text: 'sample report'
}
...
}
scope.passedObject.chartOptions = scope.passedObject.chartOptions || chartOptions;
var chart = Highcharts.chart(element[0], scope.passedObject.chartOptions);
}
// successfully add new series
// series rendered immediately but does not persist though view changes and page reload.
scope.addSeries = function(seriesObject){
scope.elementObject.chart.addSeries(seriesObject);
};
// successfully pushes object to array
// does not immediately render because Highchart doesn't see change
// but does render after reloading because Highchart checks options again
// Does not render with animation like addSeries() API does.
scope.addSeriesNoAPI = function(seriesObject){
scope.elementObject.chart.series.push(seriesObject);
};
}
There is no issue generating the chart the first time, and if I return to this view, the chart generates again with the same default options.
However, if I use the Highcharts API to programatically add a new data series chart.addSeries(newSeriesObject) the series is added and renders on the chart, but will not persist between routes and refreshes.
If I manually add the series object to my chartOptions via straight JS scope.passedObject.chartOptions.series.push(newSeriesObject) the object is successfully pushed to the array and will persist, but the chart will not automatically update because it doesn't know the series array changed, which is clearly why the API exists.
Upon closer inspection, it appears that Highchart's addSeries() function is pushing the new object to some array other than the one I am persisting, but I can't figure out how to get this to work.
I assumed that this line of code: var chart = Highcharts.chart(element[0], scope.passedObject.chartOptions); would bind the chart to the scope.passedObject.chartOptions object, such that when something like the addSeries() function added a new series, it would be added into scope.passedObject.chartOptions. After logging and watching objects, however that does not appear to be the case.
One way is to hold that data in the root scope (but that is not the best way).
Another is to use angular service to store this data. And include this service where you need it.
An angular service unlike controllers will not get reset when changing views/pages.
I would suggest that you create an angular service that stores the chart options, which has a chart options object and a getter and a setter. You could also hold the functions that make changes to the chartobject in this service.
In your case you seem to do manipulations to the chart object within the controller (after you are getting it from a service perhaps?), if so you will have to set the new object back to the service.
Also when I say service I mean something like this:
app.service('highChartService', function() {
var chartOptions;
var highChartServiceObj = {};
highChartServiceObj.getChartOptions = function() {
return chartOptions;
};
highChartServiceObj.setChartOptions = function (options) {
chartOptions = options;
};
highChartServiceObj.computeChartObject = function () {
//do some computation
}
return highChartServiceObj;
});
Add the above service to your directive and use that to update your highchart object when any changes are made.
Also if I am not wrong what you would like to do is add newseriesobject like this chart.addSeries(newSeriesObject) which solves your highchart dilemma. Why not then update your chart object as the next step: scope.passedObject.chartOptions.series.push(newSeriesObject)? Though I would rather that be part of the service too if I was doing this, all of the highchart manipulations would just be a part of the service, where I would have a function like this in above service:
highChartServiceObj.updateChart(newSeriesObject, chart) {
chartOptions.series.push(newSeriesObject);
chart.addSeries(newSeriesObject);
}
I have an array of objects and each object can have slightly different data. I using ng-repeat on a directive that selects the correct directive to display. That "display" directive takes the object and displays the data in the correct format. This all works great.
Now I need to reorganize the order of the array. When I update the array, the "display" directives do not re-order, only the data changes. For example most of the data sets have a 'title' field, so those update, but the "display" does not.
How would I best refresh the ng-repeat so that it would properly re-order the "display" directives?
I can post code, but I feel this is more of a conceptual question.
HTML:
<module-builder module="{{$index}}" ng-repeat="module in build.modules track by $index"></module-builder>
module-builder:
(function() {
'use strict';
angular
.module('app.email_editor')
.directive('moduleBuilder', moduleBuilder);
function moduleBuilder ($compile) {
var directive = {
link: link,
restrict: 'EA'
};
return directive;
function link(scope, element, attrs) {
var module = scope.build.modules[attrs.module];
var template = '';
switch (module.Name){
case 'moduleTypeOne':
template = "<module-type-one class='{{module.Class}}'></module-type-one>";
break;
case 'moduleTypeTwo':
template = "<module-type-two class='{{module.Class}}'></module-type-two>";
break;
default:
break;
}
element.html(template);
$compile(element.contents())(scope);
}
}
})();
data sample:
BTW html5Sotable is the interface for re-ordering
You problem in track by $index, in this case angular re-render element only if $index changed.
So, if you really have duplicated items in your array, you should track it by something else.
I am doing a learning project for the MEAN stack, and I am really stuck at something which I need help.
On the real project, what I do is I have a Form creator were different components can be created and arranged according to the user needs, then directives similar to the used on this simplified example renders the from component by component and enable the user to populate it.
On this JSFiddle, as I mentioned, there is a simplified version where I use a similar approach to the one I want to use on my project.
My Logic is: I create a new array where all the values I input on the textbox are stored after a small processing on the format, then I have two directives that have access to a factory function where the data is stored, that loop trough all the items and render them one by one.
I can see that the factory function is working and create the set of data as I wanted to be.
Here is where the problems start:
I call the directive like this
<render-all-items></render-all-items>
the definition of this directive is
.directive('renderAllItems', function (DataServ) {
return {
restrict: 'E',
scope: {},
link: function (scope, elem, attrs) {
scope.values = DataServ.currentTemplate.getAllItems();
},
template: '<div ng-repeat="item in values">{{item}}<render-item render="item"></renderItem></div>'
};
});
This directive supposedly iterates the list of elements and render them one by one. The single Item render is working after a initial typo correction.
The output on the modal is:
[[
Item order = and item value =
]]
Item order = and item value =
And is always the same output, no matter how many items are on the array.
My main goal is easy:
I should be able to add as many items I want using the textbox and then when I press the open modal, I should be able to see the list of elements rendered in the modal dialog.
I would really appreciate guidance on where I am doing it wrong to achieve the result I want.
Thanks in advance.
You have a typo on this line:
<div><pre><render-item render="template.createTemplateItem(textBoxData)"></render-item></pre>
Should be:
<div><pre><render-item render="template.CreateTemplateItem(textBoxData)"></render-item></pre>
This segment of code:
Template.prototype.getAllItems = function () {
//take a template item object and add it to
//the template items repository
return JSON.stringify(this.items);
};
is called once by your renderAllItems directive when it links:
link: function (scope, elem, attrs) {
scope.values = DataServ.currentTemplate.getAllItems();
},
All changes to that template's items array are not reflected in the directive because you JSON.stringify'd the array.
https://jsfiddle.net/urq3gu5o/
I've built a directive that basically creates a markdown-friendly text editor. It will be used at various places throughout my site, anywhere an end user wants to use markdown to do some basic content styling (product descriptions, that sort of thing). The challenge is that as this directive will be deployed in multiple places, it won't be editing the same property on every model. For example, in one spot it may edit the LongDescription of a product, whereas in another spot it may edit the ShortDescription of an ad campaign, or the Bio of a user.
I need to be able to pass in the property that I want to edit to the directive using the scope '=' method that permits two-way data binding, so the property is changed both in the directive and on the original controller, allowing the user to save those changes. The problem that I'm having is that if I pass the property itself to the directive:
<markdown-editor model="product.Description"></markdown-editor>
two-way data binding doesn't work, since this passes the value of the Description property. I know that for the '=' method to two-way bind in a directive, I have to pass an object as the attribute value from my HTML. I can easily pass the entire object:
<markdown-editor model="product"></markdown-editor>
and then access the Description property within the directive:
<textarea ng-model="model.Description"></textarea>
but this hardcodes Description into the directive, and I may not always want that property.
So my question is, how can I two-way bind to a single property of my object, without the directive knowing ahead of time what that property is? I've come up with a workaround but it's pretty ugly:
HTML:
<markdown-editor model="contest" property="Description"></markdown-editor>
Directive JS:
angular.module('admin.directives').directive('markdownEditor', [
'admin.constants.templateConstants',
'$sce',
function (Templates, $sce) {
var directive = {
restrict: 'E',
replace: true,
templateUrl: Templates.Directives.MarkdownEditor,
scope: {
model: '=',
property: '#'
},
controllerAs: 'markdownEditor',
controller: markdownEditorController
}
function markdownEditorController($scope) {
var vm = this;
vm.display = { markdown: true };
vm.content = { markdown: '', html: '' };
console.log($scope.model);
vm.setDisplay = function (type) {
vm.display = {};
vm.display[type] = true;
}
$scope.$watch('model', function (newModel, oldModel, $scope) {
vm.content.markdown = $scope.model[$scope.property];
});
$scope.$watch('markdownEditor.content.markdown', function (newDescription, oldDescription, $scope) {
$scope.model[$scope.property] = newDescription;
if (newDescription !== "" && newDescription !== null && newDescription !== undefined) {
vm.content.html = $sce.trustAsHtml(marked(newDescription));
}
});
}
return directive;
}
]);
Relevant part of the directive template:
<textarea class="ad-basic-input" ng-model="markdownEditor.content.markdown" ng-if="markdownEditor.display.markdown"></textarea>
Notice that the directive uses a watch to look for changes on the content.markdown field, then pushes those back into model[property] manually (the second $watch near the bottom). It also has to $watch for changes to the model being passed in from the controller above because that's being loaded asynchronously, and needs to be assigned to the content.markdown field initially.
This code works, but having these two watches, especially the one that looks for changes on the model, seems like a big code smell to me. Surely there must be a better way to pass in, edit, and two-way bind a single property of an object on the controller, when that property is unknown?
Thanks!
This is an addendum to this question I asked previously:
Why does my custom directive not update when I derive the output from the bounded data?
My current dilemma is that I have duplicates in data that I generate inside a custom directive used in an ng-repeat. This means I have to use "track by". This somehow breaks the binding and when I update the model, it no longer updates. If I don't use update and remove the duplicates (which for the example can be done easily but for my real problem I cannot), it works. Here is the jsfiddle of how the issue:
http://jsfiddle.net/Lwsq09d0/2/
My custom directive has this:
scope: {
data: "="
},
link: function (scope, element, attrs) {
scope.$watch(function () {
return scope.data
}, function () {
var getPages = function(extra) {
var pages = [];
pages.push('...');
for (var i = scope.data.list[0]; i <= scope.data.list[1] + extra; i++) {
pages.push(i);
}
pages.push('...');
return pages;
}
scope.pages = getPages(1);
}, true);
},
// Remove "track by $index" to see this working and make sure to remove the duplicates
// "..." pushed in to the generated data.
template: '<ul><li ng-repeat="d in pages track by $index" my-button="d"></li></ul>'
In the fiddle, I have an ng-click call a controller function to modify data.
I've seen other questions about track by breaking binding, but I haven't seen one where the ng-repeat variable is generated in the custom directive via the bound data.
Thanks for any help.
Track by is optimized not to rebuild the DOM for already created items. In your example, you are using $index as the identifier. As such, ng-repeatsees the identifier 1 (for the second element in the pages array) and decides that it does not have to rebuild the DOM. This is causing the problem that you are experiencing.
One possible solution might be to generate page Objects that have a unique id, and to track by that:
var lastID = 0;
function createPage(name){
return { name: name, id: lastID++ };
}
// ... Directive code
pages.push(createPage('...')); // Do this for everything that you push to pages array
// ... More directive code
template: '<ul><li ng-repeat="d in pages track by d.id" my-button="d.name"></li></ul>'
Your JSFiddle, updated to work: http://jsfiddle.net/uv11fe93/