Migrate to package:web
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.
import 'package:web/web.dart';
void main() {
final div = document.querySelector('div')!;
div.text = 'Text set at ${DateTime.now()}';
}
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:
-
Wasm compatibility
Packages can only be compatible with Wasm if they use
dart:js_interopanddart:js_interop_unsafe.package:webis based ondart:js_interop, so by default, it's supported ondart2wasm.Dart core web libraries, like
dart:htmlanddart:svg, are deprecated and not supported when compiling to Wasm. -
Staying modern
package:webuses the Web 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 indart:html, allowspackage:webto be more concise, easier to understand, more consistent, and more able to stay up-to-date with the future of Web developments. -
Versioning
Because it's a package,
package:webcan be versioned more easily than a library likedart:htmland 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 withpackage:webwithout 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 and
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:
import 'dart:html' as html; // Remove
import 'package:web/web.dart' as web; // Add
Add web to the dependencies in
your pubspec:
dart pub add web
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'.
The dart fix covers many of the common type
renames. If you come across a dart:html type
without a dart fix to rename it, first let us
know by filing an
issue.
Then, you can try manually discovering the
package:web type name of an existing
dart:html member by looking up its definition.
The value of the @Native annotation on a
dart:html member definition tells the compiler
to treat any JS object of that type as the Dart class that
it annotates. For example, the
@Native annotation tells us that the native JS
name of dart:html's
HtmlElement member is HTMLElement,
so the package:web name will also be
HTMLElement:
@Native("HTMLElement")
class HtmlElement extends Element implements NoncedElement { }
To find the dart:html definition for an
undefined member in package:web, try either of
the following methods:
- Ctrl or command click the undefined name in the IDE and choose Go to Definition.
-
Search for the name in the
dart:htmlAPI docs and check its page under Annotations.
Similarly, you might find an undefined
package:web API whose corresponding
dart:html member's definition uses the keyword
native. Check if the definition uses the
@JSName annotation for a rename; the value of
the annotation will tell you the name the member uses in
package:web:
@JSName('appendChild')
Node append(Node node) native;
native is an internal keyword that means the
same as external in this context.
Element and HTMLElement
#
Element is now HTMLElement but the
extension type Element also exists as a base
type for HTMLElement, SVGElement,
etc. See
Element MDN documentation. querySelector returns an Element as it can
return other subtypes of
Element
besides HTMLElement like
SVGElement. A downcast to
HTMLElement
is needed if you need to access its methods.
element.querySelector('#selectme')!.className = 'test'; // valid in both
element.querySelector('#selectme')!.style.color = 'red'; // Remove
(element.querySelector('#selectme') as HTMLElement).style.color = 'red'; // Add
List operations
#
Unlike dart:html,
package:web methods like
Element.querySelectorAll
and
Element.children return values that don't
implement List. If a List is
required, you'll need to wrap it with a class.
For immutable operations, you can use
JSImmutableListWrapper:
final anchors = document.querySelectorAll('a');
for (final anchor in anchors) {} // Remove
for (final anchor in JSImmutableListWrapper(anchors)) {} //Add
for (final child in parent.children) {} // Remove
for (final child in JSImmutableListWrapper(parent.children)) {} // Add
You need to add implementation for simple mutable operations
like removeWhere:
parent.children.removeWhere(test); // Remove
for (var i = parent.children.length - 1; i >= 0; --i) {
if (test(parent.children.item(i)!)) {
parent.children.item(i)!.remove();
}
} // Add
Common DOM manipulation examples
#
Here are some common examples of trivial changes that need
to be made when migrating from dart:html to
package:web:
element.querySelector('#selector')?.innerHtml = 'something'; // Remove
element.querySelector('#selector')?.innerHTML = 'something'.toJS; // Add
element.parent.classes.add('class'); // Remove
element.parentElement.classList.add('class'); // Add
element.appendHtml(html); // Remove
element.insertAdjacentHTML('beforeend', html.toJS); // Add
var checkbox = CheckboxInputElement(); // Remove
var checkbox = HTMLInputElement()..type='checkbox'; // Add
element.querySelectorAll('a').classes.add('link'); // Remove
for (final a in JSImmutableListWrapper(element.querySelectorAll('a'))) {
a.classList.add('a');
} // Add
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.
obj is Window; // Remove
obj.instanceOfString('Window'); // Add
element is InputElement; // Remove
element.isA<HTMLInputElement>(); // 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.
window.addEventListener('click', callback); // Remove
window.addEventListener('click', callback.toJS); // Add
Generally, you can spot which methods need a conversion because they'll be flagged with some variation of the exception:
A value of type '...' can't be assigned to a variable of type 'JSFunction?'
Conditional imports
#
It's common for code to use a conditional import based on
whether dart:html
is supported to differentiate between native and web:
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 deprecated and not
supported when compiling to Wasm, the correct alternative
now is to use dart.library.js_interop to
differentiate between native and web:
export 'src/hw_none.dart' // Stub implementation
if (dart.library.io) 'src/hw_io.dart' // dart:io implementation
if (dart.library.js_interop) 'src/hw_web.dart'; // package:web implementation
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 might also contain APIs that
have a non-trivial implementation. These members might or
might not exist in the package:web
helpers. If your code
relies on the specifics of that implementation, you might 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
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 by binding the callback. See #54507 for more details.
For example, when passing a callback to a DOM event
listener, manually bind it to the current zone before
converting it with .toJS:
// Before: Callback lost zone context
element.addEventListener(
'click',
(web.Event event) {
// ... relies on zone-local values
}.toJS,
);
element.addEventListener(
'click',
Zone.current.bindUnaryCallback((web.Event event) {
// ... zone-local values are preserved
}).toJS,
);
There is no conversion API or helper available yet to automatically do this.
Helpers
#
The core of package:web contains
external interop members, but doesn't provide
other functionality that dart:html provided by
default. To mitigate these differences,
package:web contains
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
Streams without writing that code yourself.
// Original dart:html version:
final htmlInput = InputElement();
await htmlInput.onBlur.first;
// Migrated package:web version:
final webInput = HTMLInputElement();
await webInput.onBlur.first;
You can find all the helpers and their documentation in the
repo at
package:web/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: