Reputation: 3457
For instance I am running a project with D3.js, importing specific modules and calling on their functions.
Setup:
I am having an object, in this case an angular directive, and drawing some circles onto a SVG canvas and want them to trigger a function on a drag event.
Reduced code snippet: Please have a look on drawPoints()
at the bottom of this snippet
import { ElementRef, HostListener, Output, EventEmitter, OnInit, Input, OnChanges, SimpleChanges, Directive } from '@angular/core';
import * as Selection from 'd3-selection';
import * as Shape from 'd3-shape';
import * as Random from 'd3-random';
import * as Drag from 'd3-drag';
import { Config } from './../models/config.model';
import { Point } from './../models/point.model';
import { Param } from './../models/param.model';
@Directive({
selector: '[appCanvas]'
})
export class CanvasDirective implements OnInit, OnChanges {
private canvas: any;
private defs: any;
private gradient: any;
private svg: any;
private expandedPoints: Point[];
private drag: Point;
public config: Config;
@Input()
private param: Param;
@Output()
private emitConfig: EventEmitter<Config>;
@HostListener('window:resize', ['$event'])
private onResize(event) {
this.init();
}
constructor(
private el: ElementRef
) {
this.canvas = el.nativeElement;
this.emitConfig = new EventEmitter();
}
ngOnInit() {
intSvg();
// ..
}
private initSvg() {
if (!this.svg) {
this.svg = Selection.select(this.canvas).append('svg');
}
this.svg
.attr('width', this.config.width)
.attr('height', this.config.height);
}
private drawPoints(points: Point[]) {
points.forEach(point => {
this.svg.append('circle')
.attr('r', point.color ? 20 : 10)
.attr('cx', point.x)
.attr('cy', point.y)
.attr('fill', point.color ? point.color : 'lightgray')
.attr('stroke-width', !point.color ? 2 : 0)
.attr('stroke', !point.color ? 'gray' : '')
.call(Drag.drag()
.on('drag', () => {
// What to call here?
// Selection.select(this) will not work
// So how to target the correct „this“?
}));
});
}
// ...
}
The problem that occures is not beeing able to reach the correct this
inside the drag function of the appended circles.
Multiple examples are out there, but they won't work inside the class, because the this
argument is protected.
Credits to Mike Bostock's example https://bl.ocks.org/mbostock/22994cc97fefaeede0d861e6815a847e
Upvotes: 4
Views: 220
Reputation: 222369
Older libraries like D3 rely on dynamic this
context instead of passing all necessary data as arguments and require to use const self = this
trick to reach lexical this
in callbacks. The trick is considered obsolete in ES6 but is necessary in such cases. Regular function needs to be used instead of an arrow in order to get dynamic context:
private drawPoints(points: Point[]) {
const self = this;
...
.call(Drag.drag()
.on('drag', function (this: ProperContextTypeIfNecessary) {
Selection.select(this);
// class instance can be referred as `self`
}));
});
It may seem inconsistent that class instance should be referred as this
in one places and self
in another ones (this wasn't a problem in ES5 because self
was supposed to be thoroughly used in such cases for consistency).
As explained in this related answer, an alternative is wrapper function that will provide callback function D3 context as a parameter, while this
may still refer to class instance. Arrow functions may be used in this case:
function contextWrapper(fn) {
const self = this;
return function (...args) {
return fn.call(self, this, ...args);
}
}
...
private drawPoints(points: Point[]) {
const self = this;
...
.call(Drag.drag()
.on('drag', contextWrapper((d3Context: ProperContextTypeIfNecessary) => {
Selection.select(d3Context);
// class instance can be referred as `this`
}));
});
Upvotes: 2
Reputation: 3457
By avoiding the popular arrow function syntax in ES6 components and using the good old function() {}
syntax instead, the problem was solved. The arrow functions change the game, because within these called functions it turns out that this
is not reduced to the called context of the function, but that the context extends globally till the class level.
Solution:
private drawPoints(points: Point[]) {
points.forEach(point => {
this.svg.append('circle')
.attr('r', point.color ? 20 : 10)
.attr('cx', point.x)
.attr('cy', point.y)
.attr('fill', point.color ? point.color : 'lightgray')
.attr('stroke-width', !point.color ? 2 : 0)
.attr('stroke', !point.color ? 'gray' : '')
.call(Drag.drag()
.on('drag', function() {
console.log(this);
}));
});
}
Further reading:
[...] and ES2015 [ES6] introduced arrow functions which don't provide their own this binding (it retains the this value of the enclosing lexical context). rwaldron, abasao, martian2049 et. al, this, MDN web docs
Upvotes: 1