From 17b188d608ea8a520e6d81d0f5e9878e3214e5d8 Mon Sep 17 00:00:00 2001 From: Marya <111139605+MaryaBelanger@users.noreply.github.com> Date: Thu, 15 Feb 2024 10:45:40 -0800 Subject: [PATCH] New JS interop docs (#4578) Fixes #5438 --------- Co-authored-by: Srujan Gaddam Co-authored-by: Parker Lougheed Co-authored-by: sigmundch --- examples/create_libraries/lib/hw_mp.dart | 2 +- firebase.json | 4 +- src/_data/side-nav.yml | 16 +- .../guides/libraries/create-packages.md | 10 +- src/content/guides/whats-new.md | 2 +- src/content/interop/js-interop.md | 123 ----- src/content/interop/js-interop/index.md | 44 ++ src/content/interop/js-interop/js-types.md | 255 ++++++++++ src/content/interop/js-interop/mock.md | 122 +++++ src/content/interop/js-interop/package-web.md | 306 ++++++++++++ .../interop/js-interop/past-js-interop.md | 123 +++++ src/content/interop/js-interop/tutorials.md | 18 + src/content/interop/js-interop/usage.md | 457 ++++++++++++++++++ src/content/libraries/dart-html.md | 10 + src/content/libraries/index.md | 2 +- src/content/resources/faq.md | 2 +- 16 files changed, 1361 insertions(+), 135 deletions(-) delete mode 100644 src/content/interop/js-interop.md create mode 100644 src/content/interop/js-interop/index.md create mode 100644 src/content/interop/js-interop/js-types.md create mode 100644 src/content/interop/js-interop/mock.md create mode 100644 src/content/interop/js-interop/package-web.md create mode 100644 src/content/interop/js-interop/past-js-interop.md create mode 100644 src/content/interop/js-interop/tutorials.md create mode 100644 src/content/interop/js-interop/usage.md diff --git a/examples/create_libraries/lib/hw_mp.dart b/examples/create_libraries/lib/hw_mp.dart index 2d5c2d84b2..176e03942b 100644 --- a/examples/create_libraries/lib/hw_mp.dart +++ b/examples/create_libraries/lib/hw_mp.dart @@ -4,4 +4,4 @@ library hw_mp; // #docregion export export 'src/hw_none.dart' // Stub implementation if (dart.library.io) 'src/hw_io.dart' // dart:io implementation - if (dart.library.html) 'src/hw_html.dart'; // dart:html implementation + if (dart.library.js_interop) 'src/hw_web.dart'; // package:web implementation diff --git a/firebase.json b/firebase.json index dee896c35b..f718a46920 100644 --- a/firebase.json +++ b/firebase.json @@ -172,11 +172,11 @@ { "source": "/go/non-promo-this", "destination": "/tools/non-promotion-reasons#this", "type": 301 }, { "source": "/go/non-promo-write", "destination": "/tools/non-promotion-reasons#write", "type": 301 }, - { "source": "/go/next-gen-js-interop", "destination": "/interop/js-interop#next-generation-js-interop-preview", "type": 301 }, + { "source": "/go/next-gen-js-interop", "destination": "/interop/js-interop", "type": 301 }, { "source": "/go/null-safety-migration", "destination": "/null-safety/migration-guide", "type": 301 }, { "source": "/go/package-discontinue", "destination": "/tools/pub/publishing#discontinue", "type": 301 }, { "source": "/go/package-retraction", "destination": "/tools/pub/publishing#retract", "type": 301 }, - { "source": "/go/package-web", "destination": "https://pub.dev/packages/web", "type": 301 }, + { "source": "/go/package-web", "destination": "/interop/js-interop/package-web", "type": 301 }, { "source": "/go/pub-cache", "destination": "/tools/pub/cmd/pub-cache", "type": 301 }, { "source": "/go/pubignore", "destination": "/tools/pub/publishing#what-files-are-published", "type": 301 }, { "source": "/go/publishing-from-github", "destination": "/tools/pub/automated-publishing#publishing-packages-using-github-actions", "type": 301 }, diff --git a/src/_data/side-nav.yml b/src/_data/side-nav.yml index 9c655c3183..c3a1d4fbb0 100644 --- a/src/_data/side-nav.yml +++ b/src/_data/side-nav.yml @@ -247,7 +247,21 @@ - title: Java & Kotlin interop permalink: /interop/java-interop - title: JavaScript interop - permalink: /interop/js-interop + children: + - title: Overview + permalink: /interop/js-interop + match-page-url-exactly: true + - title: Usage + permalink: /interop/js-interop/usage + - title: JS types + permalink: /interop/js-interop/js-types + - title: Tutorials + permalink: /interop/js-interop/tutorials + - title: Past JS interop + permalink: /interop/js-interop/past-js-interop + - divider + - title: Web interop + permalink: /interop/js-interop/package-web - title: Tools & techniques expanded: false diff --git a/src/content/guides/libraries/create-packages.md b/src/content/guides/libraries/create-packages.md index b1ab5799ce..a7f786135a 100644 --- a/src/content/guides/libraries/create-packages.md +++ b/src/content/guides/libraries/create-packages.md @@ -72,7 +72,7 @@ by importing a single file. The lib directory might also include other importable, non-src, libraries. For example, perhaps your main library works across platforms, but -you create separate libraries that rely on `dart:io` or `dart:html`. +you create separate libraries that rely on `dart:io` or `dart:js_interop`. Some packages have separate libraries that are meant to be imported with a prefix, when the main library is not. @@ -151,13 +151,13 @@ A common use case is a library that supports both web and native platforms. To conditionally import or export, you need to check for the presence of `dart:*` libraries. Here's an example of conditional export code that -checks for the presence of `dart:io` and `dart:html`: +checks for the presence of `dart:io` and `dart:js_interop`: ```dart title="lib/hw_mp.dart" export 'src/hw_none.dart' // Stub implementation if (dart.library.io) 'src/hw_io.dart' // dart:io implementation - if (dart.library.html) 'src/hw_html.dart'; // dart:html implementation + if (dart.library.js_interop) 'src/hw_web.dart'; // package:web implementation ``` Here's what that code does: @@ -165,9 +165,9 @@ Here's what that code does: * In an app that can use `dart:io` (for example, a command-line app), export `src/hw_io.dart`. -* In an app that can use `dart:html` +* In an app that can use `dart:js_interop` (a web app), - export `src/hw_html.dart`. + export `src/hw_web.dart`. * Otherwise, export `src/hw_none.dart`. To conditionally import a file, use the same code as above, diff --git a/src/content/guides/whats-new.md b/src/content/guides/whats-new.md index 1f28ddfe15..c414bc9cf1 100644 --- a/src/content/guides/whats-new.md +++ b/src/content/guides/whats-new.md @@ -219,7 +219,7 @@ we made the following changes: [source descriptor]: /tools/pub/cmd/pub-add#source-descriptor [SDK archive]: /get-dart/archive [glossary]: /resources/glossary -[JS static interop support]: /interop/js-interop#next-generation-js-interop-preview +[JS static interop support]: /interop/js-interop [analyzer plugins]: /tools/analysis#plugins ### Articles added to the Dart blog {:.no_toc} diff --git a/src/content/interop/js-interop.md b/src/content/interop/js-interop.md deleted file mode 100644 index c19c5a6591..0000000000 --- a/src/content/interop/js-interop.md +++ /dev/null @@ -1,123 +0,0 @@ ---- -title: JavaScript interoperability -short-title: JS interop -description: "Use package:js to integrate JavaScript code into your Dart web app." ---- - -The [Dart web platform](/overview#web-platform) supports calling -JavaScript using the `js` package, -also known as _package:js_. - -For help using the `js` package, see the following: - -* Documentation for the `js` package: - * [pub.dev site page][js] - * [API reference][js-api] -* Packages that use the `js` package: - * [sass][] is an example of a more unusual use case: providing a - way for JavaScript code to call Dart code. - -[js]: {{site.pub-pkg}}/js -[js-api]: {{site.pub-api}}/js -[sass]: {{site.pub-pkg}}/sass - -## Next-generation JS interop preview - -:::note -This interop feature is **experimental**, -and [in active development](https://github.com/dart-lang/sdk/issues/35084). -::: - -Dart's JS interop story is currently evolving. -Many of the features that enable future JS interop -are ready to experiment with as of Dart version 3.2. -These features support the existing production -and development web compilers, -as well as Dart's in-progress Wasm compiler ([`dart2wasm`][]). - -For a glimpse into the next generation of JS interop, -you can refactor your code to conform to -the new syntax and semantics now. -Doing so will likely not prevent the need to refactor again -once these features stabilize, as the features are still in development. -However, the features available for preview are much closer -to future JS interop than any pattern supported today. -So, there are a few reasons to try them out now: - -* New JS interop developers can learn and build with future JS interop, - so they won't have to unlearn obsolete patterns in a few months. -* Existing JS interop developers eager to experiment with - the latest features in JS interop - or with `dart2wasm` when it becomes available. -* Potentially ease transition of existing JS interop code - once migration becomes necessary. - -The following sections are the set of features -expected to work across compilers for JS interop. - -*Requirements:* - -* Dart SDK constraint: `>= 3.2` - -[`dart2wasm`]: https://github.com/dart-lang/sdk/blob/main/pkg/dart2wasm#running-dart2wasm - -### `dart:js_interop` - -The key feature of next-generation JS interop is static interop. -We recommend using static interop through [`dart:js_interop`][] -as the default choice for interoping with JavaScript. -It is more declarative and explicit, more likely to be optimized, -more likely to perform better, and required for `dart2wasm`. -Static interop addresses several gaps in the existing JS interop story: - -* **Missing features:** Static interop enables previously - unavailable features, like easily wrapping and transforming APIs, - renaming members, and static checking. - -* **Inconsistencies:** Static interop makes backends more consistent, - so development and production web compilers - won't behave as differently as before. - -* **Clarity:** Static interop takes a step towards making - JS interop more idiomatic in Dart, - and making the boundary between the two languages more visible. - For example, it enforces that JS classes are not meant to be mixed with Dart - (dynamic calls aren't allowed, - and JS interop types can't be implemented by a Dart class). - -You can implement static interop using -the `dart:js_interop` annotation [`@staticInterop`][]. -The set of features for future static interop currently includes: - -* `@staticInterop` interfaces - * External factory constructors with and without `@anonymous` - * External `static` class members - * External non-`static` extension members on a `@staticInterop` - class (fields, getters, setters, methods) - * Non-external extension members on a `@staticInterop` class -* Top-level external members -* [`@JSExport`][] for mocking and exports - -For examples that showcase how to use static interop, -check out the [implementation of `package:web`][package-web], -which provides bindings to browser APIs using static interop. - -[`@staticInterop`]: {{site.dart-api}}/dart-js_interop/staticInterop-constant.html -[`dart:js_interop`]: {{site.dart-api}}/{{site.sdkInfo.channel}}/dart-js_interop/dart-js_interop-library.html -[`@JSExport`]: {{site.pub-pkg}}/dart-js_interop/JSExport-class.html -[package-web]: https://github.com/dart-lang/web - -### `dart:js_interop_unsafe` - -[`dart:js_interop_unsafe`][] provides low-level interop API -and is supported by the JS and `dart2wasm` backends. -`dart:js_interop_unsafe` can provide more flexibility, -for example, in potential, rare edge cases we haven't yet -accounted for where static interop is not expressive enough. - -However, it is not as ergonomic, and we do not plan -to optimize it in the same way as static interop. -As a result, we highly recommend using static interop over -`dart:js_interop_unsafe` whenever it's possible. - -[`dart:js_interop_unsafe`]: {{site.dart-api}}/{{site.sdkInfo.channel}}/dart-js_interop_unsafe/dart-js_interop_unsafe-library.html diff --git a/src/content/interop/js-interop/index.md b/src/content/interop/js-interop/index.md new file mode 100644 index 0000000000..bc8115ec6d --- /dev/null +++ b/src/content/interop/js-interop/index.md @@ -0,0 +1,44 @@ +--- +title: JavaScript interoperability +short-title: JS interop +description: Integrate JavaScript code into your Dart web app. +--- + +The [Dart web platform](/overview#web-platform) supports communication with +JavaScript apps and libraries, as well as browser APIs, using `dart:js_interop`. + +Web developers can benefit from using external JS libraries in their Dart code, +without having to rewrite anything in Dart. + +{% comment %} +## Why use JS interop? + +TODO: Should we have a paragraph here explaining the benefits of using interop? +{% endcomment %} + +## Overview + +For information on how to write and use JavaScript interop: + * [Usage reference] + * [JS types reference] + +For information on interacting with web APIs: + * [`package:web` and migration] + +For tutorials and help: + * [How to mock JavaScript interop objects] + +For information on previous JavaScript interop libraries: + * [Past JS interop] + +For additional documentation on JavaScript interop: + * [`dart:js_interop` API reference] + * [`dart:js_interop_unsafe` API reference] + +[Usage reference]: /interop/js-interop/usage +[JS types reference]: /interop/js-interop/js-types +[`package:web` and migration]: /interop/js-interop/package-web +[How to mock JavaScript interop objects]: /interop/js-interop/mock +[Past JS interop]: /interop/js-interop/past-js-interop +[`dart:js_interop` API reference]: https://api.dart.dev/dev/dart-js_interop/dart-js_interop-library.html +[`dart:js_interop_unsafe` API reference]: https://api.dart.dev/dev/dart-js_interop_unsafe/dart-js_interop_unsafe-library.html diff --git a/src/content/interop/js-interop/js-types.md b/src/content/interop/js-interop/js-types.md new file mode 100644 index 0000000000..2d89deb661 --- /dev/null +++ b/src/content/interop/js-interop/js-types.md @@ -0,0 +1,255 @@ +--- +title: JS types +description: Usage information about the core types in JS interop. +--- + +Dart values and JS values belong to separate language domains. When compiling to +Wasm, they execute in separate *runtimes* as well. As such, you should treat JS +values as foreign types. To provide Dart types for JS values, +[`dart:js_interop`] exposes a set of types prefixed with `JS` called "JS types". +These types are used to distinguish between Dart values and JS values at +compile-time. + +Importantly, these types are reified differently based on whether you compile to +Wasm or JS. This means that their runtime type will differ, and therefore you +[can't use `is` checks and `as` casts](#compatibility-type-checks-and-casts). +In order to interact with and examine these JS values, you should use +[`external`] interop members or [conversions](#conversions). + +## Type hierarchy + +JS types form a natural type hierarchy: + +- Top type: `JSAny`, which is any non-nullish JS value + - Primitives: `JSNumber`, `JSBoolean`, `JSString` + - `JSSymbol` + - `JSBigInt` + - `JSObject`, which is any JS object + - `JSFunction` + - `JSExportedDartFunction`, which represents a Dart callback that was + converted to a JS function + - `JSArray` + - `JSPromise` + - `JSDataView` + - `JSTypedArray` + - JS typed arrays like `JSUint8Array` + - `JSBoxedDartObject`, which allows users to box and pass Dart values + opaquely within the same Dart runtime + +You can find the definition of each type in the [`dart:js_interop` API docs]. + +{% comment %} +TODO (srujzs): Should we add a tree diagram instead for JS types? +{% endcomment %} + +## Conversions + +To use a value from one domain to another, you will likely want to *convert* the +value to the corresponding type of the other domain. For example, you may want +to convert a Dart `List` into a JS array of strings, which is +represented by the JS type `JSArray`, so that you can pass the array +to a JS interop API. + +Dart supplies a number of conversion members on various Dart types and JS types +to convert the values between the domains for you. + +Members that convert values from Dart to JS usually start with `toJS`: + +```dart +String str = 'hello world'; +JSString jsStr = str.toJS; +``` + +Members that convert values from JS to Dart usually start with `toDart`: + +```dart +JSNumber jsNum = ...; +int integer = jsNum.toDartInt; +``` + +Not all JS types have a conversion, and not all Dart types have a conversion. +Generally, the conversion table looks like the following: + +
+ +| JS type | Dart type | +|-------------------------------------|------------------------------------------| +| `JSNumber`, `JSBoolean`, `JSString` | `num`, `int`, `double`, `bool`, `String` | +| `JSExportedDartFunction` | `Function` | +| `JSArray` | `List` | +| `JSPromise` | `Future` | +| Typed arrays like `JSUint8Array` | Typed lists from `dart:typed_data` | +| `JSBoxedDartObject` | Opaque Dart value | + +{:.table .table-striped} +
+ +:::warning +Compiling to JavaScript vs Wasm can introduce inconsistencies in both +performance and semantics for conversions. Conversions may have different costs +depending on the compiler, so prefer to only convert values if you need to. + +Conversions also may or may not produce a new value. This doesn’t matter for +immutable values like numbers, but does matter for types like `List`. Depending +on the implementation, a conversion to `JSArray` may return a reference, a +proxy, or a clone of the original list. To avoid this, do not rely on any +relation between the `List` and `JSArray` and only rely on their contents being +the same. Typed array conversions have a similar limitation. Look up the +specific conversion function for more details. +::: + +## Requirements on `external` declarations and `Function.toJS` + +In order to ensure type safety and consistency, the compiler places requirements +on what types can flow into and out of JS. Passing arbitrary Dart values into JS +is not allowed. Instead, the compiler requires users to use a compatible interop +type or a primitive, which would then be implicitly converted by the compiler. +For example, these would be allowed: + +```dart tag=good +@JS() +external void primitives(String a, int b, double c, num d, bool e); +``` + +```dart tag=good +@JS() +external JSArray jsTypes(JSObject _, JSString __); +``` + +```dart tag=good +extension type InteropType(JSObject _) implements JSObject {} + +@JS() +external InteropType get interopType; +``` + +Whereas these would return an error: + +```dart tag=bad +@JS() +external Function get function; +``` + +```dart tag=bad +@JS() +external set list(List _); +``` + +These same requirements exist when you use [`Function.toJS`] to make a Dart +function callable in JS. The values that flow into and out of this callback must +be a compatible interop type or a primitive. + +If you use a Dart primitive like `String`, an implicit conversion happens in the +compiler to convert that value from a JS value to a Dart value. If performance +is critical and you don’t need to examine the contents of the string, then using +`JSString` instead to avoid the conversion cost may make sense like in the +second example. + +## Compatibility, type checks, and casts + +The runtime type of JS types may differ based on the compiler. This affects +runtime type-checking and casts. Therefore, almost always avoid `is` checks +where the value is an interop type or where the target type is an interop type: + +```dart tag=bad +void f(JSAny a) { + if (a is String) { … } +} +``` + +```dart tag=bad +void f(JSAny a) { + if (a is JSObject) { … } +} +``` + +Also, avoid casts between Dart types and interop types: + +```dart tag=bad +void f(JSString s) { + s as String; +} +``` + +To type-check a JS value, use an interop member like [`typeofEquals`] or +[`instanceOfString`] that examines the JS value itself: + +```dart tag=good +void f(JSAny a) { + // Here `a` is verified to be a JS function, so the cast is okay. + if (a.typeofEquals('function')) { + a as JSFunction; + } +} +``` + +From Dart 3.4 onwards, you can use the [`isA`] helper function to check whether +a value is any interop type: + +```dart tag=good +void f(JSAny a) { + if (a.isA()) {} // `typeofEquals('string')` + if (a.isA()) {} // `instanceOfString('Array')` + if (a.isA()) {} // `instanceOfString('CustomInteropType')` +} +``` + +Depending on the type parameter, it'll transform the call into the appropriate +type-check for that type. + +{% comment %} +TODO: Add a link to and an example using `isA` once it's in a dev release. Users +should prefer that method if it's available. +{% endcomment %} + +Dart may add lints to make runtime checks with JS interop types easier to avoid. +See issue [#4841] for more details. + +## `null` vs `undefined` + +JS has both a `null` and an `undefined` value. This is in contrast with Dart, +which only has `null`. In order to make JS values more ergonomic to use, if an +interop member were to return either JS `null` or `undefined`, the compiler maps +these values to Dart `null`. Therefore a member like `value` in the following +example can be interpreted as returning a JS object, JS `null`, or `undefined`: + +```dart +@JS() +external JSObject? get value; +``` + +If the return type was not declared as nullable, then the program will throw an +error if the value returned was JS `null` or `undefined` to ensure soundness. + +:::warning +There is a subtle inconsistency with regards to `undefined` between compiling to +JS and Wasm. While compiling to JS *treats* `undefined` values as if they were +Dart `null`, it doesn’t actually *change* the value itself. If an interop member +returns `undefined` and you pass that value back into JS, JS will see +`undefined`, *not* `null`, when compiling to JS. + +However, when compiling to Wasm, this is not the case, +and the value will be `null` in JS. This is because +the compiler implicitly *converts* the value to Dart `null` when compiling to +Wasm, thereby losing information on whether the original value was JS `null` or +`undefined`. Avoid writing code where this distinction matters by explicitly +passing Dart `null` instead to an interop member. + +Currently, there's no platform-consistent way to provide `undefined` +to interop members or distinguish between JS `null` and `undefined` values, +but this will likely change in the future. See [#54025] for more details. +::: + +{% comment %} +TODO: add links (with stable) when ready: +{% endcomment %} + +[`dart:js_interop`]: https://api.dart.dev/dev/dart-js_interop/dart-js_interop-library.html +[`external`]: https://dart.dev/language/functions#external +[`Function.toJS`]: https://api.dart.dev/dev/dart-js_interop/FunctionToJSExportedDartFunction/toJS.html +[`dart:js_interop` API docs]: https://api.dart.dev/dev/dart-js_interop/dart-js_interop-library.html#extension-types +[`typeofEquals`]: https://api.dart.dev/dev/dart-js_interop/JSAnyUtilityExtension/typeofEquals.html +[`instanceOfString`]: https://api.dart.dev/dev/dart-js_interop/JSAnyUtilityExtension/instanceOfString.html +[`isA`]: https://api.dart.dev/dev/dart-js_interop/JSAnyUtilityExtension/isA.html +[#4841]: https://github.com/dart-lang/linter/issues/4841 +[#54025]: https://github.com/dart-lang/sdk/issues/54025 \ No newline at end of file diff --git a/src/content/interop/js-interop/mock.md b/src/content/interop/js-interop/mock.md new file mode 100644 index 0000000000..4976f443c5 --- /dev/null +++ b/src/content/interop/js-interop/mock.md @@ -0,0 +1,122 @@ +--- +title: How to mock JavaScript interop objects +--- + +In this tutorial, you'll learn how to mock JS objects so that you can test +interop instance members without having to use a real implementation. + +## Background and motivation + +Mocking classes in Dart is usually done through overriding instance members. +However, since [extension types] are used to declare interop types, all +extension type members are dispatched statically and therefore overriding can't +be used. This [limitation is true for extension members] as well, and therefore +instance extension type or extension members can't be mocked. + +While this applies to any non-`external` extension type member, `external` +interop members are special as they invoke members on a JS value. + +```dart +extension type Date(JSObject _) implements JSObject { + external int getDay(); +} +``` + +As discussed in the [Usage] section, calling `getDay()` will result in calling +`getDay()` on the JS object. Therefore, by using a different `JSObject`, a +different *implementation* of `getDay` can be called. + +In order to do this, there should be some mechanism of creating a JS object that +has a property `getDay` which when called, calls a Dart function. A simple way +is to create a JS object and set the property `getDay` to a converted callback +e.g. + +```dart +final date = Date(JSObject()); +date['getDay'] = (() => 0).toJS; +``` + +While this works, this is prone to error and doesn't scale well when you are +using many interop members. It also doesn't handle getters or setters properly. +Instead, you should use a combination of [`createJSInteropWrapper`] and +[`@JSExport`] to declare a type that provides an implementation for all the +`external` instance members. + +## Mocking example + +```dart +import 'dart:js_interop'; + +import 'package:expect/minitest.dart'; + +// The Dart class must have `@JSExport` on it or at least one of its instance +// members. +@JSExport() +class FakeCounter { + int value = 0; + @JSExport('increment') + void renamedIncrement() { + value++; + } + void decrement() { + value--; + } +} + +extension type Counter(JSObject _) implements JSObject { + external int value; + external void increment(); + void decrement() { + value -= 2; + } +} + +void main() { + var fakeCounter = FakeCounter(); + // Returns a JS object whose properties call the relevant instance members in + // `fakeCounter`. + var counter = createJSInteropWrapper(fakeCounter) as Counter; + // Calls `FakeCounter.value`. + expect(counter.value, 0); + // `FakeCounter.renamedIncrement` is renamed to `increment`, so it gets + // called. + counter.increment(); + expect(counter.value, 1); + expect(fakeCounter.value, 1); + // Changes in the fake affect the wrapper and vice-versa. + fakeCounter.value = 0; + expect(counter.value, 0); + counter.decrement(); + // Because `Counter.decrement` is non-`external`, we never called + // `FakeCounter.decrement`. + expect(counter.value, -2); +} +``` + +## [`@JSExport`] and [`createJSInteropWrapper`] + +`@JSExport` allows you to declare a class that can be used in +`createJSInteropWrapper`. `createJSInteropWrapper` will create an object literal +that maps each of the class' instance member names (or renames) to a JS callback +that triggers the instance member when called. In the above example, getting and +setting `counter.value` gets and sets `fakeCounter.value`. + +You can specify only some members of a class to be exported by omitting the +annotation from the class and instead only annotate the specific members. You +can see more specifics on more specialized exporting (including inheritance) in +the documentation of [`@JSExport`]. + +Note that this mechanism isn't specific to testing only. You can use this to +provide a JS interface for an arbitrary Dart object, allowing you to essentially +*export* Dart objects to JS with a predefined interface. + +{% comment %} +TODO: Should we add a section on general testing? We can't really mock +non-instance members unless the user explicitly replaces the real API in JS. +{% endcomment %} + +[Usage]: /interop/js-interop/usage +[`createJSInteropWrapper`]: https://api.dart.dev/dev/dart-js_interop/createJSInteropWrapper.html +[`@JSExport`]: https://api.dart.dev/dev/dart-js_interop/JSExport-class.html +[limitation is true for extension members]: https://github.com/dart-lang/mockito/blob/master/FAQ.md#how-do-i-mock-an-extension-method +[extension types]: /language/extension-types diff --git a/src/content/interop/js-interop/package-web.md b/src/content/interop/js-interop/package-web.md new file mode 100644 index 0000000000..bfc8a2791a --- /dev/null +++ b/src/content/interop/js-interop/package-web.md @@ -0,0 +1,306 @@ +--- +title: Migrate to package:web +description: How to migrate web interop code from dart:html to package:web. +--- + +Dart's [`package:web`][] exposes access to browser APIs, +enabling interop between Dart applications and the web. +Use `package:web` to interact with the browser and +manipulate objects and elements in the DOM. + +```dart +import 'package:web/web.dart'; + +void main() { + final div = document.querySelector('div')!; + div.text = 'Text set at ${DateTime.now()}'; +} +``` + +:::important +If you maintain a public Flutter package that uses `dart:html` or any of the +other Dart SDK web libraries, +**you should migrate to `package:web` as soon as possible**. +`package:web` is replacing `dart:html` and other web libraries +as Dart's web interop solution long-term. +Read the **`package:web` vs `dart:html`** section for more information. +::: + +## `package:web` vs `dart:html` + +The goal of `package:web` is to revamp how Dart exposes web APIs +by addressing several concerns with the existing Dart web libraries: + +1. **Wasm compatibility** + + Packages can only be compatible with [Wasm][] + if they use [`dart:js_interop`][] and [`dart:js_interop_unsafe`][]. + `package:web` is based on `dart:js_interop`, + so by default, it's supported on `dart2wasm`. + + Dart core web libraries, like [`dart:html`][html] and [`dart:svg`][svg], + are **not supported** when compiling to Wasm. + +2. **Staying modern** + + `package:web` uses the [Web IDL][idl] to automatically generate + [interop members][] and [interop types][] + for each declaration in the IDL. + Generating references directly, + as opposed to the additional members and abstractions in `dart:html`, + allows `package:web` to be more concise, easier to understand, more consistent, + and more able to stay up-to-date with the future of Web developments. + +3. **Versioning** + + Because it's a package, `package:web` can be versioned + more easily than a library like `dart:html` and avoid breaking user code as it + evolves. + It also makes the code less exclusive and more open to contributions. + Developers can create [alternative interop declarations][] of their own + and use them together with `package:web` without conflict. + +--- + +These improvements naturally result in some +implementation differences between `package:web` and `dart:html`. +The changes that affect existing packages the most, +like IDL [renames](#renames) and +[type tests](#type-tests), +are addressed in the migration sections that follow. While we only refer to +`dart:html` for brevity, the same migration patterns apply to any other Dart +core web library like `dart:svg`. + +## Migrating from `dart:html` + +Remove the `dart:html` import and replace it with `package:web/web.dart`: + +```dart +import 'dart:html' as html; // Remove +import 'package:web/web.dart' as web; // Add +``` + +Add `web` to the `dependencies` in your pubspec: + +```yaml +dependencies: + web: ^0.5.0 +``` + +The following sections cover some of the common migration issues +from `dart:html` to `package:web`. + +For any other migration issues, check the [dart-lang/web][] repo and file an +issue. + +### Renames + +Many of the symbols in `dart:html` were renamed from +their original IDL declaration to align more with Dart style. +For example, `appendChild` became `append`, +`HTMLElement` became `HtmlElement`, etc. + +In contrast, to reduce confusion, +`package:web` uses the original names from the IDL definitions. +A `dart fix` is available to convert types that have been renamed +between `dart:html` and `package:web`. + +After changing the import, any renamed objects will be new "undefined" errors. +You can address these either: +- From the CLI, by running `dart fix --dry-run`. +- In your IDE, by selecting the `dart fix`: **Rename to '`package:web name`'**. + +{% comment %} +Maybe a pic here of menu selection in IDE? +TODO: Update this documentation to refer to symbols instead of just types once +we have a dart fix for that. +{% endcomment -%} + +The `dart fix` covers many of the common type renames. +If you come across a `dart:html` type without a built-in fix, let us know by +filing an [issue][]. +You can manually discover the `package:web` type name +by looking up the `dart:html` class' `@Native` annotation. +You can do this by either: + +- Ctrl or cmd clicking the name in the IDE and choosing **Go to Definition**. +- Searching for the name in the [`dart:html` API docs][html] + and checking its page under *Annotations*. + +The `@Native` annotation tells the compiler to treat any JS object of that type +as the class that it annotates. + +Similarly, if you find an API with the keyword `native` in `dart:html` that +doesn't have an equivalent in `package:web`, check to see if there was a rename +with the `@JSName` annotation. +`native` is an internal keyword that means the same as `external` in this +context. + +### Type tests + +It's common for code that uses `dart:html` to utilize runtime checks like `is`. +When used with a `dart:html` object, `is` and `as` verify that the object is +the JS type within the `@Native` annotation. +In contrast, all `package:web` types are reified to [`JSObject`][]. This means a +runtime type test will result in different behavior between `dart:html` and +`package:web` types. + +To be able to perform type tests, migrate any `dart:html` code +using `is` type tests to use [interop methods][] like `instanceOfString` +or the more convenient and typed [`isA`][] helper +(available from Dart 3.4 onward). +The [Compatibility, type checks, and casts][] +section of the JS types page covers alternatives in detail. + +```dart +obj is Window; // Remove +obj.instanceOfString('Window'); // Add +``` + +### Type signatures + +Many APIs in `dart:html` support various Dart types in their type signatures. +Because `dart:js_interop` [restricts] the types that can be written, some of +the members in `package:web` will now require you to *convert* the value before +calling the member. +Learn how to use interop conversion methods from the [Conversions][] +section of the JS types page. + +```dart +window.addEventListener('click', callback); // Remove +window.addEventListener('click', callback.toJS); // Add +``` + +{% comment %} +TODO: Think of a better example. People will likely use the stream helpers +instead of `addEventListener`. +{% endcomment -%} + +Generally, you can spot which methods need a conversion because they'll be +flagged with some variation of the exception: + +```plaintext +A value of type '...' can't be assigned to a variable of type 'JSFunction?' +``` + +### Conditional imports + +It is common for code to use a conditional import based on whether `dart:html` +is supported to differentiate between native and web: + +```dart +export 'src/hw_none.dart' + if (dart.library.io) 'src/hw_io.dart' + if (dart.library.html) 'src/hw_html.dart'; +``` + +However, since `dart:html` is not supported when compiling to Wasm, the correct +alternative now is to use `dart.library.js_interop` to differentiate between +native and web: + +```dart +export 'src/hw_none.dart' + if (dart.library.io) 'src/hw_io.dart' + if (dart.library.js_interop) 'src/hw_web.dart'; +``` + +### Virtual dispatch and mocking + +`dart:html` classes supported virtual dispatch, but because JS interop uses +extension types, virtual dispatch is [not possible]. Similarly, `dynamic` calls +with `package:web` types won't work as expected (or, they might continue to work +just by chance, but will stop when `dart:html` is removed), as their members are +only available statically. Migrate all code that relies on virtual dispatch to +avoid this issue. + +One use case of virtual dispatch is mocking. If you have a mocking class that +`implements` a `dart:html` class, it can't be used to implement a `package:web` +type. Instead, prefer mocking the JS object itself. See the [mocking tutorial] +for more information. + +### Non-`native` APIs + +`dart:html` classes may also contain APIs that have a non-trivial +implementation. These members may or may not exist in the `package:web` +[helpers](#helpers). If your code relies on the specifics of that +implementation, you may be able to copy the necessary code. +However, if you think that's not tractable or if that code would be beneficial +for other users as well, consider filing an issue or uploading a pull request to +[`package:web`][dart-lang/web] to support that member. + +### Zones + +In `dart:html`, callbacks are automatically zoned. +This is not the case in `package:web`. There is no automatic binding of +callbacks in the current zone. + +If this matters for your application, you can still use zones, but you will have +to [write them yourself][zones] by binding the callback. See [#54507] for more +details. +There is no conversion API or [helper](#helpers) available yet to +automatically do this. + +## Helpers + +The core of `package:web` contains `external` interop members, +but does not provide other functionality that `dart:html` provided by default. +To mitigate these differences, `package:web` contains [`helpers`][helpers] +for additional support in handling a number of use cases +that aren't directly available through the core interop. +The helper library contains various members to expose some legacy features from +the Dart web libraries. + +For example, the core `package:web` only has support for adding and removing +event listeners. Instead, you can use [stream helpers][] that makes it easy to +subscribe to events with Dart `Stream`s without writing that code yourself. + +```dart +// dart:html version +InputElement htmlInput = InputElement(); +await htmlInput.onBlur.first; + +// package:web version +HTMLInputElement webInput = document.createElementById('input'); +await webInput.onBlur.first; +``` + +You can find all the helpers and their documentation in the repo at +[`package:web/helpers`][helpers]. They will continuously be updated to aid users +in migration and make it easier to use the web APIs. + +## Examples + +Here are some examples of packages that have been migrated from `dart:html` +to `package:web`: + +- [Upgrading `url_launcher` to `package:web`][] + +{% comment %} +Do we have any other package migrations to show off here? +{% endcomment -%} + +[`package:web`]: {{site.pub-pkg}}/web +[Wasm]: https://github.com/dart-lang/sdk/blob/main/pkg/dart2wasm/README.md +[html]: {{site.dart-api}}/{{site.sdkInfo.channel}}/dart-html/dart-html-library.html +[svg]: {{site.dart-api}}/{{site.sdkInfo.channel}}/dart-svg/dart-svg-library.html +[`dart:js_interop`]: https://api.dart.dev/dev/dart-js_interop/dart-js_interop-library.html +[`dart:js_interop_unsafe`]: https://api.dart.dev/dev/dart-js_interop_unsafe/dart-js_interop_unsafe-library.html +[idl]: https://www.npmjs.com/package/@webref/idl +[interop members]: /interop/js-interop/usage#interop-members +[interop types]: /interop/js-interop/usage#interop-types +[dart-lang/web]: https://github.com/dart-lang/web +[issue]: https://github.com/dart-lang/web/issues/new +[helpers]: https://github.com/dart-lang/web/tree/main/lib/src/helpers +[zones]: /articles/archive/zones +[Conversions]: /interop/js-interop/js-types#conversions +[interop methods]: https://api.dart.dev/dev/dart-js_interop/JSAnyUtilityExtension.html#instance-methods +[alternative interop declarations]: /interop/js-interop/usage +[Compatibility, type checks, and casts]: /interop/js-interop/js-types#compatibility-type-checks-and-casts +[Upgrading `url_launcher` to `package:web`]: https://github.com/flutter/packages/compare/main...johnpryan:wasm/url-launcher +[stream helpers]: https://github.com/dart-lang/web/blob/main/lib/src/helpers/events/streams.dart +[not possible]: /language/extension-types +[`JSObject`]: https://api.dart.dev/dev/dart-js_interop/JSObject-extension-type.html +[`isA`]: https://api.dart.dev/dev/dart-js_interop/JSAnyUtilityExtension/isA.html +[restricts]: /interop/js-interop/js-types#requirements-on-external-declarations-and-function-tojs +[#54507]: https://github.com/dart-lang/sdk/issues/54507 +[mocking tutorial]: /interop/js-interop/mock \ No newline at end of file diff --git a/src/content/interop/js-interop/past-js-interop.md b/src/content/interop/js-interop/past-js-interop.md new file mode 100644 index 0000000000..f1980a87a1 --- /dev/null +++ b/src/content/interop/js-interop/past-js-interop.md @@ -0,0 +1,123 @@ +--- +title: Past JS interop +description: Archive of past JS interop implementations. +--- + +:::warning +None of these legacy interop libraries are supported when compiling to Wasm. +::: + +This page addresses previous iterations of JS interop for Dart that are +considered legacy. They are not deprecated yet, but will likely be in the +future. Therefore, prefer using [`dart:js_interop`] going forwards and migrate +usages of old interop libraries when possible. While [`dart:html`] and other web +libraries are closely related, they're covered in the [`package:web`] page. + +## `dart:js` + +[`dart:js`] exposed a concrete [`object wrapper`] to interop with JS objects. +This wrapper contained String-based methods to dynamically get, set, and call +properties on the wrapped JS object. It was less performant due to wrapping +costs and ergonomically more difficult to use. For example, you did not get +code-completion as you couldn't declare interop members and instead relied on +Strings. Many of the functionalities exposed in `dart:js` like [`allowInterop`] +were later re-exposed through other interop libraries. + +This library has been legacy ever since `package:js` and `dart:js_util` were +released. It will likely be the first to be deprecated. + +## `package:js` + +[`package:js`] introduced functionality to declare interop types and members. +It allowed users to write interop classes instead of interop extension types. At +runtime, these classes were erased to a type that is similar to +`dart:js_interop`'s [`JSObject`]. + +```dart +@JS() +class JSType {} +``` + +Users of `package:js` will find the syntax and semantics of `dart:js_interop` +familiar. You may be able to migrate to `dart:js_interop` by replacing the class +definition with an extension type and have it work in many cases. + +There are significant differences, however: + +- `package:js` types could not be used to interop with browser APIs. + `dart:js_interop` types can. +- `package:js` allowed dynamic dispatch. This meant that if you casted the + `package:js` type to `dynamic` and called an interop member on it, it would + forward to the right member. This is no longer possible with + `dart:js_interop`. +- `package:js`' [`@JS`] has no soundness guarantees as return types of + `external` members were not checked. `dart:js_interop` is sound. +- `package:js` types could not rename instance members or have non-`external` + members. +- `package:js` types could subtype and be a supertype of non-interop classes. + This was often used for mocks. With `dart:js_interop`, mocking is done by + substituting the JS object instead. See the [tutorial on mocking]. +- [`@anonymous`] types were a way to declare an interop type with an object + literal constructor. `dart:js_interop` doesn't distinguish types this way and + any `external` named-argument constructor is an object literal constructor. + +### `@staticInterop` + +Along with `@JS` and `@anonymous`, `package:js` later exposed +[`@staticInterop`], which was a prototype of interop extension types. It is as +expressive and restrictive as `dart:js_interop` and was meant to be a +transitory syntax until extension types were available. + +`@staticInterop` types were implicitly erased to `JSObject`. It required users +to declare all instance members in extensions so that only static semantics +could be used, and had stronger soundness guarantees. Users could use it to +interact with browser APIs, and it also allowed things like renaming and +non-`external` members. Like interop extension types, it didn't have support for +dynamic dispatch. + +`@staticInterop` classes can almost always be migrated to an interop extension +type by just changing the class to an extension type and removing the +annotations. + +`dart:js_interop` exposed `@staticInterop` (and `@anonymous`, but only if +`@staticInterop` is also used) to support static interop semantics until +extension types were added to the language. All such types should now be +migrated to extension types. + +## `dart:js_util` + +[`dart:js_util`] supplied a number of utility functions that could not be +declared in an `package:js` type or were necessary for values to be passed back +and forth. This included members like: + +- `allowInterop` (which is now [`Function.toJS`]) +- `getProperty`/`setProperty`/`callMethod`/`callConstructor` (which are now in + [`dart:js_interop_unsafe`]) +- Various JS operators +- Type-checking helpers +- Mocking support +- And more. + +`dart:js_interop` and `dart:js_interop_unsafe` contain these helpers now with +possibly alternate syntax. + +{% comment %} +TODO: add links (with stable) when ready: +TODO: Link to `package:web` section +{% endcomment %} + +[`dart:js_interop`]: https://api.dart.dev/dev/dart-js_interop +[`dart:html`]: {{site.dart-api}}/{{site.sdkInfo.channel}}/dart-html +[`package:web`]: /interop/js-interop/package-web +[`dart:js`]: {{site.dart-api}}/{{site.sdkInfo.channel}}/dart-js +[`object wrapper`]: {{site.dart-api}}/{{site.sdkInfo.channel}}/dart-js/JsObject-class.html +[`allowInterop`]: {{site.dart-api}}/{{site.sdkInfo.channel}}/dart-js_util/allowInterop.html +[`package:js`]: https://pub.dev/packages/js +[`JSObject`]: https://api.dart.dev/dev/dart-js_interop/JSObject-extension-type.html +[`@JS`]: https://github.com/dart-lang/sdk/blob/main/sdk/lib/js/_js_annotations.dart#L11 +[tutorial on mocking]: /interop/js-interop/mock +[`@anonymous`]: https://github.com/dart-lang/sdk/blob/main/sdk/lib/js/_js_annotations.dart#L40 +[`@staticInterop`]: https://github.com/dart-lang/sdk/blob/main/sdk/lib/js/_js_annotations.dart#L48 +[`dart:js_util`]: {{site.dart-api}}/{{site.sdkInfo.channel}}/dart-js_util +[`Function.toJS`]: https://api.dart.dev/dev/dart-js_interop/FunctionToJSExportedDartFunction/toJS.html +[`dart:js_interop_unsafe`]: https://api.dart.dev/dev/dart-js_interop_unsafe diff --git a/src/content/interop/js-interop/tutorials.md b/src/content/interop/js-interop/tutorials.md new file mode 100644 index 0000000000..693de43872 --- /dev/null +++ b/src/content/interop/js-interop/tutorials.md @@ -0,0 +1,18 @@ +--- +title: JS interop tutorials +description: Tutorials for common JavaScript interop use cases in Dart. +--- + +## Tutorials + +### [How to mock JavaScript interop in Dart][] + +This tutorial will walk through how you can use Dart classes to mock the +`external` instance members of an interop type. + +{% comment %} +TODO: add a section on how to bundle a JS and Dart app for interop +TODO: maybe add a section on conversions +{% endcomment %} + +[How to mock JavaScript interop in Dart]: /interop/js-interop/mock \ No newline at end of file diff --git a/src/content/interop/js-interop/usage.md b/src/content/interop/js-interop/usage.md new file mode 100644 index 0000000000..88b1c3ff37 --- /dev/null +++ b/src/content/interop/js-interop/usage.md @@ -0,0 +1,457 @@ +--- +title: Usage +description: How to declare and use JS interop members. +--- + +JS interop provides the mechanisms to interact with JavaScript APIs from Dart. +It allows you to invoke these APIs and interact with the values that you get +from them using an explicit, idiomatic syntax. + +Typically, you access a JavaScript API by making it available somewhere within +the [global JS scope]. To call and receive JS values from this API, you use +[`external` interop members](#interop-members). In order to construct and +provide types for JS values, you use and declare +[interop types](#interop-types), which also contain interop members. To pass +Dart values like `List`s or `Function` to interop members or convert from JS +values to Dart values, you use [conversion functions] unless the interop member +[contains a primitive type]. + +## Interop types + +When interacting with a JS value, you need to provide a Dart type for it. You +can do this by either using or declaring an interop type. Interop types are +either a ["JS type"] provided by Dart or an [extension type] wrapping an interop +type. + +Interop types allow you to provide an interface for a JS value and lets you +declare interop APIs for its members. They are also used in the signature of +other interop APIs. + +```dart +extension type Window(JSObject _) implements JSObject {} +``` + +`Window` is an interop type for an arbitrary `JSObject`. There is no [runtime +guarantee][] that `Window` is actually a JS [`Window`]. There also is no conflict +with any other interop interface that is defined for the same value. If you want +to check that `Window` is actually a JS `Window`, you can +[check the type of the JS value through interop]. + +You can also declare your own interop type for the JS types Dart provides by +wrapping them: + +```dart +extension type Array._(JSArray _) implements JSArray { + external Array(); +} +``` + +In most cases, you will likely declare an interop type using `JSObject` as the +[representation type] because you're likely interacting with JS objects which +don't have an interop type provided by Dart. + +Interop types should also generally [implement] their representation type so +that they can be used where the representation type is expected, like in many +APIs in [`package:web`]. + +## Interop members + +[`external`] interop members provide an idiomatic syntax for JS members. They +allow you to write a Dart type signature for its arguments and return value. The +types that can be written in the signature of these members have [restrictions]. +The JS API the interop member corresponds to is determined by a combination of +where it's declared, its name, what kind of Dart member it is, and any +[renames](#js). + +### Top-level interop members + +Given the following JS members: + +```js +globalThis.name = 'global'; +globalThis.isNameEmpty = function() { + return globalThis.name.length == 0; +} +``` + +You can write interop members for them like so: + +```dart +@JS() +external String get name; + +@JS() +external set name(String value); + +@JS() +external bool isNameEmpty(); +``` + +Here, there exists a property `name` and a function `isNameEmpty` that are +exposed in the global scope. To access them, you use top-level interop members. +To get and set `name`, you declare and use an interop getter and setter with the +same name. To use `isNameEmpty`, you declare and call an interop function with +the same name. You can declare top-level interop getters, setters, methods, and +fields. Interop fields are equivalent to getter and setter pairs. + +Top-level interop members must be declared with a [`@JS()`](#js) annotation to +distinguish them from other `external` top-level members, like those that can be +written using `dart:ffi`. + +### Interop type members + +Given a JS interface like the following: + +```js +class Time { + constructor(hours, minutes) { + this._hours = Math.abs(hours) % 24; + this._minutes = arguments.length == 1 ? 0 : Math.abs(minutes) % 60; + } + + static dinnerTime = new Time(18, 0); + + static getTimeDifference(t1, t2) { + return new Time(t1.hours - t2.hours, t1.minutes - t2.minutes); + } + + get hours() { + return this._hours; + } + + set hours(value) { + this._hours = Math.abs(value) % 24; + } + + get minutes() { + return this._minutes; + } + + set minutes(value) { + this._minutes = Math.abs(value) % 60; + } + + isDinnerTime() { + return this.hours == Time.dinnerTime.hours && this.minutes == Time.dinnerTime.minutes; + } +} +// Need to expose the type to the global scope. +globalThis.Time = Time; +``` + +You can write an interop interface for it like so: + +```dart +extension type Time._(JSObject _) implements JSObject { + external Time(int hours, int minutes); + external factory Time.onlyHours(int hours); + + external static Time dinnerTime; + external static Time getTimeDifference(Time t1, Time t2); + + external int hours; + external int minutes; + external bool isDinnerTime(); + + bool isMidnight() => hours == 0 && minutes == 0; +} +``` + +Within an interop type, you can declare several different types of +`external` interop members: + +- **Constructors**. When called, constructors with only positional parameters + create a new JS object whose constructor is defined by the name of the + extension type using `new`. For example, calling `Time(0, 0)` in Dart will + generate a JS invocation that looks like `new Time(0, 0)`. Similarly, calling + `Time.onlyHours(0)` will generate a JS invocation that looks like + `new Time(0)`. Note that the JS invocations of the two constructors follow the + same semantics, regardless of whether they're given a Dart name or if they are + a factory. + + - **Object literal constructors**. It is useful sometimes to create a JS + [object literal] that simply contains a number of properties and their + values. In order to do this, you declare a constructor with only named + parameters, where the names of the parameters will be the property names: + + ```dart + extension type Options._(JSObject o) implements JSObject { + external Options({int a, int b}); + external int get a; + external int get b; + } + ``` + + A call to `Options(a: 0, b: 1)` will result in creating the JS object + `{a: 0, b: 1}`. The object is defined by the invocation arguments, so + calling `Options(a: 0)` would result in `{a: 0}`. You can get or set the + properties of the object through `external` instance members. + + :::warning + There's a bug that currently requires object literal constructors to have an + [`@JS`](#js) annotation on the library. See [#54801] for more details. + ::: + +- **`static` members**. Like constructors, these members use the name of the + extension type to generate the JS code. For example, calling + `Time.getTimeDifference(t1, t2)` will generate a JS invocation that looks like + `Time.getTimeDifference(t1, t2)`. Similarly, calling `Time.dinnerTime` will + result in a JS invocation that looks like `Time.dinnerTime`. Like top-levels, + you can declare `static` methods, getters, setters, and fields. + +- **Instance members**. Like with other Dart types, these members require an + instance in order to be used. These members get, set, or invoke properties on + the instance. For example: + + ```dart + final time = Time(0, 0); + print(time.isDinnerTime()); // false + final dinnerTime = Time.dinnerTime; + time.hours = dinnerTime.hours; + time.minutes = dinnerTime.minutes; + print(time.isDinnerTime()); // true + ``` + + The call to `dinnerTime.hours` gets the value of the `hours` property of + `dinnerTime`. Similarly, the call to `time.minutes=` sets the value of the + `minutes` property of time. The call to `time.isDinnerTime()` calls the + function in the `isDinnerTime` property of `time` and returns the value. + Like top-levels and `static` members, you can declare instance methods, + getters, setters, and fields. + +- **Operators**. There are only two `external` interop operators allowed in + interop types: `[]` and `[]=`. These are instance members that match the + semantics of JS' [property accessors]. For example, you can declare them like: + + ```dart + extension type Array(JSArray _) implements JSArray { + external JSNumber operator [](int index); + external void operator []=(int index, JSNumber value); + } + ``` + + Calling `array[i]` gets the value in the `i`th slot of `array`, and + `array[i] = i.toJS` sets the value in that slot to `i.toJS`. Other JS + operators are exposed through [utility functions] in `dart:js_interop`. + +Lastly, like any other extension type, you're allowed to declare any +[non-`external` members] in the interop type. `isMidnight` is one such example. + +### Extension members on interop types + +You can also write `external` members in [extensions] of interop types. For +example: + +```dart +extension on Array { + external int push(JSAny? any); +} +``` + +The semantics of calling `push` are identical to what it would have been if it +was in the definition of `Array` instead. Extensions can have `external` +instance members and operators, but cannot have `external` `static` members or +constructors. Like with interop types, you can write any non-`external` members +in the extension. These extensions are useful for when an interop type doesn't +expose the `external` member you need and you don't want to create a new interop +type. + +### Parameters + +`external` interop methods can only contain positional and optional arguments. +This is because JS members only take positional arguments. The one exception is +object literal constructors, where they can contain only named arguments. + +Unlike with non-`external` methods, optional arguments do not get replaced with +their default value, but are instead omitted. For example: + +```dart +external int push(JSAny? any, [JSAny? any2]); +``` + +Calling `array.push(0.toJS)` in Dart will result in a JS invocation of +`array.push(0.toJS)` and *not* `array.push(0.toJS, null)`. This allows users to +not have to write multiple interop members for the same JS API to avoid passing +in `null`s. If you declare a parameter with an explicit default value, you will +get a warning that the value will be ignored. + +## `@JS()` + +It is sometimes useful to refer to a JS property with a different name than the +one written. For example, if you want to write two `external` APIs that point to +the same JS property, you’d need to write a different name for at least one of +them. Similarly, if you want to define multiple interop types that refer to the +same JS interface, you need to rename at least one of them. Another example is +if the JS name cannot be written in Dart e.g. `$a`. + +In order to do this, you can use the [`@JS()`] annotation with a constant +string value. For example: + +```dart +extension type Array._(JSArray _) implements JSArray { + external int push(JSNumber number); + @JS('push') + external int pushString(JSString string); +} +``` + +Calling either `push` or `pushString` will result in JS code that uses `push`. + +You can also rename interop types: + +```dart +@JS('Date') +extension type JSDate._(JSObject _) implements JSObject { + external JSDate(); + + external static int now(); +} +``` + +Calling `JSDate()` will result in a JS invocation of `new Date()`. Similarly, +calling `JSDate.now()` will result in a JS invocation of `Date.now()`. + +Furthermore, you can namespace an entire library, which will add a prefix to all +interop top-level members, interop types, and `static` interop members within +those types. This is useful if you want to avoid adding too many members to the +global JS scope. + +```dart +@JS('library1') +library; + +import 'dart:js_interop'; + +@JS() +external void method(); + +extension type JSType._(JSObject _) implements JSObject { + external JSType(); + + external static int get staticMember; +} +``` + +Calling `method()` will result in a JS invocation of `library1.method()`, +calling `JSType()` will result in a JS invocation of `new library1.JSType()`, +and calling `JSType.staticMember` will result in a JS invocation of +`library1.JSType.staticMember`. + +Unlike interop members and interop types, Dart only ever adds a library name in +the JS invocation if you provide a non-empty value in the `@JS()` annotation on +the library. It does not use the Dart name of the library as the default. + +```dart +library interop_library; + +import 'dart:js_interop'; + +@JS() +external void method(); +``` + +Calling `method()` will result in a JS invocation of `method()` and not +`interop_library.method()`. + +You can also write multiple namespaces delimited by a `.` for libraries, +top-level members, and interop types: + +```dart +@JS('library1.library2') +library; + +import 'dart:js_interop'; + +@JS('library3.method') +external void method(); + +@JS('library3.JSType') +extension type JSType._(JSObject _) implements JSObject { + external JSType(); +} +``` + +Calling `method()` will result in a JS invocation of +`library1.library2.library3.method()`, calling `JSType()` will result in a JS +invocation of `new library1.library2.library3.JSType()`, and so forth. + +You can't use `@JS()` annotations with `.` in the value on interop type members +or extension members of interop types, however. + +If there is no value provided to `@JS()` or the value is empty, no renaming will +occur. + +`@JS()` also tells the compiler that a member or type is intended to be treated +as a JS interop member or type. It is required (with or without a value) for all +top-level members to distinguish them from other `external` top-level members, +but can often be elided on and within interop types and on extension members as +the compiler can tell it is a JS interop type from the representation type and +on-type. + +## `dart:js_interop` and `dart:js_interop_unsafe` + +[`dart:js_interop`] contains all the necessary members you should need, +including `@JS`, JS types, conversion functions, and various utility functions. +Utility functions include: + +- [`globalContext`], which represents the global scope that the compilers use to + find interop members and types. +- [Helpers to inspect the type of JS values] +- JS operators +- [`dartify`] and [`jsify`], which check the type of certain JS values and + convert them to Dart values and vice versa. Prefer using the specific + conversion when you know the type of the JS value, as the extra type-checking + may be expensive. +- [`importModule`], which allows you to import modules dynamically as + `JSObject`s. + +More utilities may be added to this library in the future. + +[`dart:js_interop_unsafe`] contains members that allow you to look up properties +dynamically. For example: + +```dart +JSFunction f = console['log']; +``` + +Instead of declaring an interop member named `log`, we're instead using a string +to represent the property. `dart:js_interop_unsafe` provides functionality to +dynamically get, set, and call properties. + +:::tip +Avoid using `dart:js_interop_unsafe` if possible. It makes security compliance +more difficult to guarantee and may lead to violations, which is why it can be +"unsafe". +::: + +{% comment %} +TODO: Some of these are not available on stable. How do we link to dev? +{% endcomment %} + +[global JS scope]: https://developer.mozilla.org/en-US/docs/Glossary/Global_scope +[conversion functions]: /interop/js-interop/js-types#conversions +[contains a primitive type]: /interop/js-interop/js-types#requirements-on-external-declarations-and-function-tojs +["JS type"]: /interop/js-interop/js-types +[`Window`]: https://developer.mozilla.org/en-US/docs/Web/API/Window +[check the type of the JS value through interop]: /interop/js-interop/js-types#compatibility-type-checks-and-casts +[`package:web`]: {{site.pub-pkg}}/web +[`external`]: /language/functions#external +[restrictions]: /interop/js-interop/js-types#requirements-on-external-declarations-and-function-tojs +[object literal]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Object_initializer +[#54801]: https://github.com/dart-lang/sdk/issues/54801 +[property accessors]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Property_accessors#bracket_notation +[utility functions]: https://api.dart.dev/dev/dart-js_interop/JSAnyOperatorExtension.html +[`@JS()`]: https://api.dart.dev/dev/dart-js_interop/JS-class.html +[`dart:js_interop`]: https://api.dart.dev/dev/dart-js_interop +[`globalContext`]: https://api.dart.dev/dev/dart-js_interop/globalContext.html +[Helpers to inspect the type of JS values]: https://api.dart.dev/dev/dart-js_interop/JSAnyUtilityExtension.html +[`dartify`]: https://api.dart.dev/dev/dart-js_interop/JSAnyUtilityExtension/dartify.html +[`jsify`]: https://api.dart.dev/dev/dart-js_interop/NullableObjectUtilExtension/jsify.html +[`importModule`]: https://api.dart.dev/dev/dart-js_interop/importModule.html +[`dart:js_interop_unsafe`]: https://api.dart.dev/dev/dart-js_interop_unsafe +[extensions]: /language/extension-methods +[extension type]: /language/extension-types +[runtime guarantee]: /language/extension-types#type-considerations +[representation type]: /language/extension-types#declaration +[implement]: /language/extension-types#implements +[non-`external` members]: /language/extension-types#members diff --git a/src/content/libraries/dart-html.md b/src/content/libraries/dart-html.md index 3dbb59cde8..c941226323 100644 --- a/src/content/libraries/dart-html.md +++ b/src/content/libraries/dart-html.md @@ -6,6 +6,13 @@ prevpage: title: dart:io --- +:::warning +`dart:html` is being replaced with [`package:web`][]. +Package maintainers should migrate to `package:web` as +soon as possible to be compatible with Wasm. +Read the [Migrate to package:web][] page for guidance. +::: + Use the [dart:html][] library to program the browser, manipulate objects and elements in the DOM, and access HTML5 APIs. DOM stands for *Document Object Model*, which describes the hierarchy of an HTML page. @@ -29,6 +36,9 @@ To use the HTML library in your web app, import dart:html: import 'dart:html'; ``` +[`package:web`]: {{site.pub-pkg}}/web +[Migrate to package:web]: /interop/js-interop/package-web + ### Manipulating the DOM To use the DOM, you need to know about *windows*, *documents*, diff --git a/src/content/libraries/index.md b/src/content/libraries/index.md index 7b4aff57eb..bee5d7504b 100644 --- a/src/content/libraries/index.md +++ b/src/content/libraries/index.md @@ -163,4 +163,4 @@ Misc --> [development JavaScript compiler]: /tools/webdev#serve [jit]: /overview#native-platform -[JavaScript interoperability]: /web/js-interop +[JavaScript interoperability]: /interop/js-interop diff --git a/src/content/resources/faq.md b/src/content/resources/faq.md index 9c63dc7417..94d15d318e 100644 --- a/src/content/resources/faq.md +++ b/src/content/resources/faq.md @@ -352,5 +352,5 @@ either of those lists results in a runtime exception. [dart analyze]: /tools/dart-analyze [webdev]: /tools/webdev -[dart-mirror]: {{site.dart-api}}/{{site.data.pkg-vers.SDK.channel}}/dart-mirrors +[dart-mirror]: {{site.dart-api}}/{{site.sdkInfo.channel}}/dart-mirrors [pub-cmd]: https://dart.dev/tools/pub/cmd