Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

it seems that change detection doesn't work in javascript callbacks inside allowInterop #69

Open
insinfo opened this issue Jan 3, 2024 · 6 comments
Labels
bug Something isn't working

Comments

@insinfo
Copy link

insinfo commented Jan 3, 2024

@GZGavinZhao @ykmnkmi

Which ng* package(s) are the source of the bug?

ngdart

Which operating system(s) does this bug appear on?

Windows

Which browser(s) does this bug appear on?

Chrome 117.0.5938.132 64 bits

Is this a regression?

No

Description

I'm using allowInterop to interact with JavaScript code, more specifically I'm creating an AngularDart application that uses Fabric.js to create a Badge Creator (Professional Employee ID Card), so I'm having a problem because it seems that AngularDart doesn't detect changes in variables What do I change inside allowInterop

Please provide the steps to reproduce the bug

 void initFabric() {
    canvas = Canvas(canvasEl);
    stage = Rect(
      jsify({
        'left':
            (canvas.width / 2) - ((currentWidth * (stage?.scaleX ?? 1)) / 2),
        'top': 50,
        'width': currentWidth,
        'height': currentHeight,
        'fill': '#fff',
        'lockMovementY': true,
        'lockMovementX': true,
        'selectable': false,
        'hoverCursor': 'default',
      }),
    );

    canvas.add(stage);

    num zoom = 1;
    canvas.on('mouse:wheel', allowInterop((opt) {
      final deltaY = opt.e.deltaY;   
      final mousePoint = canvas.getPointer(opt.e, true);
      zoom = canvas.getZoom();
      zoom *= pow(0.999, deltaY);
      if (zoom > 20) zoom = 20;
      if (zoom < 0.01) zoom = 0.01;    
      canvas.zoomToPoint(Point(mousePoint.x, mousePoint.y), zoom);
      opt.e.preventDefault();
      opt.e.stopPropagation();
    }));

    canvas.on('selection:created', allowInterop((obj) {
  //change detection doesn't work
      onSelectObject(obj);
    }));
    canvas.on('selection:updated', allowInterop((obj) {
  //change detection doesn't work
      onSelectObject(obj);
    }));

    canvas.on('selection:cleared', allowInterop((event) {
      //change detection doesn't work
      onUnSelectObject(event);
    }));
   
  }

  void onUnSelectObject(event) {
    //change current width of stage (valid drawing area)
 //change detection doesn't work
    currentWidth = stage!.getScaledWidth();
    currentHeight = stage!.getScaledHeight();
  }

  void onSelectObject(event) {
 //change detection doesn't work
    final activeObj = canvas.getActiveObject();
    final scaledWidth = activeObj.getScaledWidth();
    final scaledHeight = activeObj.getScaledHeight();

    currentWidth = scaledWidth;
    currentHeight = scaledHeight;

  }

Please provide the exception or error you saw

.

Please provide the dependency environment you discovered this bug in (run dart pub deps -s compact)

Dart SDK version: 3.2.1 (stable) (Wed Nov 22 08:59:13 2023 +0000) on "windows_x64"

Anything else?

No response

@insinfo
Copy link
Author

insinfo commented Jan 3, 2024

I just saw that change detection works if I call _changeDetectorRef.detectChanges();

import 'dart:async';
import 'dart:html' as html;

import 'dart:js_util';
import 'dart:math';
import 'package:ngdart/angular.dart';
import 'package:rava_frontend/src/shared/directives/value_accessors/custom_form_directives.dart';
// ignore: unused_import
import 'package:rava_frontend/src/shared/js_interop/bootstrap_interop.dart';

import 'package:rava_frontend/src/shared/js_interop/fabric_interop.dart';
import 'package:rava_frontend/src/shared/utils/flatcolor.dart';

class CustomSize {
  num width;
  num height;
  CustomSize({required this.width, required this.height});
}

@Component(
  selector: 'cracha-editor-comp',
  templateUrl: 'cracha_editor_page.html',
  styleUrls: ['cracha_editor_page.css'],
  directives: [
    routerDirectives,
    formDirectives,
    //customFormDirectives,
  ],
)
class CrachaEditorPage
    implements OnInit, AfterContentInit, AfterViewInit, OnDestroy {
  @ViewChild('sidebar')
  html.HtmlElement? sidebarElement;

  // ignore: unused_field
  final ChangeDetectorRef _changeDetectorRef;

  @ViewChild('viewport')
  html.DivElement? viewport;

  @ViewChild('viewportInner')
  html.DivElement? viewportInner;

  @ViewChild('canvasEl')
  html.CanvasElement? canvasEl;

  final html.Element nativeElement;

  late Canvas canvas;
  Rect? stage;

  /// tamanho da area de desenho (Prancheta)
  // double currentWidth = 637;
  // double currentHeight = 1012;

  final CustomSize currentSize = CustomSize(width: 637, height: 1012);

  CrachaEditorPage(this.nativeElement, this._changeDetectorRef);
  StreamSubscription? ssOnResize;

  @override
  void ngOnInit() {
    ssOnResize = html.window.onResize.listen(onResize);
  }

  @override
  void ngAfterContentInit() {}

  @override
  void ngAfterViewInit() async {
    //https://developer.mozilla.org/en-US/docs/Web/API/CSS_Object_Model/Determining_the_dimensions_of_elements
    //canvasEl.width = nativeElement.
    //html.window.getComputedStyleMap();
    await Future.delayed(Duration(milliseconds: 20));
    // final comStyle = viewportInner!.getComputedStyle();
    // print('height: ${comStyle.height}');
    // print('width: ${comStyle.width}');

    // final rect = viewportInner!.getBoundingClientRect();
    // print('height: ${rect.height}');
    // print('width: ${rect.width}');
    // var width = viewportInner!.offsetWidth;
    // var height = viewportInner!.offsetHeight;
    setElementCanvasSize();
    initFabric();
  }

  void onResize(e) {
    // setCanvasSize();
  }

  void updateSize() {
    final activeObj = canvas.getActiveObject();
    // print('updateSize ${activeObj == null}');
    // consoleLog(activeObj);
    // print(activeObj);
    if (activeObj == null) {
      stage?.set('width', currentSize.width);
      stage?.set('height', currentSize.height);
    } else {
      activeObj.set('width', currentSize.width);
      activeObj.set('height', currentSize.height);
    }

    canvas.renderAll();
  }

  void setElementCanvasSize() {
    final ele = viewportInner!;
    // print('clientHeight: ${ele.clientHeight}');
    // print('clientWidth: ${ele.clientWidth}');
    canvasEl!.height = ele.clientHeight;
    canvasEl!.width = ele.clientWidth;
  }

  void initFabric() {
    canvas = Canvas(canvasEl);
    stage = Rect(
      jsify({
        'left': (canvas.width / 2) -
            ((currentSize.width * (stage?.scaleX ?? 1)) / 2),
        'top': 50,
        'width': currentSize.width,
        'height': currentSize.height,
        'fill': '#fff',
        'lockMovementY': true,
        'lockMovementX': true,
        'selectable': false,
        'hoverCursor': 'default',
      }),
    );

    canvas.add(stage);

    num zoom = 1;

    canvas.on('mouse:wheel', allowInterop((opt) {
      final deltaY = opt.e.deltaY;
      //var deltaX = opt.e.deltaX;
      final mousePoint = canvas.getPointer(opt.e, true);
      zoom = canvas.getZoom();
      zoom *= pow(0.999, deltaY);
      if (zoom > 20) zoom = 20;
      if (zoom < 0.01) zoom = 0.01;
      //min(max(zoom + (deltaY/150), .5), 6)
      //canvas.setZoom(zoom);
      canvas.zoomToPoint(Point(mousePoint.x, mousePoint.y), zoom);
      opt.e.preventDefault();
      opt.e.stopPropagation();
    }));

    canvas.on('selection:created', allowInterop((obj) {
      onSelectObject(obj);
    }));
    canvas.on('selection:updated', allowInterop((obj) {
      onSelectObject(obj);
    }));

    canvas.on('selection:cleared', allowInterop((event) {
      //change detection doesn't work
      onUnSelectObject(event);
    }));
  }

  void onUnSelectObject(event) {
    //change current width of stage (valid drawing area)
    currentSize.width = stage!.getScaledWidth().toDouble();
    currentSize.height = stage!.getScaledHeight().toDouble();
    _changeDetectorRef.detectChanges();
  }

  void onSelectObject(event) {
    final activeObj = canvas.getActiveObject();
    final scaledWidth = activeObj.getScaledWidth();
    final scaledHeight = activeObj.getScaledHeight();

    currentSize.width = scaledWidth;
    currentSize.height = scaledHeight;
 
    _changeDetectorRef.detectChanges();
  }

  void addObject(String name) {
    switch (name) {
      case 'rect':
        var rect = Rect(
          jsify({
            'left': (canvas.width / 2) - ((100 * (1)) / 2),
            'top': 50,
            'width': 100,
            'height': 100,
            'fill': FlatColor().generateHex2(),
          }),
        );
        canvas.add(rect);
        break;
      default:
    }
  }

  //https://www.riodasostras.rj.gov.br/cdn/Vendor/limitless/4.0/bs5/template/html/layout_1/full/assets/js/app.js
  // Toggle component sidebar
  void sidebarComponentToggle(e) {
    e.preventDefault();
    sidebarElement?.classes.toggle('sidebar-mobile-expanded');
  }

  @override
  void ngOnDestroy() {
    ssOnResize?.cancel();
  }
}

@insinfo
Copy link
Author

insinfo commented Jan 3, 2024

fabric_interop.dart

@JS()
library fabric;

import 'dart:html';
import 'package:js/js.dart';



/// canvas = new fabric.Canvas(this.htmlCanvas.nativeElement, {
///   hoverCursor: 'pointer',
///   selection: true,
///   selectionBorderColor: 'blue',
///   isDrawingMode: true
/// });
@JS('fabric.Canvas')
class Canvas {
  external Canvas(Element? element, [config]);
  external add(dynamic element);
  external renderAll();
  external on(String event, dynamic func);
  external Point getPointer(Event e, [bool ignoreZoom]);

  external num getZoom();
  external Canvas zoomToPoint(Point point, num value);
  external Canvas setZoom(num value);
  external dynamic /*Object|Null*/ getActiveObject();
  external get width;
  external get height;
}

@anonymous
@JS()
abstract class IObjectOptions {
  external num get width;
  external set width(num v);

  external num get height;
  external set height(num v);

  external num get scaleX;

  /// rect.set('fill', 'red');
  /// rect.set({ strokeWidth: 5, stroke: 'rgba(100,200,200,0.5)' });
  /// rect.set('angle', 15).set('flipY', true);
  external set(dynamic propNameOrMap, dynamic val);

  external scaleToWidth(dynamic val);
  external scaleToHeight(dynamic val);

  external num getScaledWidth();
  external num getScaledHeight();
}

@JS('fabric.Rect')
class Rect extends IObjectOptions {
  external Rect(options);
}

@JS('fabric.Circle')
class Circle extends IObjectOptions {
  external Circle(options);
}

@JS('fabric.Triangle')
class Triangle extends IObjectOptions {
  external Triangle(options);
}

@JS("fabric.Point")
class Point {
  external num x;
  external num y;
  external factory Point(num x, num y);
}

@insinfo
Copy link
Author

insinfo commented Jan 3, 2024

The temporary solution is to use detectChanges() or create a variable member of the component to obtain the custom angular zone and use this zone (Zone.run) when changing a property linked to the template within a function called by javascript

// ignore_for_file: deprecated_member_use

import 'dart:async';
import 'dart:html' as html;

import 'dart:js_util';
import 'dart:math';
import 'package:ngdart/angular.dart';
import 'package:rava_frontend/src/shared/directives/value_accessors/custom_form_directives.dart';
// ignore: unused_import
import 'package:rava_frontend/src/shared/js_interop/bootstrap_interop.dart';

import 'package:rava_frontend/src/shared/js_interop/fabric_interop.dart';
import 'package:rava_frontend/src/shared/utils/flatcolor.dart';

class CustomSize {
  num width;
  num height;
  CustomSize({required this.width, required this.height});
}

@Component(
  selector: 'cracha-editor-comp',
  templateUrl: 'cracha_editor_page.html',
  styleUrls: ['cracha_editor_page.css'],
  directives: [
    routerDirectives,
    formDirectives,
    //customFormDirectives,
  ],
)
class CrachaEditorPage
    implements OnInit, AfterContentInit, AfterViewInit, OnDestroy {
  @ViewChild('sidebar')
  html.HtmlElement? sidebarElement;

  // ignore: unused_field
  final ChangeDetectorRef _changeDetectorRef;

  @ViewChild('viewport')
  html.DivElement? viewport;

  @ViewChild('viewportInner')
  html.DivElement? viewportInner;

  @ViewChild('canvasEl')
  html.CanvasElement? canvasEl;

  final html.Element nativeElement;

  late Canvas canvas;
  Rect? stage;
  Zone angularZone = Zone.current;

  /// tamanho da area de desenho (Prancheta)
  // double currentWidth = 637;
  // double currentHeight = 1012;

  final CustomSize currentSize = CustomSize(width: 637, height: 1012);

  CrachaEditorPage(this.nativeElement, this._changeDetectorRef);
  StreamSubscription? ssOnResize;

  @override
  void ngOnInit() {
    ssOnResize = html.window.onResize.listen(onResize);
  }

  @override
  void ngAfterContentInit() {}

  @override
  void ngAfterViewInit() async {
    //https://developer.mozilla.org/en-US/docs/Web/API/CSS_Object_Model/Determining_the_dimensions_of_elements
    //canvasEl.width = nativeElement.
    //html.window.getComputedStyleMap();
    await Future.delayed(Duration(milliseconds: 20));
    // final comStyle = viewportInner!.getComputedStyle();
    // print('height: ${comStyle.height}');
    // print('width: ${comStyle.width}');

    // final rect = viewportInner!.getBoundingClientRect();
    // print('height: ${rect.height}');
    // print('width: ${rect.width}');
    // var width = viewportInner!.offsetWidth;
    // var height = viewportInner!.offsetHeight;
    setElementCanvasSize();
    initFabric();
  }

  void onResize(e) {
    // setCanvasSize();
  }

  void updateSize() {
    final activeObj = canvas.getActiveObject();
    // print('updateSize ${activeObj == null}');
    // consoleLog(activeObj);
    // print(activeObj);
    if (activeObj == null) {
      stage?.set('width', currentSize.width);
      stage?.set('height', currentSize.height);
    } else {
      activeObj.scaleToWidth(currentSize.width);
      activeObj.scaleToHeight(currentSize.height);
      //  img.scaleToWidth(canvas.width / 2);
    }

    canvas.renderAll();
  }

  void setElementCanvasSize() {
    final ele = viewportInner!;
    // print('clientHeight: ${ele.clientHeight}');
    // print('clientWidth: ${ele.clientWidth}');
    canvasEl!.height = ele.clientHeight;
    canvasEl!.width = ele.clientWidth;
  }

  void initFabric() {
   
    canvas = Canvas(canvasEl);
    stage = Rect(
      jsify({
        'left': (canvas.width / 2) -
            ((currentSize.width * (stage?.scaleX ?? 1)) / 2),
        'top': 50,
        'width': currentSize.width,
        'height': currentSize.height,
        'fill': '#fff',
        'lockMovementY': true,
        'lockMovementX': true,
        'selectable': false,
        'hoverCursor': 'default',
      }),
    );

    canvas.add(stage);

    num zoom = 1;

    canvas.on('mouse:wheel', allowInterop((opt) {
      final deltaY = opt.e.deltaY;
      //var deltaX = opt.e.deltaX;
      final mousePoint = canvas.getPointer(opt.e, true);
      zoom = canvas.getZoom();
      zoom *= pow(0.999, deltaY);
      if (zoom > 20) zoom = 20;
      if (zoom < 0.01) zoom = 0.01;
      //min(max(zoom + (deltaY/150), .5), 6)
      //canvas.setZoom(zoom);
      canvas.zoomToPoint(Point(mousePoint.x, mousePoint.y), zoom);
      opt.e.preventDefault();
      opt.e.stopPropagation();
    }));

    canvas.on('selection:created', allowInterop((obj) {
      onSelectObject(obj);
    }));
    canvas.on('selection:updated', allowInterop((obj) {
      onSelectObject(obj);
    }));

    canvas.on('selection:cleared', allowInterop((event) {
      onUnSelectObject(event);
    }));
    canvas.on('object:modified', allowInterop((e) => onUnSelectObject(e)));
  }

  void onUnSelectObject(event) {
    //change current width of stage (valid drawing area)
    currentSize.width = stage!.width;
    currentSize.height = stage!.height;
    _changeDetectorRef.detectChanges();
  }

  void onModifiedObject(event) {
    print('onModifiedObject $event');
    consoleLog(event);
  }

  void onSelectObject(event) {
   // print('onSelectObject Zone.current ${Zone.current} | angularZone: ${angularZone}');
    final activeObj = canvas.getActiveObject();
    final scaledWidth = activeObj.getScaledWidth();
    final scaledHeight = activeObj.getScaledHeight();
    //execute code on Zone of Angular
    angularZone.run(() {
      currentSize.width = scaledWidth;
      currentSize.height = scaledHeight;
    });

    //_changeDetectorRef.detectChanges();
  }

  void addObject(String name) {
    switch (name) {
      case 'rect':
        final item = Rect(
          jsify({
            'left': (canvas.width / 2) - ((100 * (1)) / 2),
            'top': 50,
            'width': 100,
            'height': 100,
            'fill': FlatColor().generateHex2(),
          }),
        );
        canvas.add(item);
        break;
      case 'circle':
        final item = Circle(
          jsify({
            'left': (canvas.width / 2) - ((100 * (1)) / 2),
            'top': 100,
            'radius': 50,
            'fill': FlatColor().generateHex2(),
          }),
        );
        canvas.add(item);
        break;
      default:
    }
  }

  //https://www.riodasostras.rj.gov.br/cdn/Vendor/limitless/4.0/bs5/template/html/layout_1/full/assets/js/app.js
  // Toggle component sidebar
  void sidebarComponentToggle(e) {
    e.preventDefault();
    sidebarElement?.classes.toggle('sidebar-mobile-expanded');
  }

  @override
  void ngOnDestroy() {
    ssOnResize?.cancel();
  }
}

@insinfo
Copy link
Author

insinfo commented Jan 5, 2024

@GZGavinZhao @ykmnkmi
I think the best solution would be to have an option to disable the use of Zones in AngularDart

@GZGavinZhao
Copy link

@insinfo Have you tried this?

@ykmnkmi
Copy link

ykmnkmi commented Jan 5, 2024

You can't call ChangeDetectorRef.markForCheck() outside Angular. Try to wrap callbacks in NgZone.run(), example.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

3 participants