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.
Related
I am trying to get Knockout and a Bootstrap-TreeView to work together.
(The component: https://github.com/jonmiles/bootstrap-treeview)
At the moment, I'm passing the JSON from an API call to the constructor of the View Model. This will change later but for simplicity, I'm doing this.
What I need then is to bind click events to each node. So if I click the root node, nothing happens, click a folder, and I can get a list of all it's direct child text values (Just alert them for now), and if I click a file node, I alert the 'data' value from that node.
Here's a fiddle to see what I have done so far.
https://jsfiddle.net/Cralis/h15n2tp7/
My View Model simply initialises with the json data. And then a computed in the view model does the setup of the Tree View.
// Create the View Model.
var ViewModel = function(jsonData) {
var self = this;
self.MyData = ko.observable(jsonData);
ko.computed(function() {
$('#tree').treeview({
data: self.MyData()
})
.on('nodeSelected', function(event, data) {
if (data.nodeLevel == 2) { // Are we clicking a File?
alert("Clicked a File. Data: " + data.data)
}
else
if(data.nodeLevel == 1) { // We're clicking a folder.
alert("Clicked a folder. Would like to somehow alert a list of all child node text values.")
}
});
})
}
// Create the View Model and initialise with initial data
var vm = new ViewModel(getTree());
// Bind.
ko.applyBindings(vm, document.getElementById("bindSection"));
This works, but I don't think I'm using Knockout much. That's because my click events are in my javascript, and my Knockout view model doesn't really have any control.
How can I allow Knockout to 'see' the click events. So, onclick of a node, a knockout computed (I think?) fires and I can then control the UI based on bind events.
Outside of this, I have a DIV which shows a list of files. What I was was that when a folder level node gets selected, I can populate that div with all the 'text' values from the children of that selected folder node.
Any pointers in how I can achieve this would be amazing. I'm just not sure how I can get data-bind="click... to the nodes, which can then run the code that's currently in the 'onclick' in my fiddle.
I've updated your fiddle with a custom binding: https://jsfiddle.net/h15n2tp7/2/
As I already posted here in this question: add-data-bind-property-to-a...
I think this is the best way do it. The problem here is the synchronization between 1) fetching JSON 2) applying bindings 3) creating DOM elements. Creating custom binding lets you do that easily without much of messy code. In your case, when a getTree function is done via $.get, you need to create a view model in .done function, and apply bindings after that. So the provided fiddle will change a bit, but the idea is the same. Note, that you don't need any observables (if the tree data does not change while the app is running). If it does change though, make sure that you implement update function in a custom binding (knockout custom binding reference).
Angular 2 data binding is great but i can't seem to find a angular 2 way of removing data binding on specific variables. My reason for this is i started hooking my application up to indexed DB and it works but i can't allow the temporary cache (just an array of all the indexed DB values) to be subject to data binding (if it was then the temporary cache would no longer mirror the database) my database is on an angular2 service. now i have found a way of removing the data binding but it isn't exactly pretty my code is this
app.copy=function(item){
return JSON.parse(JSON.stringify(item,app.replacer),app.reviver);
}
app.reviver=function(key,value){
if(value.fn){
value=new Function(value.parameters,value.body);
}else if(key==="time"){
value= new Date(value);
}
return value;
};
app.replacer=function(key,value){
if(typeof value ==="function"){
value=value.toString();
value={
fn:true,
parameters:value.match(/\(([\s\S]*?)\)/)[1].replace(/[\s\r\/\*]/g,""),
body:value.match(/\{([\s\S]*)\}/)[1].replace(/[\t\r\n]/g,"")
};
}
return value;
};
like i said it works but it isn't pretty. i can just run app.copy on the variables before they leave the cache so that they don't get data bound to anything. I was wondering if there was a cleaner way to tell angular 2 this variable isn't suppose to be data bound. and if not then at least i was able to get my solution up here for others.
If you establish "binding" imperatively you can stop the binding imperatively. There is currently no support in Angular2 to cancel a declarative binding imperatively.
Bind the view only to fields of the component.
Use observables in the service that fire an event when values change.
In the component subscribe to the observable and update the fields in the component when values in the service change.
Update values in the service when values change in the component.
I would like to have a polymer element with two sub-elements, one that produces data, and the other that performs some action when the data changes (in my case: sending a notification to a server).
To implement this, I wrote a polymer element, namely root, with the following structure (names changed to simplify the discussion):
<producer data={{foo.bar}}></producer>
<consumer data=[[foo]]></consumer>
The producer changes the data using the set('property', 'value') method, so that the root element sees the notifications. The problem is that the consumer element won't notice the changes to foo since they involve a sub-property.
To solve this, I tried using a computed binding as follows:
<producer data={{foo.bar}}></producer>
<consumer data=[[_compute(foo)]]></consumer>
...
_compute: function() {
return this.foo;
}
However this won't cause the consumer to be notified. I think the reason for this is that the returned object is the same reference (only a sub-attribute changed). Currently the workaround I've used is to use the following version of the compute function:
_compute: function() {
return Object.assign({}, this.foo);
}
This works (the consumer element gets notified), however I'm affraid it might not be the most efficient (I'm creating an object at every call of _compute) and/or elegant way. Then my question is: what is the proper way to achieve this behavior in Polymer?
Do you have access to modify the consumer element?
The best way to fix this is to have the consumer element have a multi-property observer that listens for sub-property changes on the data property.
It might look something like this:
Polymer({
is: 'consumer',
properties: {
data: Object
},
observers: ['consumeData(data, data.*)'],
consumeData: function (data) {
//Do whatever you were planning on doing with data here
}
});
The advantage of an approach like this is that your 'consumer' element just 'knows' how to consume the data object when a sub-property on it changes. Because of the lighter weight approach to data binding in Polymer, trying to implement this behavior outside of the 'consumer' element will necessarily be more expensive and more complicated, since it requires either tricking the data binding into thinking the data object is new by supplying it with a new reference to a copy or forgoing the data binding altogether and building an approach on top of events and calling methods on the consumer in response to events. So if at all possible, I would recommend trying the approach above.
Polymer's data binding does not work the same way as some other two-way enabled data binding implementations, like what you might find in AngularJS. Rather than using dirty-checking, which is extremely expensive, Polymer uses an event based 'path notification' approach. When a sub-property on a property changes, a Polymer element which has that property will fire an event to it's immediate children bound to that property, notifying them that the path 'property.subProperty' has changed. In order for consumer to act on those changes, it has to be told to listen to changes along that 'property.subProperty' path. We specify paths in our polymer observers by using the syntax above. In this case, putting data.* in our observer means we want to listen to any path off of data, so that any notified property change on the data property will trigger the observer.
As you have noticed there isn't an elegant way of doing this. The way you got it working is interesting.
An alternative way which I would expect to work would be to fire an event from within the producer element.
this.fire('data', {data: this.foo.bar});
and then have the parent/root element listen for this event and then update the data property of the consumer element.
<producer on-data="handleData"></producer>
<consumer id="consumer"></consumer>
handleData: function(e) {
self.$.consumer.data = e.detail.data;
}
Edit:
You could make a new property that you compute within the producer element. Then you won't have to do a computed function everytime you want to access foo.bar
Producer element
properties: {
foo: {},
bar: {
computed: 'computeBar(foo)'
}
}
Root element:
<produce bar="{{bar}}"></producer>
<consumer data="[[bar]]"></consumer>
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.
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.