Wupper

Dart Web UI Framework based on HTML Elements inspired by Flutter

Usage

Motivation

There are not enough JavaScript frameworks out there! Let's create one with Dart, which feels like programming with Flutter!

Our requirements when starting with this were:

  • Create a web app with dart so we can use our existing libraries
  • We have experience with Flutter so create something that works and feels similar
  • Use a UI framework with Dart on Web which works:
    • Dart-Angular seems to have no future
    • Vue-Dart has probably been canceled
    • Dart-React has no Null Safety and even no Type Safety

This made us creating a very minimal own single-page-application framework with Dart on web:

Widgets

Similar to Flutter we say: Everything is a widget. But instead of using Flutter widgets we use HTML Elements to create widgets with them.

A basic widget looks like this:

class TodoListItem extends Widget {
  final String todo;

  TodoListItem({required this.todo});

  @override
  Element build() => lIElement(
        children: [
          paragraphElement(text: todo),
          buttonElement(
            text: 'X',
            onClick: (_) => findParent<TodoListPage>().removeTodo(todo),
          ),
        ],
      );
}

Use normal final variables for parameters and State variables for state management. It will have a getter and setter:

class CounterPage extends Widget {
  CounterPage({required this.title});

  final String title;
  final State<int> count = State(0);

  @override
  Element build() {
    return divElement(
      children: [
        headingElementH1(text: title),
        count.bind((count) => paragraphElement(text: 'Counter: $count')),
        buttonElement(
          text: 'Counter +',
          onClick: (_) => count.set(count.state + 1),
        ),
      ],
    );
  }
}

If you just want to bind the text node of an element you can use State.bindText():

count.bindText(
  paragraphElement(),
  (count) => 'Counter: $count'
),

You can also use State.bindAttribute() to only update a single attribute:

count.bindAttribute(
  paragraphElement(),
  'style',
  (count) => count == 0 ? 'color: blue;' : 'color: red;',
),

The widget class must extend Widget and at least implement a build method. This method must return a HTML Element. We can compose a widget from other widgets as well using WidgetClass().appendTo(this).

class TodoListPage extends Widget {
  final InputElement textField = inputElement(
    type: 'text',
    placeholder: 'New todo',
  );
  State<List<String>> todos = State([]);

  void addTodoAction([_]) {
    final value = textField.value;
    if (value == null || value.isEmpty) return;
    todos.set(todos.state.add(value));
    textField.value = '';
  }

  void removeTodo(String todo) {
    todos.set(todos.state..removeWhere((t) => t == todo));
  }

  @override
  Element build() => divElement(
        className: 'container',
        children: [
          textField,
          buttonElement(
            text: 'Add',
            onClick: addTodoAction,
          ),
          uListElement(
            children: [
              for (final todo in todos) TodoListItem(todo: todo).appendTo(this),
            ],
          ),
        ],
      );
}

With .appendTo(this) this widget becomes part of the other widget and receives a link back to it's parent. So we make sure that every widget always knows their parent widget and we get a "widget tree". We can use this to find a specific parent somewhere up in it by using findParent<Type>(). This works very similar to Provider in Flutter:

findParent<TodoListPage>().removeTodo(todo)

We also have a very basic hash router:

class TodoApp extends Widget {
  @override
  Element build() {
    return BasicRouter(routeBuilder: (route) {
      switch (route) {
        case '/':
          return TodoListPage();
        default:
          return NotFoundPage();
      }
    }).appendTo(this);
  }
}

class NotFoundPage extends Widget {
  @override
  Element build() => paragraphElement(text: '404: Not found');
}

FutureBuilder and StreamBuilder

You have FutureBuilders and StreamBuilders which are working similar to Flutter.

FutureBuilder

Similar to Flutters FutureBuilder but without AsyncSnapshot. Build something depending of the result of a future. Optional you can set an errorBuilder to handle errors happening while waiting on the future.

Example:
FutureBuilder<String>(
  future: loadDataFromServer(),
  builder: (AsyncSnapshot<String> snapshot, Widget parent) =>
    divElement(text: snapshot.data ?? snapshot.error?.toString() ?? 'No data yet'),
).appendTo(this);

StreamBuilder

Similar to Flutters StreamBuilder but without AsyncSnapshot. Constantly rebuild something depending of the last value of a stream. Optional you can set an errorBuilder to handle errors.

Example:
StreamBuilder<String>(
  stream: someDataStream,
  builder: (AsyncSnapshot<String> snapshot, Widget parent) =>
    divElement(text: snapshot.data ?? snapshot.error?.toString() ?? 'No data yet'),
).appendTo(this);

runApp and widget tree

Once we have our first widgets we need to append them to the DOM. We can do this by using runApp():

void main() => runApp((Map<String, String> args) => TodoApp());

By default, this appends the whole web app to the element with the ID app. We can change this by:

void main() => runApp((Map<String, String> args) => TodoApp(), target = document.querySelector(#'other-id'));

It is important to understand how the widget tree is related to the DOM! All widgets are represented as an element in the DOM, so we can see the widget tree in it.

For example our TodoListItem widget would look like this:

<li data-widget-type="TodoListItem" data-widget-id="774243307">
  <p>Buy milk</p>
  <button onClick="minified:">X</button>
</li>

The data-widget-type becomes "minified" in production.

Libraries

wupper