I have a jsbin setup here http://jsbin.com/esofiq/1/edit
I'm getting confused by the way I think angularjs should work, I have some json data attached to a data attribute, angularjs fetches the data and creates the view. Doesn't calling $scope.mydata within the controller set 'mydata' as the model, and shouldn't it now update the view if the data within the data attribute is changed?
Is this easier to achieve in other frameworks if this isn't appropriate for angular?
I think these two will give you the idea:
How Angular uses data
How to make AJAX calls
Although this is not the usual way to do things in Angular, you can achieve what you want, adding a watch to your data
$scope.$watch(
function () { return $("#mydata").data("a");},
function(newValue) {
$scope.mydata = newValue;
}, true);
Basically we are adding a change listener to your data.
Please check this plunker, where the jquery data is changed every 2 seconds, and the div reacts to this change.
Related
I am interested in the current best practices and solutions for using the data driven documents library with two-way AJAX data bindings. More specifically I am wondering how d3 should be best integrated with libs supporting two-way data bindings such as Angular or Knockout.
The obvious conflicts that arise stem from the fact that d3 and the AJAX libs are both inserting data to the DOM, which basically means that one has to wrap the other.
About Data on DOM
You were worried about the data inserted to the DOM. This are some of the properties added:
D3js: __data__, __onmouseover.force, __onmouseout.force, __onmousedown.drag, __ontouchstart.drag, __onmousedown
AngularJS: value, type, version, align, ng339
So there's no colisions and no need to wrap one into another. You can test this using Object.keys(SOME_DOM_ELEMENT); and Object.keys(SOME_DOM_ELEMENT.__proto__);
About Implementation
Pure javascript
This is how you assign data to D3js:
d3selector.data( myNameSpace.myDataObject );
And this is my data binding approach using watch: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/watch
d3selector
myNameSpace.watch('myDataObject', function (id, oldval, newval) {
d3selector.data( newval );
return newval;
});
This way, everytime you change myNameSpace.myDataObject the data used by D3js will be updated too. But this will only work on Firefox.
AngularJS
There's an answer in this question Angular + d3.js: Data binding with SVG text attr? that explains how to do it using $watch.
Is similar to the Firefox watch:
directive('myDirective', function ( /* dependencies */ ) {
// Imagine your $scope has myDataObject
$scope.$watch('myDataObject', function (newVal, oldVal) {
d3selector.data(newVal);
});
}
Now everytime you change myDataObject in the $scope the data of D3js will be updated too.
More info
Here is an example of two way data binding using polymer: http://bl.ocks.org/psealock/a4f1e24535f0353d91ea you can test it here: http://bl.ocks.org/psealock/raw/a4f1e24535f0353d91ea/
As you can see in refreshChart the binding is not really being used. Instead, on the event triggered when the data changes, D3js loads the new data:
this.g.data(this.pie(this.data));
D3js is not prepared to listen for changes on the data, unless you use the data method. That's why the already rendered data will not change.
If in the future data bindings were implemented, I guess there will be a new method on selection:
selection.update - return placeholders for updated elements.
similar to the current enter and exit:
selection.enter - returns placeholders for missing elements.
selection.exit - returns elements that are no longer needed.
removing the need to create refresh functions.
I have a dropdown that is defined using angular's ng-options syntax. I also have ng-model on the element and I am trying to get the selected model to output to the console. I have read a lot of different SO questions, but I just keep getting undefined. When I have the model defined in a service on rootScope then I get null.
Not sure what is going on, but it seems like the object isn't getting into the ng-model.
In the view:
<select ng-model="selected" ng-change="updateCategory()"
ng-options="conference.Name for conference in Adminconferences
| orderBy:['Name'] "></select>
In the controller:
$scope.updateCategory = function () {
console.log($scope.selected);
}
Update: I realized that maybe if i include what i am trying to do then maybe I can get better responses and have a better question.
This dropdown list is located in the header of my site. I want the user to be able to select which conference they want to manage. Based on that, I want all of the data in the site to be based on the conference's Id. I am not sure what the angular way of doing this is. I think i need to use rootScope and just reference it whenever i need the id for data calls. Is this acceptable?
I think you need to initialize $scope.selected in the controller before you use it inside function.
Initialize it like this:
$scope.selected = 0;
and then use it inside the function.
I am creating a game where the first thing that needs to happen is some state is loaded in from an external JSON file - the contents of one of my directives are dependent on this data being available - because of this, I would like to delay applying the directive until after the data has loaded. I have written the following:
window.addEventListener('mythdataLoaded', function (e) {
// Don't try to create characters until mythdata has loaded
quest.directive('character', function() {
return {
restrict: 'A',
scope: {
character: '#'
},
controller: 'CharacterCtrl',
templateUrl: 'partials/character.html',
replace: true,
link: function(scope, element) {
$(document).on('click', '#'+scope.character, function () {
$('#'+scope.character+'-popup').fadeToggle();
});
}
};
});
});
// Load in myth data
var myth_data;
$.getJSON("js/mythdata_playtest.json", function(json) {
myth_data = json;
window.dispatchEvent(new Event('mythdataLoaded'));
});
However, it appears that my directive's link function never runs - I'm thinking this is because angular has already executed the part of it's cycle where directives are compiled/linked by the time this directive gets added. Is there some way to force angular to compile this directive after it is created? I googled around a bit, and some people suggested adding $compile to the link function for similar issues - but the link function is never run, so that doesn't work for this case. Thanks!
It seems to me it would be better to always configure the directive, to do the JSON call in the directive, and attach logic to the element in the JSON call's success handler. This would, if I understand you correctly, do what you want.
AngularJS is meant as a framework, not a library, so using it in the way you mentioned is not recommended. Exactly as you mentioned, AngularJS does a lot of things for you when it runs. AngularJS, by default, runs on document loaded, and your $.getJSON callback arrives after that. When AngularJS runs it does all its magic with compiling the content and all that.
As a sidenote, it's also more the Angular way to use $http over $.getJSON.
I think you're thinking about this the wrong way. A major ideology in angular is that you set up declarative elements and let it react to the state of the scope.
What I think you might want to do is pass in what you need through the directive scope, and use other angular built in directives to hide or show your default ("non directive") state until the scope gets set from the controller for example.
Example:
You want a box to be hidden until an api call comes back. Your directive sets special styles on your element (not hidden). Instead of delaying to dynamically set your directive, you can pass in a scope var with a default value and use something like ng-show="data.ready" in your directive template to handle the actual dom stuff.
In my Controller, I'm quering data from a $resource object bundled in a caching service.
$scope.data = myService.query(); //myService caches the response.
In the same controller I have the configuration for a chart (for the GoogleChartingDirectve).
$scope.chart = {
'type': 'AreaChart',
'cssStyle': 'height:400px; width:600px;',
....
'rows' : convert($scope.data),
...
}
convert is just a function which takes the data from myService and returns an array for the chart.
This is only working, if the response of the query has already been cached (After changing the route, I can see the graph, but if I call the route directly it is empty).
Perhaps the problem is my caching?
angular.module('webApp')
.service('myService', function myService($cacheFactory,res) {
var cache = $cacheFactory('resCache');
return {
query: function() {
var data = cache.get('query');
if (!data) {
data = res.query();
cache.put('query', data);
}
return data;
}
};
});
I've tried the $scope.$watch in my Controller. It is fired only once with no data. If I change to a different route and back again it is fired again; this time with data.
$scope.$watch('data',function(newValue){
console.log("Watch called " + newValue);
}
I'm not sure what the problem actually is.
It looks like the $watch event is missing.
If I call the refresh with a button, $watch is fired and data is shown.
$scope.refresh = function(){
$scope.data = $scope.data.concat([]);
}
I'm just an angular newbie and playing around in a small demo project.
So I'm looking for the best way to solve some common problems.
Any idea how to solve my problem?
I guess the problem is that you are not aware that res.query() is an asynchronous call. The $resource service works as follow: the query call returns immediatly an empty array for your data. If the data are return from the server the array is populated with your data. Your $watch is called if the empty array is assigned to your $scope.data variable. You can solve your problem if you are using the $watchCollection function (http://docs.angularjs.org/api/ng.$rootScope.Scope):
$scope.$watchCollection('data', function(newNames, oldNames) {
var convertedData = convert($scope.data);
...
});
Your cache is working right. If you make your server call later again you got the array with all populated data. Btw. the $ressource service has a cache option (http://docs.angularjs.org/api/ngResource.$resource) - so you don't need to implement your own cache.
You may ask why the $ressource service works in that way - e.g. returning an empty array and populating the data later? It's the angular way. If you would use your data in a ng-repeat the data are presented to the view automatically because ng-repeat watches your collection. I guess you chart is a typical javascript (for example jQuery) that doesn't watch your data.
I have the following html (which can be accessed directly or called via ajax):
<section id="content" ng-controller="setTreeDataCtrl" get-subthemes>
<dl ng-repeat="subtheme in allSubthemes">
<dt>{{subtheme.Title}}</dt>
</dl>
Then I'm using the following directive:
myApp.directive('getSubthemes', function() {
return function($scope, element, attrs) {
$scope.allSubthemes = [];
angular.forEach($scope.data.Themes, function(value, key) {
angular.forEach(value.SubThemes, function(value2, key2) {
$scope.allSubthemes.push({
'ThemeTitle': value.Title,
'ThemeUrlSlug': value.UrlSlug,
'Title': value2.Title,
'UrlSlug': value2.UrlSlug
});
});
});
}
});
$scope.allSubthemes seems ok, but the dl's don't get rendered.
I can see for a second everything rendered properly and then it get's back to {{subtheme.Title}}, almost like it's being "unrendered"... any ideas of what I'm doing wrong?
Demo jsFiddle: http://jsfiddle.net/HMp3a/
rGil fixed the jsFiddle. It was missing a ng-app="pddc" declaration on an element so Angular did not know where to begin its magic.
I'd like to mention another way to render to the data in question. I suggest using an ng-repeat within an ng-repeat. See my forked & updated fiddle here. You can actually refer to the parent theme within the ng-repeat of the subtheme, so you don't have to copy values from the parent theme into each subtheme (which effectively eliminates the need for the directive in this example).
Another reason to use a nested ng-repeat is because of async issues that could come up when pulling data from a web service asynchronously. What could happen is when the directive executes, it may not have any data to loop through and populate because the data hasn't arrived yet.
If you use two ng-repeats, Angular will watch the $scope.data and re-run the ng-repeats when the data arrives. I've added a 500 ms delay to setting the data in my example to simulate web service latency and you'll see that even with the "latency", the data eventually renders.
There are two other ways around the async issue:
Use scope.$watch() in your directive, to watch for the data manually, or
Use the "resolve" functionality from Angular's routing feature to make sure the data is retrieved prior to controller execution.
While these alternative methods work, I think both are more complicated then just using two ng-repeats.