reactify 1.1.1

reactify #

Pub Docs Build Status

Reactive user interface components in pure Dart

Philosophy #

Typed, tested, and tiny.

No framework, minimal dependencies, just two straightforward classes: UserInterface and Component. Harness Dart 2's powerful built-in dart:html library while utilizing reactive best practices:

  • Everything is a Component, which renders as one or more vanilla HTML elements
  • Instead of hard-coding values into components, read values from a "state" property
  • State is passed downwards from root components to sub-components, never upwards
  • Sub-components may be supplied with event listeners that emit state changes back upwards when they are triggered
  • Anytime state is changed, re-render only the affected root component instead of reloading the whole page

Write in Dart, transpile to Javascript, then use in HTML just like you'd expect.

Installing #

Must first install the Dart SDK

Package installation instructions Convention for import: import 'package:reactify/reactify.dart' as reactify;

To run in a browser, transpile into JS using either dart2js or webdev serve. Instructions below

Basic Usage #

For working examples, see the counter (basic) and game (advanced). Source code and documentation here.

If you are unfamiliar with how Dart implements HTML elements and DOM manipulation with dart:html, you may want to reference the excellent official tutorial here.

Start with an HTML file with a script tag and a hookable element.

index.html

<!DOCTYPE html>
<head>
  <script type="text/javascript" defer src="main.dart.js"></script>
</head>
<body>
  <div id="root"></div>
</body>

Declare a UserInterface with at least one Component and insert it into the HTML.

main.dart

import 'dart:html';
import 'package:reactify/reactify.dart' as reactify;

void main() {
  document.getElementById("root").replaceWith(ExampleUI.initialize());
}

final ExampleUI = reactify.UserInterface(components: [ExampleComponent]);

final ExampleComponent = reactify.Component(
    id: 'sample',
    template: (self) => DivElement()..text = self.getState('static'),
    state: {'static': 'This is a sample Component'});

After transpiling Dart to Javascript (see below), index.html renders in a browser as:

<div id="root">
  <div id="component-sample">This is a sample Component</div>
</div>

Demystifying the callback properties #

Reactive web development uses callback functions extensively, and you will find them in three Component properties: template, computedState, and handlers. Callbacks allow the caller to define interactions with a component's other properties, such as its state, before the component has been constructed.

template #

Every Component should have a template. This is a single callback function that gets rendered as an HTML element whenever 1) a UserInterface containing the Component is initialized, or 2) the Component's root state is changed at any point following initialization. The simplest template is template: (_) => DivElement(), which renders as <div></div>.

The callback function accepts one argument, which is a reference to that component itself.

While you may name it however you like, a helpful convention is to name this argument _ if you do not need to acces it, and self if you do, as in: template: (self) => DivElement()..text = self.getState('example').

computedState #

computedState is a map of callback functions that each return a dynamic value. As with template, each callback function accepts one argument, which is a reference to that component itself. These are useful for deriving a value from existing state values or external values.

handlers #

handlers is a map of handlers. Each handler is actually two callback functions chained together: an event listener (callback function #1), which returns a callback containing a reference to the component itself (callback function #2), which may trigger side effects but should return nothing. A helpful convention is to name the Event argument _ if you do not need to access it, and e if you do, as in: handlers: {'exampleHandler': (e) => (self) => self.setState('example', (e.target as InputElement).value)}. This can then be called by a sub-component, as in: InputElement()..onChange.listen((e) => self.getHandler('exampleHandler', e)).

Advanced Usage #

Use computedState and getComputed if a value must be calculated.

final ExampleComponent = reactify.Component(
    id: 'sample',
    template: (self) => DivElement()..text = self.getComputed('computed'),
    computedState: {
      'computed': (_) {
        var calculation = 1 + 1;
        return 'This is a sample Component that equals $calculation';
      }
    });

Use self.injectComponent(subComponent) to insert a sub-component into a root component.

final ExampleComponent = reactify.Component(
    id: 'sample',
    template: (self) => self.injectComponent(SubComponent),
    state: {'root': 'This state has been passed through to a sub-component!'});

final SubComponent = reactify.Component(
    template: (self) => DivElement()..text = self.getState('root'));

Use handlers and getHandler so that the sub-component can trigger changes to root state.

final ExampleComponent = reactify.Component(
    id: 'sample',
    template: (self) =>
        DivElement()..children.add(self.injectComponent(SubComponent)),
    state: {
      'count': 0
    },
    computedState: {
      'computed': (self) {
        var calculation = self.getState('count') + 1;
        return 'This is a computed state that equals $calculation and is updated by a handler callback on a sub-component';
      }
    },
    handlers: {
      'increment': (_) =>
          (self) => self.setState('count', self.getState('count') + 10)
    });

final SubComponent = reactify.Component(
  id: 'sub',
    template: (self) => DivElement()
      ..text = self.getComputed('computed')
      ..children.add(ButtonElement()
        ..text = "+10"
        ..onClick.listen((_) => self.getHandler('increment', _))));

For other snippets, see the Dart UI cookbook

Transpiling to Javascript #

Dart is not supported by browsers natively, so must be converted to Javascript first. Two command line options for this:

  • Option 1: $ dart2js (docs). If you prefer this route for development, I still recommend auto-transpiling on save.*
  • Option 2: $ webdev serve (docs) - enables faster load times, auto-transpiling, and page refreshing on save. Recommended for development, but be aware of the gotchas**. Notable requirements:
    • Working directory must contain a pubspec.yaml file and a /web directory
    • /web must contain a file called index.html which serves as entrypoint for the server
    • pubspec.yaml must specify build_runner, build_web_compilers, and build_daemon as dev_dependencies

*To customize on-save behavior in VSCode, you need a build task and a custom Keyboard Shortcut. A simple build task could be:

  "tasks": [
        {
            "label": "Run dart2js",
            "type": "shell",
            "command": "dart2js",

            "problemMatcher": [
                "$eslint-stylish"
            ],
            "group": {
                "kind": "build",
                "isDefault": true
            },
            "runOptions": {
                "runOn": "default"
            }

and a keyboard shortcut could be:

    {
        "key": "cmd+s",
        "command": "workbench.action.tasks.build",
        "when": "editorTextFocus && editorLangId == 'dart'"
    }

**Gotchas of webdev serve: if /web contains a xxx.dart file, by default it will be transpiled and saved as xxx.dart.js within a hidden output folder. The output folder can be changed, but it cannot be merged with your current working directory. Plus, the file naming convention cannot be changed.

By contrast, dart2js saves the .js file within the current working directory, and the file naming convention can be changed with a flag. Thus, if you want to serve index.html yourself or load it manually in a browser, you can first run dart2js to create the .js file in the current directory.

1.0.0 #

  • Initial version

1.0.1 #

  • Added CHANGELOG
  • Added more hyperlinks to README and docstrings
  • Added meta dependency to identify @required arguments in class constructors

1.0.2 #

  • Added main.dart.js files to examples so that one can view them simply by opening index.html in a browser'

1.0.3 #

  • Added hosted links to examples

1.1.0 #

  • Introduced a private reconciliation algorithm (_reconcile) that compares the virtual DOM before and after state change, and only performs DOM manipulation on elements that have changed. Rules for _reconcile:
    • If two nodes are of different types, replace the entire remaining tree with the replacement node
    • If two nodes have a different number of direct children, replace the entire remaining tree with the replacement node
    • If two nodes have different outer HTML, check for different text (specifically, check for different childNode[0].nodeValue) and different attributes. Wherever there is a discrepancy between prior and replacement, overwrite with the values from replacement
    • Apply _reconcile recursively, so that the same type/numberChildren/text/attribute check is applied to all child elements with different outer HTML
  • Updated _refresh to delegate upwards so that _reconcile is called on root components only

1.1.1 #

  • If a sub-component has no id, then id is blank (instead of setting id to component-null)

example/README.md

Examples #

To view either example in action, click on the link to the hosted version below.

To develop with either example, run $ webdev serve (docs) within the example's directory (must be at the same level as a pubspec.yaml). Useful flags: $ webdev serve --auto refresh --launch-in-chrome

counter #

Live example

A simple counter featuring a root component with state and a sub-component that inherits its handlers from root and reads its own computedState.

catch_me_game #

Live example

A find-the-number game featuring a toggle for the UI's globalState; root component with state, computedState, and handlers; multiple sub-components; a hidden component; conditional logic; and CSS styling.

Use this package as a library

1. Depend on it

Add this to your package's pubspec.yaml file:


dependencies:
  reactify: ^1.1.1

2. Install it

You can install packages from the command line:

with pub:


$ pub get

Alternatively, your editor might support pub get. Check the docs for your editor to learn more.

3. Import it

Now in your Dart code, you can use:


import 'package:reactify/reactify.dart';
  
Popularity:
Describes how popular the package is relative to other packages. [more]
0
Health:
Code health derived from static analysis. [more]
100
Maintenance:
Reflects how tidy and up-to-date the package is. [more]
100
Overall:
Weighted score of the above. [more]
50
Learn more about scoring.

We analyzed this package on Sep 19, 2019, and provided a score, details, and suggestions below. Analysis was completed with status completed using:

  • Dart: 2.5.0
  • pana: 0.12.21

Platforms

Detected platforms: web

Primary library: package:reactify/reactify.dart with components: html.

Dependencies

Package Constraint Resolved Available
Direct dependencies
Dart SDK >=2.0.0 <3.0.0
collection ^1.0.0 1.14.12
meta ^1.0.0 1.1.7
Dev dependencies
test ^1.0.0