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 --versionDart VM version: 1.22.0-dev.0.0 (Wed Dec 7 09:15:00 2016) on "macos_x64"
pubspec.lock0.6.1
Will attach failing code in a second.
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?):
Map interface to be usedJsObjectMap:/// 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
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.
Most helpful comment
I gave the user a workaround that involves calling
Object.keysand creating a newMap. 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
Mapinterface will likely not map well to a a JS object.Some workaround ideas (might be too much to implement in Dartium, just dart2js/ddc?):
Mapinterface to be usedJsObjectMap: