Skip to content

Conversation

@fsw
Copy link
Contributor

@fsw fsw commented Dec 11, 2025

dart:html allowed modifying child elements and nodes using List interface:

import 'dart:html';

document.querySelector('#foo')!.children.removeWhere((e) => e.classes.contains('remove-me'));

This was a pretty clear and handy interface IMHO.

As discussed in this PR dart-lang/site-www#7040 with @srujzs it might be useful to implement something similar in package:web. This PR is my attempt at doing so.

It adds JSLiveNodeListWrapper that requires a container node and a HTMLCollection or NodeList that is live and implements mutable List methods.

For easier usage it also adds childrenAsList and childNodesAsList via extension so you can do:

import 'package:web/web.dart';

document.querySelector('#foo')!.childrenAsList.removeWhere((e) => e.classList.contains('remove-me'));
document.querySelector('#foo')!.childNodesAsList.removeWhere((e) => e.textContent == 'remove-me');

Questions/Notes:

  • Does this implementation make sense and matches ideas behind helpers in this module? JSLiveNodeListWrapper don't have to extend JSImmutableListWrapper but have its own implementation but it kinda felt it makes sense this way.

  • Alternatively handy methods like removeWhere could be added via extension directly to HTMLCollection and NodeList but methods like add() might not be possible as those require a parent node to add elements to. This would require knowing if NodeList was returned from querySelector or childNodes and in further case accessing parent somehow. Also implementing full List interface might have other pros.

  • could/should NodeList (returned f.e. from querySelector) be extended with asImmutableList via extension? I think it would be more similar to above and dart:html interfaces and JSImmutableListWrapper and JSLiveNodeListWrapper could then be used internally or even marked as private.

  • While doing this I noticed ListMixin and ListBase is going to be deprecated in core? Could implementing List interface directly by both those wrappers while making changes here give any benefits?


  • I’ve reviewed the contributor guide and applied the relevant portions to this PR.

@gemini-code-assist
Copy link

Summary of Changes

Hello @fsw, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request enhances the package:web library by reintroducing a convenient mutable List interface for interacting with DOM child elements and nodes. By implementing a JSLiveNodeListWrapper and providing extension methods, developers can now perform common DOM manipulation tasks using standard Dart List operations directly on live HTMLCollection and NodeList objects, improving developer experience and code readability.

Highlights

  • Mutable List Interface for DOM Nodes: Introduces a mutable List interface for children and childNodes in package:web, similar to the functionality previously available in dart:html.
  • New JSLiveNodeListWrapper Class: Adds JSLiveNodeListWrapper to enable mutable operations on live HTMLCollection and NodeList objects, requiring a container node for additions and removals.
  • Extension Methods for Convenience: Provides childrenAsList (for Element) and childNodesAsList (for Node) extension methods, allowing direct use of Dart List methods like removeWhere and add.
  • Changelog and Tests: Updates the CHANGELOG.md to reflect the new features and adds comprehensive tests in helpers_test.dart to validate the mutable list operations.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a very useful feature, providing a mutable List interface for childNodes and children, similar to the old dart:html API. The overall approach of using a wrapper class is solid.

My review includes a critical fix for a type safety issue in JSLiveNodeListWrapper that could lead to incorrect behavior when manipulating lists of elements. I've also included a few medium-severity suggestions to improve documentation, robustness, and test coverage.

Regarding your questions:

  • Extending JSImmutableListWrapper is a good approach to reuse code.
  • Your chosen implementation is more powerful than adding extension methods like removeWhere directly, as it provides the full mutable List API.
  • Adding an asImmutableList for static NodeLists is a great idea for API consistency and could be a good follow-up.
  • ListMixin is not deprecated and is the correct tool for this job.

Co-authored-by: Kevin Moore <kevmoo@users.noreply.github.com>
@fsw
Copy link
Contributor Author

fsw commented Dec 11, 2025

Copy link
Contributor

@srujzs srujzs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this implementation make sense and matches ideas behind helpers in this module? JSLiveNodeListWrapper don't have to extend JSImmutableListWrapper but have its own implementation but it kinda felt it makes sense this way.

I think so! I'll say that it might better if we started from the dart:html implementations instead. I say this mostly because if users are trying to use these helpers to bridge gaps, divergence in behavior would be painful to discover. See _ChildNodeListLazy and _ChildrenElementList. It's fine if we think some of the dart:html implementation methods need to be updated, though.

could/should NodeList (returned f.e. from querySelector) be extended with asImmutableList via extension? I think it would be more similar to above and dart:html interfaces and JSImmutableListWrapper and JSLiveNodeListWrapper could then be used internally or even marked as private.

Can you elaborate on what you mean here? Do you mean only offer an extension that does the wrapping?

While doing this I noticed ListMixin and ListBase is going to be deprecated in core? Could implementing List interface directly by both those wrappers while making changes here give any benefits?

I don't think they are but those comments are indeed suspect. @lrhn, is this something we should worry about?

if (value > length) {
throw UnsupportedError('Cannot add null to live node List.');
}
for (var i = length - 1; i >= value; i--) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this is new compared to the dart:html version. I think that's okay though since we were throwing in the dart:html version anyways.

Copy link
Contributor Author

@fsw fsw Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, it is here to reuse JSImmutableListWrapper logic without overriding many methods

@override
void removeRange(int start, int end) {
RangeError.checkValidRange(start, end, length);
for (var i = 0; i < end - start + 1; i++) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also new compared to the dart:html version, but again, it was throwing before so this is okay.

void removeRange(int start, int end) {
RangeError.checkValidRange(start, end, length);
for (var i = 0; i < end - start + 1; i++) {
parentNode.removeChild(this[start]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be start + i? end is non-inclusive as well so that might be another issue.

I think this would be less confusing if we just modified the given parameter e.g.

for (; start < end; start++) {
  parentNode.removeChild(this[start]);
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

d'oh, indeed I made end inclusive, fixed and added tests.

But this[start] is OK. This does not look intuitive but removeChild(N) shifts node list and element N+1th becomes Nth immediately. So for removeRange(5, 8) we want to call removeChild(5) 3 times

We can do something like:

for (; start < end; end--) {
  parentNode.removeChild(this[start]);
}

But not sure if this would be less confusing.

/// [live](https://developer.mozilla.org/en-US/docs/Web/API/NodeList#live_vs._static_nodelists)
/// can be safely modified at runtime. This requires an instance of `P`, a
/// container that elements would be added to or removed from.
class JSLiveNodeListWrapper<P extends Node, T extends JSObject, U extends Node>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dart:html has overrides for first and last for children that use firstElementChild and lastElementChild, respectively.

I think this is the same as item(0) and item(len - 1) for children, but may be worth double-checking.

remove as well is a bit tricky because we should likely use removeChild instead of the gap-closing that ListMixin does.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this is realiable source:
https://stackoverflow.com/questions/43324751/is-there-a-difference-between-children0-and-firstelementchild
seems difference is only in returned value when list is empty but we throw exceptions in both cases.

I've added remove. It was indeed tricky as type of parameter in List interface is Object?, please take a look if type checking i used makes sense.

}

extension NodeExtension on Node {
/// Returns [childNodes] as a modifiable [List]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Dart doc comments should be full sentences.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I thought subjectless sentences are allowed? (sorry, I am not native speaker) or do you mean just the dot at the end?


extension NodeExtension on Node {
/// Returns [childNodes] as a modifiable [List]
List<Node> get childNodesAsList => JSLiveNodeListWrapper(this, childNodes);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this should be nodesAsList, because that was a special helper that dart:html provided in addition to childNodes (which was an immutable list).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I cant see nodesAsList but nodes in dart:html. I think this is less confusing 'nativeMethod+AsList' but up to you of course.

@fsw
Copy link
Contributor Author

fsw commented Dec 12, 2025

Does this implementation make sense and matches ideas behind helpers in this module? JSLiveNodeListWrapper don't have to extend JSImmutableListWrapper but have its own implementation but it kinda felt it makes sense this way.

I think so! I'll say that it might better if we started from the dart:html implementations instead. I say this mostly because if users are trying to use these helpers to bridge gaps, divergence in behavior would be painful to discover. See _ChildNodeListLazy and _ChildrenElementList. It's fine if we think some of the dart:html implementation methods need to be updated, though.

Hmm, I was trying to reuse what we have in JSImmutableListWrapper and put both in single implementation.
But having separate implementation for both cases can indeed be beneficial. I will try to rewrite this.

could/should NodeList (returned f.e. from querySelector) be extended with asImmutableList via extension? I think it would be more similar to above and dart:html interfaces and JSImmutableListWrapper and JSLiveNodeListWrapper could then be used internally or even marked as private.

Can you elaborate on what you mean here? Do you mean only offer an extension that does the wrapping?

Precisely. Something like:

extension NodeListExtension on NodeList {
  /// Returns node list as a modifiable [List].
  List<Element> get asList => JSImmutableListWrapper(this);
}

To have a somehow unified interface. (field is native, fieldAsList implements list, querySelector(X) is native and querySelector(X).asList implements list)

@ykmnkmi
Copy link
Contributor

ykmnkmi commented Dec 13, 2025

If we have List<JSAny?>.toJSProxyOrRef can we have JSArray.toDartProxyOrRef?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants