Component System

An overview of Jaspr's component system.


Jaspr comes with a component system that is very similar to Flutter's widgets. This is a core design decision of jaspr, in order to look and feel very familiar to developers coming from Flutter. You might think of it as just replacing the word 'Widget' with 'Component' (which actually was a small part of Jaspr's development process ๐Ÿ˜‰), while keeping the same structure and behavior.

When building an app or website with Jaspr you will mostly use these three basic components:

StatelessComponent

A custom component that does not require any mutable state and looks like this:

dart
class MyComponent extends StatelessComponent {
  const MyComponent({Key? key}): super(key: key);

  @override
  Component build(BuildContext context) {
    return p([ .text('Hello World') ]);
  }
}

Similar to Flutter, this component:

  • extends the abstract StatelessComponent class,
  • has a constructor receiving a Key and optionally some custom parameters,
  • has a build() method receiving a BuildContext.

StatefulComponent

A custom component that has mutable state and looks like this:

dart
class MyComponent extends StatefulComponent {
  const MyComponent({Key? key}): super(key: key);

  @override
  State createState() => MyComponentState();
}

class MyComponentState extends State<MyComponent> {

  @override
  Component build(BuildContext context) {
    return p([ .text('Hello World') ]);
  }
}

Similar to Flutter, this component:

  • extends the abstract StatefulComponent class,
  • has a constructor receiving a Key and optionally some custom parameters,
  • has a createState() method returning an instance of its custom state class

and has an associated state class that:

  • extends the abstract State<T> class,
  • has a build() method inside the state class receiving a BuildContext,
  • can have optional initState(), didChangeDependencies(), and other lifecycle methods.

InheritedComponent

A base class for components that efficiently propagate information down the tree and looks like this:

dart
class MyInheritedComponent extends InheritedComponent {
  const MyInheritedComponent({required super.child, super.key}) ;

  static MyInheritedComponent of(BuildContext context) {
    final MyInheritedComponent? result = context.dependOnInheritedComponentOfExactType<MyInheritedComponent>();
    assert(result != null, 'No MyInheritedComponent found in context');
    return result!;
  }

  @override
  bool updateShouldNotify(covariant MyInheritedComponent oldComponent) {
    return false;
  }
}

In every aspect, this component behaves the same as Flutter's InheritedWidget.

Foundation Components

Jaspr has three foundational component types, which are accessible through factory constructors on the Component class: Component.element(), Component.text() and Component.fragment(). Additionally, there are two more Component.empty() and Component.wrapElement() which are all explained below.

Component.element()

The foundational component that renders a single html element with a tag, attributes and other parameters, as well as a list of child components.

The Component.element() factory constructor always creates a component of type DomComponent.

dart
final component = Component.element(
  tag: 'div',
  id: 'my-id',
  classes: 'class-a class-b',
  styles: Styles(color: Colors.black),
  attributes: {'my-attribute': 'my-value'},
  events: {'click': (e) => print('clicked')},
  children: [
    /* ... */
  ],
);

// The same component, but using HTML components instead.
final component2 = div(
  id: 'my-id', 
  classes: 'class-a class-b',
  styles: Styles(color: Colors.black),
  attributes: {'my-attribute': 'my-value'},
  events: {'click': (e) => print('clicked')},
  [
    /* ... */
  ],
);

Either component renders the following HTML:

html
<div id="my-id" class="class-a class-b" style="color: black;" my-attribute="my-value">
  ...
</div>

See Writing HTML for more details and examples.

Component.text()

A simple component that renders a text node. A text node in html is just some standalone string that is placed inside another html element. Therefore the Component.text() constructor also only receives a single string to render to the page.

The Component.text() factory constructor always creates a component of type Text.

dart
final component = Component.text('Hello World!');

// The same component, but using dot-shorthands instead.
final Component component2 = .text('Hello World!');

Component.fragment()

A component that renders its child components without any wrapper element.

This is meant to be used in places where you want to render multiple components / elements, but only a single component is allowed by the API, like in the build() method of stateless and stateful components.

The Component.fragment() factory constructor always creates a component of type Fragment.

dart
final component = Component.fragment([
  Component.element(tag: 'h1', children: [Component.text('Welcome')]),
  Component.element(tag: 'p', children: [Component.text('Hello World')]),
]);

// The same component, but using dot-shorthands and HTML components instead.
final Component component2 = .fragment([
  h1([ .text('Welcome')]),
  p([ .text('Hello World')]),
]);

Either component renders the following HTML:

html
<h1>Welcome</h1>
<p>Hello World</p>

Component.empty()

A helper constructor that creates an empty fragment, thus rendering nothing.

This is useful when you want to return "nothing" from a build method.

dart
final component = Component.empty();

Component.wrapElement()

A component which applies its attributes and parameters (like classes, styles,) etc.) to its direct child element(s).

This does not create a HTML element itself. All properties are merged with the respective child element's properties, with the child's properties taking precedence where there are conflicts.

dart
final component = Component.wrapElement(
  classes: 'wrapping-class',
  styles: Styles(backgroundColor: Colors.blue, padding: Padding.all(8.px)),
  child: div(
    classes: 'some-class',
    styles: Styles(backgroundColor: Colors.red),
    [
      .text('Hello World'),
    ],
  ),
);

The above component renders the following HTML:

html
<div class="wrapping-class some-class" style="padding: 8px; background-color: red;">
  Hello World
</div>

Formatting Whitespace

When pre-rendering your components in server and static mode, Jaspr will output cleanly formatted html on a best-effort basis. This means it will add newlines and indentations to your html element, while trying to not affect the way the html is rendered.

For example let's look at this simple Jaspr code:

dart
div([
  b([ .text('A') ]),
  em([ .text('B') ]),
  span([ .text('C') ]),
])

Which will generate the following html:

html
<div>
  <b>A</b>
  <em>B</em>
  <span>C</span>
</div>

Here Jaspr adds newlines and indentation for each child element. However in some cases, you might not want Jaspr to introduce extra whitespace because it would affect the way the html is rendered.

If you don't want Jaspr to format the output html for a part of your code, wrap it in a <span> element. This works since Jaspr will not apply any additional formatting to <span> elements.

So for example this code:

dart
span([
  b([ .text('A') ]),
  em([ .text('B') ]),
  span([ .text('C') ]),
]),

will generate to following html:

html
<span><b>A</b><em>B</em><span>C</span></span>