Component System
An overview of Jasprs component system.
Jaspr comes with a component system that is very similar to Flutters 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 jasprs development process ๐), while keeping the same structure and behavior.
The following page and other documentation assumes, that you have already a basic understanding of Flutters widget system, especially the widget tree and the three base widgets: StatelessWidget, StatefulWidget and InheritedWidget.
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:
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 aBuildContext
.
StatefulComponent
A custom component that has mutable state and looks like this:
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 aBuildContext
, - 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:
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 Flutters 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.
Other than Flutter, Jaspr has a fixed number of foundational components which make up all other components and represent the core building blocks of HTML: element nodes and text nodes. Flutter is different because it allows you to write custom render objects with your own layouting and painting logic. This isn't possible in Jaspr because with HTML, the browser handles both layouting and painting.
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
.
In general, it is recommended to use the html utility methods like div()
, button()
. You can use Component.element()
directly for lower-level control (mainly specifying the tag as a String
), but is more verbose.
var 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 the html utility method instead.
var 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:
<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
.
As it is recommended to use div()
and other html methods over Component.element()
, the text()
utility method may also be used over the Component.text()
constructor for consistency.
var component = Component.text('Hello World!');
// The same component, but using the utility method instead.
var component2 = text('Hello World!');
As usual for web, styling is done through a combination of CSS attributes, either in a Stylesheet or inline though the styles
attribute of the parent elements.
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
.
As it is recommended to use div()
and other html methods over Component.element()
, the fragment()
utility method may also be used over the Component.fragment()
constructor for consistency.
var 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 the utility methods instead.
var component2 = fragment([
h1([text('Welcome')]),
p([text('Hello World')]),
]);
Either component renders the following 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.
var component = Component.empty();
// The same component, but using the utility method
var component2 = fragment([]);
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.
var 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:
<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 lets look at this simple Jaspr code:
div([
b([text('A')]),
em([text('B')]),
span([text('C')]),
])
Which will generate the following 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 and additional formatting to <span>
elements.
So for example this code:
span([
b([text('A')]),
em([text('B')]),
span([text('C')]),
]),
will generate to following html:
<span><b>A</b><em>B</em><span>C</span></span>