Sdk: js-interop: Support for `external Map` (JsObject -> Map)

Created on 22 Dec 2016  路  17Comments  路  Source: dart-lang/sdk

Describe the issue you're seeing

The following API is not supported. Instead of a Map-type, you get back a JsObjectImpl, which while not a Map, also does not have index access ['hello'], so you have to really dig in and use package:js/js_util.dart#getProperty to get around this.

function getData() {
  return { 'hello': 'world' };
}
@JS()
external Map<String, String> getData();

Does it happen in Dartium or when compiled to JavaScript?

Dartium and dart2js (Did not try DDC here)

  • Dart SDK version: use dart --version

Dart VM version: 1.22.0-dev.0.0 (Wed Dec 7 09:15:00 2016) on "macos_x64"

  • pkg/js version: look at pubspec.lock

0.6.1

Will attach failing code in a second.

type-enhancement web-js-interop

Most helpful comment

I gave the user a workaround that involves calling Object.keys and creating a new Map. But obviously if the underlying object changes, most users will expect/want the Map to be updated.

I realize this is tricky since the whole Map interface will likely not map well to a a JS object.

Some workaround ideas (might be too much to implement in Dartium, just dart2js/ddc?):

  1. Allow a subset of the Map interface to be used
  2. Create a new interface JsObjectMap:
/// Represents a plain object in JavaScript accessed like it would be in JavaScript.
///
/// Not all methods are available, use `toDartMap` to _convert_ to a full Map interface.
abstract class JsObjectMap implements Map {
  ...
}

All 17 comments

I gave the user a workaround that involves calling Object.keys and creating a new Map. But obviously if the underlying object changes, most users will expect/want the Map to be updated.

I realize this is tricky since the whole Map interface will likely not map well to a a JS object.

Some workaround ideas (might be too much to implement in Dartium, just dart2js/ddc?):

  1. Allow a subset of the Map interface to be used
  2. Create a new interface JsObjectMap:
/// Represents a plain object in JavaScript accessed like it would be in JavaScript.
///
/// Not all methods are available, use `toDartMap` to _convert_ to a full Map interface.
abstract class JsObjectMap implements Map {
  ...
}

This could be a possible solution:

class JsMap {
  dynamic _jsObject;

  JsMap(this._jsObject);

  operator [](String key) => getProperty(_jsObject, key);

  operator []=(String key, value) {
    setProperty(_jsObject, key, value);
  }
}

Sure you get [] and []=, but I imagine most people want to iterate over the keys/values.

(I'd also want the implementation to be as "free" as possible, and wrapping every Map? Meh.)

Yeah. That snippet wouldn't allow access to nested objects either

Would it be feasible to make JSObjectImpl inherit from JsObject and not JSObject? The former has the [] and []= operators, while the latter does not and is marked as deprecated except for internal use.

However, interop calls have the potential to return JSObjectImpl in certain cases, exposing a (deprecated) JSObject to the user in a way that they cannot avoid. I think having at least some option available to users of the library for returning a plain JavaScript object (JsObject?) in place of a typed Map, List, etc. is essential, because the built-in dart datastructures cannot feasibly cover all possibilities for JavaScript objects.

I'll admit I'm a bit out of my depth here so apologies if this is not feasible or already exists.

@ahirschberg I think that might be reasonable, but I'll wait for after the holiday break and see what the owners of the pkg/js jazz want to do - there might be a good reason, it might just be oversight. I think the jsToMap hack I introduced above will probably satisfy for now, I hope.

You can create an adapter to handle a JSObject as a Map.

import 'dart:collection';

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

class JsMap extends MapMixin<String, dynamic> {
  @JS('Object.keys')
  external static List<String> _getKeys(jsObject);

  var _jsObject;

  JsMap(this._jsObject);

  @override
  dynamic operator [](Object key) => getProperty(_jsObject, key);

  @override
  operator []=(String key, dynamic value) => setProperty(_jsObject, key, value);

  @override
  remove(Object key) {
    final value = this[key];
    deleteProperty(_jsObject, key);
    return value;
  }

  @override
  Iterable<String> get keys => _getKeys(_jsObject);

  @override
  bool containsKey(Object key) => hasProperty(_jsObject, key);

  @override
  void clear() => Maps.clear(this);
}

This snippet almost works. Only a deleteProperty method is missing in js/js_util.dart and I don't think it would be hard to add. And even without this change in the Dart SDK you can work around by adding a js function function deleteProperty(o, k) { delete o[k]; } and use it with @JS() external static void deleteProperty(o, k);.
Hope that help.

@a14n this would be fine for a simple object but a nested object would still be a JsObjectImpl

@kulshekhar, I've adapted @a14n's code to fix the nesting problem although I haven't tested it too heavily. It seems to work for my purposes though. I added an optional generic type to the JsMap and automatically wrap the []'s return value in a JsMap if the generic is not provided. See my code here:

JsMap with adapted [] operator and my usage example (with typing).

The problem is that making JSObject implement Map significantly hurts Dart2JS type inference as measured using the Swarm test application.
If Dart2JS starts to take advantage of strong mode types more I would expect we can make JSObject implement Map.

@sigmundch

@jacob314 I don't think we _have_ to make it implement, rather, it would be nice if the user types something as Map to coerce it into some interceptor-based Map like outlined above. The bigger issue here is JsObject v JsObjectImpl and not exposing the [] operators

+1 Same problem here. For now the workaround is ok but I also have nested objects and would love a native solution.

I realize this was a silly request ~a year later for performance reasons.

@matanlurey The intent of this request was certainly good, though. Without defining weird custom types, it's presently very difficult to interface with JavaScript objects that don't adhere to a prototype. Personally, I'm trying to write some JS-interop for a particular API, and lacking a way to access JS objects with a Map-like interface is proving to be extremely challenging.

JSObject != Map, and trying to make them pretend to be the same is not a good practice.

In fact, newer versions of JavaScript/EcmaScript make it possible to hide keys from iteration, so it's not a great thing to standardize on. If you need raw DOM access, I'd recommend just defining @JS() annotated classes:

@JS()
library js_interop;

import 'package:js/js.dart';

@JS()
@anonymous
abstract class Custom {
  external String get name;
}

If you really just want to poke at untyped interfaces, you can create something simple:

import 'package:js/js_util.dart' as js;

class JsObject {
  final dynamic _object;

  const JsObject(this._object);

  dynamic operator[](String name) => js.getProperty(_object, name);
  operator[]=(String name, dynamic value) => js.setProperty(_object, name, value);
}

I absolutely agree that JS objects are not maps. When dealing with untyped interfaces that simply store objects rather than maintaining a prototype, something like your JsObject class is very necessary, though. It would be great if we could get access to a nice interface like JsObject (that is similar in functionality to a Map) without having to manually wrap the object in question.

Unfortunately Dart calling conventions != JS, so it would be very hard to do this for free.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

bergwerf picture bergwerf  路  3Comments

ranquild picture ranquild  路  3Comments

DartBot picture DartBot  路  3Comments

DartBot picture DartBot  路  3Comments

matanlurey picture matanlurey  路  3Comments