angulardart-community / angular

Fast and productive web framework provided by Dart.
https://pub.dev/packages/ngdart
MIT License
110 stars 14 forks source link

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

Open insinfo opened 9 months ago

insinfo commented 9 months ago

@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 commented 9 months ago

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 commented 9 months ago

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 commented 9 months ago

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 commented 9 months ago

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

GZGavinZhao commented 9 months ago

@insinfo Have you tried this?

ykmnkmi commented 9 months ago

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