Flutter Widgets as HTMLElement
The enterprise version of WebF includes a redesigned Flutter widget adapter that offers deeper integration with Flutter widgets and an improved non-blocking asynchronous binding API.
In a WebF application, any Flutter Widget can be transformed into its foundational unit — an HTMLElement.
Utilizing the WebF Widget Adaptor, any Flutter Widget can be converted into an HTMLElement node, allowing it to be seamlessly integrated into a WebF page.
Defining a Custom Element for the Web
Introduction to the WidgetElement
Class
The WidgetElement
class plays an instrumental role by bridging the gap between Flutter's Widgets and the web's
HTMLElements.
In the Dart ecosystem, when you create a subclass of WidgetElement
and use Flutter Widgets inside its build
method,
you're essentially designing a custom HTMLElement. This offers a unique advantage: the ability to seamlessly integrate a
Flutter Widget into web applications using this custom HTMLElement tag.
These custom elements behave analogously to standard Web Components in modern browsers. For web developers, these Flutter widget-based custom elements can be regarded as pre-defined Web Components.
Example 1: Implementing a Video Player in WebF
This demonstration elucidates the process of incorporating the video_player package from pub.dev into web applications built with WebF. Here's a step-by-step guide:
Adding the Dependency: First and foremost, include the
video_player
package in your pubspec.yaml file.Creating the Subclass: Subsequently, define a new class named
VideoElement
that extends WidgetElement.class VideoPlayerElement extends WidgetElement {
VideoPlayerElement(super.context);
// Additional attributes and methods...
Widget build(BuildContext context, ChildNodeList children) {
return SingleChildScrollView(
child: Column(
children: <Widget>[
Container(
padding: const EdgeInsets.only(top: 20.0),
),
const Text('With assets mp4'),
Container(
padding: const EdgeInsets.all(20),
child: AspectRatio(
aspectRatio: _controller.value.aspectRatio,
child: Stack(
alignment: Alignment.bottomCenter,
children: <Widget>[
VideoPlayer(_controller),
_ControlsOverlay(controller: _controller),
VideoProgressIndicator(_controller, allowScrubbing: true),
],
),
),
),
],
),
);
}
}
Interestingly, the WidgetElement shares many similarities with the State associated with StatefulWidget in Flutter. So, if you're already acquainted with typical Flutter app development, working with WidgetElement should come naturally.
- Incorporate Custom Elements in Your Web App
To register your class as a custom element within the WebF environment, utilize the WebF.defineCustomElement
method.
This not only registers the class but also associates a custom element tag with it.
// Dart code
void main() {
WebF.defineCustomElement(
'video-player', (context) => VideoPlayerElement(context));
}
After this step, your Web app can conveniently spawn instances of your widget element class using either
the document.createElement
method or directly in HTML, based on the logic within your build
method.
Instantiating the Custom Element in JavaScript
In a web application, your custom element can be instantiated through JavaScript:
// JavaScript Code
const videoPlayerElement = document.createElement('video-player');
document.body.appendChild(videoPlayerElement);
Alternatively, directly employ the custom element within your HTML:
<!-- HTML Code -->
<div class="container">
<h3>Video Title</h3>
<video-player id="video_player"/>
</div>
For developers using frameworks like Vue, the custom element—crafted from a Flutter widget—can be utilized just like any standard HTML element:
<!-- Vue.js Code -->
<template>
<img alt="Vue logo" src="./assets/logo.png">
<video-player
ref="videoPlayer"
src="https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4"
@canplay="handleCanPlay"
@playing="handlePlaying"
@paused="handlePaused"
>
</video-player>
<div class="status-bar">
Player Status: {{state}}
</div>
</template>
<script>
export default {
name: 'App',
methods: {
handleCanPlay() {
this.state = 'canplay';
},
handlePlaying() {
this.state = 'playing';
},
handlePaused() {
this.state = 'paused';
}
}
}
</script>
Define API and Events
Methods and properties for custom elements can be 100% defined and implemented in Dart. Once you have defined your properties and methods for your custom element, WebF's binding system will create the corresponding JavaScript API for you can let you easily communicate with JavaScript.
Defining Properties
To enhance your custom element with additional properties, declare a StaticDefinedBindingPropertyMap
to define the extended properties for your WidgetElement
, and append it to the properties
getter.
static StaticDefinedBindingPropertyMap videoPlayerProperties = {
'src': StaticDefinedBindingProperty(
getter: (element) => castToType<VideoPlayerElement>(element).src,
setter: (element, value) =>
castToType<VideoPlayerElement>(element).src = value,
),
};
List<StaticDefinedBindingPropertyMap> get properties => [
...super.properties,
videoPlayerProperties,
];
The StaticDefinedBindingProperty
class provides both getter and setter callbacks, enabling you to manage the property's value dynamically. Ensure that the getter
and setter
implementations correctly cast the element type and handle the property's behavior as intended.
When JavaScript seeks to retrieve a value using this property, the getter
callback gets activated.
Conversely, when JavaScript intends to assign a value to this property, the setter
callback comes into play.
On the JavaScript side, if you're manipulating the DOM element directly, you can effortlessly interact with these properties on the DOM instance:
// JavaScript Implementation
const videoPlayerElement = document.createElement('video-player');
// Retrieve the value returned from Dart
console.log(videoPlayerElement.src); // Outputs: [VIDEO SRC]
// Assign a value to Dart
videoPlayerElement.src = 'NEW VIDEO SRC'; // This action will activate the setter callback in Dart.
For those utilizing specific frameworks, it's advisable to access the DOM instance through the respective framework's provided API.
As an illustration, Vue developers can employ the ref()
function to fetch the DOM instance and then extract the value
sourced from Dart.
<template>
<video-player
ref="videoPlayer"
></video-player>
</template>
<script>
export default {
name: 'App',
mounted() {
console.log(this.$refs['videoPlayer'].src); // Outputs: [VIDEO SRC]
}
}
</script>
Use an Asynchronous Approach to Read Properties
In dedicated thread mode, accessing properties blocks the JavaScript thread while waiting for the property getter to
return. For better performance optimization, JavaScript developers are encouraged to use a more efficient approach to
access properties on widgetElement
without blocking the JavaScript thread.
By appending the _async
suffix to the synchronous property, a Promise object is returned to JavaScript, enabling an
asynchronous approach:
console.log(this.$refs['videoPlayer'].src); // The synchronous approach
console.log(this.$refs['videoPlayer'].src_async); // The asynchronous approach, which returns a Promise instead of the actual value.
Use the .then
method or the async
/await
keywords to handle asynchronous access:
async function getSrc() {
return await this.$refs['videoPlayer'].src_async;
}
Defining Methods
Incorporation methods to your custom elements in WebF closely parallels the approach for adding properties. WebF's binding system auto-generates the relevant JavaScript functions corresponding to your Dart methods.
Defining Methods
To equip your custom element with additional methods, declare a StaticDefinedSyncBindingObjectMethodMap
to define the extended synchronous methods for your WidgetElement
and append it to the methods
getter. Similarly, declare a StaticDefinedAsyncBindingObjectMethodMap
for asynchronous methods and append it to the asyncMethods
getter.
static StaticDefinedSyncBindingObjectMethodMap videoPlayerSyncMethods = {
'status': StaticDefinedSyncBindingObjectMethod(
call: (element, args) {
return castToType<VideoPlayerElement>(element).status(args);
},
),
};
List<StaticDefinedSyncBindingObjectMethodMap> get methods => [
...super.methods,
videoPlayerSyncMethods,
];
static StaticDefinedAsyncBindingObjectMethodMap videoPlayerAsyncMethods = {
'play': StaticDefinedAsyncBindingObjectMethod(
call: (element, args) async {
return castToType<VideoPlayerElement>(element).play(args);
},
),
};
List<StaticDefinedAsyncBindingObjectMethodMap> get asyncMethods => [
...super.asyncMethods,
videoPlayerAsyncMethods,
];
Post initialization, the custom element instance on the JavaScript side will possess a play()
that returns a Promise.
If directly interacting with the DOM element, these methods can be invoked with ease:
// JavaScript Code
const videoPlayerElement = document.createElement('video-player');
// Invoke the `status` method, returned in sync results, as crafted in Dart.
videoPlayerElement.status();
// Invoke the `play` method, returned with a Promise object, as corresponding to the Future in Dart.
await videoPlayerElement.play();
For developers working within distinct frameworks, it's recommended to fetch the DOM instance using the framework's native API.
For instance, in Vue, the ref()
function allows developers to obtain the DOM instance, subsequently facilitating the
invocation of functions defined in Dart.
<template>
<video-player
ref="videoPlayer"
src="https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4"
></video-player>
<div class="video-player-control">
<div class="control-button" @click="handlePlay">Play</div>
<div class="control-button" @click="handlePause">Pause</div>
</div>
</template>
<script>
export default {
name: 'App',
methods: {
handlePlay() {
this.$refs['videoPlayer'].play();
},
handlePause() {
this.$refs['videoPlayer'].pause();
}
}
}
</script>
Use an Asynchronous Approach to Synchronous Functions
In dedicated thread mode, calling synchronous methods blocks the JavaScript thread while waiting for the Dart synchronous methods to return. For better performance optimization, JavaScript developers are encouraged to use a more efficient approach to call the same Dart synchronous method on widgetElement
without blocking the JavaScript thread.
By appending the _async
suffix to the synchronous Dart method name, a Promise object is returned to JavaScript, enabling an asynchronous approach:
this.$refs['videoPlayer'].status(); // This function is synchronous
this.$refs['videoPlayer'].status_async(); // Calls the same synchronous `status` method in Dart but returns a Promise object.
Use the .then
method or the async
/await
keywords to handle asynchronous execution:
async function play() {
await this.$refs['videoPlayer'].status_async();
}
Handling Events from Dart
Events are essential when Dart aims to inform the JavaScript side and initiate a callback for specific purposes.
In the WidgetElement
class, the dispatchEvent
function allows you to send events to the JavaScript side and invoke
an event listener callback.
onCanPlay() {
// Dispatch an event to the JavaScript side to trigger a callback.
dispatchEvent(Event('canplay'));
}
If you wish to embed additional data within the event, consider using the CustomEvent
class rather than the Event
class.
onCanPlay() {
CustomEvent customEvent = CustomEvent('canplay', detail: 'YOUR DATA');
// Dispatch this custom event to the JavaScript side to trigger a callback.
dispatchEvent(customEvent);
}
On the JavaScript side, when interacting directly with the DOM element, the addEventListener
function allows you to
register a callback that awaits notification from Dart.
For web developers, the events triggered by WidgetElement align seamlessly within standard W3C Event events, Hence handling these events remains consistent.
// JavaScript Code
const videoPlayerElement = document.createElement('video-player');
// Register a callback for the `play` event dispatched from the Dart side.
videoPlayerElement.addEventListener('play', (e) => {
// If the event object is a CustomEvent, access e.detail to retrieve the data sent from Dart.
});
For those working with specific frameworks, the majority provide built-in methods to handle these event callbacks and invoke associated methods.
For example, in Vue, simply prefix the event name with @ to register the event handler for your custom element tags.
<template>
<video-player
ref="videoPlayer"
src="https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4"
@canplay="handleCanPlay"
@playing="handlePlaying"
@paused="handlePaused"
></video-player>
</template>
<script>
export default {
name: 'App',
methods: {
handleCanPlay() {
this.state = 'canplay';
},
handlePlaying() {
this.state = 'playing';
},
handlePaused() {
this.state = 'paused';
},
}
}
</script>
Use CSS Styles to Control Your Customized UI
Custom elements can be styled with CSS. This includes setting width and height, arranging them alongside other regular elements, and using positioning to place them in specific locations.
For instance, to ensure the video player always remains visible on the screen, you can use position: fixed
.
<template>
<video-player
id="video-player"
ref="videoPlayer"
/>
</template>
<style>
#video-player {
width: 300px;
position: fixed;
left: 0;
top: 0;
right: 0;
margin: auto;
}
</style>
Use CSS Properties to Customize the Behavior of Your Widget Elements
Matched CSS properties for your custom elements can be accessed via the renderStyle
object in your build()
methods. These properties allow you to adjust the layout and visual effects based on the CSS values.
For example, consider a custom element called <flutter-search />
that accepts the CSS properties font-size
and border-radius
to customize its appearance:
WebF.defineCustomElement('flutter-search', (context) => FlutterSearch(context));
class FlutterSearch extends WidgetElement {
FlutterSearch(super.context);
final TextEditingController _controller = TextEditingController();
bool _hasText = false;
void initState() {
super.initState();
_controller.addListener(() {
setState(() {
_hasText = _controller.text.isNotEmpty;
});
});
}
Widget build(BuildContext context, ChildNodeList childNodes) {
return Container(
child: TextField(
maxLines: 1,
controller: _controller,
decoration: InputDecoration(
prefixIcon: Icon(Icons.search, color: Colors.grey),
suffixIcon: _hasText
? IconButton(
icon: Icon(Icons.clear, color: Colors.grey),
onPressed: () {
_controller.clear(); // Clear the input
},
)
: null,
hintText: 'Search', // Placeholder text
hintStyle: TextStyle(color: Colors.grey, fontSize: 12), // Placeholder style
border: OutlineInputBorder(
borderRadius: renderStyle.borderRadius != null
? BorderRadius.only(
topLeft: renderStyle.borderRadius![0],
topRight: renderStyle.borderRadius![1],
bottomRight: renderStyle.borderRadius![2],
bottomLeft: renderStyle.borderRadius![3],
)
: BorderRadius.zero,
borderSide: BorderSide.none, // No default border
),
focusedBorder: OutlineInputBorder(
borderRadius: renderStyle.borderRadius != null
? BorderRadius.only(
topLeft: renderStyle.borderRadius![0],
topRight: renderStyle.borderRadius![1],
bottomRight: renderStyle.borderRadius![2],
bottomLeft: renderStyle.borderRadius![3],
)
: BorderRadius.zero, // Rounded corners
borderSide: BorderSide(color: Colors.blue, width: 1.0), // Outline when focused
),
contentPadding: EdgeInsets.symmetric(vertical: 12.0), // Vertical padding
),
style: TextStyle(
overflow: TextOverflow.visible, // Handles text overflow
fontSize: renderStyle.fontSize.computedValue, // Adjust font size dynamically
),
),
);
}
}
With this setup, the internal font size and border radius of the <flutter-search />
element can be customized using the following HTML and CSS:
<flutter-search id="search"></flutter-search>
#search {
font-size: 16px; /* Adjusts the font size for the input text */
border-radius: 8px; /* Changes the radius of the input's corners */
}
Embedding HTML Elements as Children of Custom Elements
Custom elements can seamlessly integrate with standard HTML elements to construct more intricate components.
For example, you can design a complex component where the outer framework is created using Flutter widgets, while the inner content is structured using HTML and CSS. This combination provides exceptional flexibility and power.
Demo
In this demo, we define two custom elements using Flutter: <flutter-tab>
and <flutter-tab-item>
.
WebF.defineCustomElement('flutter-tab', (context) => FlutterTab(context));
WebF.defineCustomElement('flutter-tab-item', (context) => FlutterTabItem(context));
The following code converts the childNodes
of the DOM tree into a list of TabData
widgets, which form the basic tab items of the DynamicTabBarWidget
:
List<TabData> tabs = childNodes.whereType<dom.Element>().map((element) {
return TabData(
index: _index++,
title: Tab(
child: Text(element.getAttribute('title') ?? ''),
),
content: element.toWidget(key: ObjectKey(element)),
);
}).toList(growable: false);
Full Implementation
import 'package:dynamic_tabbar/dynamic_tabbar.dart';
class FlutterTab extends WidgetElement {
FlutterTab(super.context);
bool isScrollable = false;
bool showNextIcon = true;
bool showBackIcon = true;
Widget build(BuildContext context, ChildNodeList childNodes) {
int _index = 0;
List<TabData> tabs = childNodes.whereType<dom.Element>().map((element) {
return TabData(
index: _index++,
title: Tab(
child: Text(element.getAttribute('title') ?? ''),
),
content: element.toWidget(key: ObjectKey(element)),
);
}).toList(growable: false);
return DynamicTabBarWidget(
dynamicTabs: tabs,
isScrollable: isScrollable,
onTabControllerUpdated: (controller) {
controller.index = 0;
},
onTabChanged: (index) {},
onAddTabMoveTo: MoveToTab.last,
showBackIcon: showBackIcon,
showNextIcon: showNextIcon,
);
}
}
class FlutterTabItem extends WidgetElement {
FlutterTabItem(super.context);
Widget build(BuildContext context, ChildNodeList childNodes) {
return WebFHTMLElement(
tagName: 'DIV',
controller: ownerDocument.controller,
children: childNodes.toWidgetList(),
);
}
}
HTML Usage
Once the custom elements are defined in Dart, you can use them in your HTML as follows:
<flutter-tab>
<flutter-tab-item title="Relative">
<div>This is Relative</div>
</flutter-tab-item>
<flutter-tab-item title="Absolute">
<div>This is Absolute</div>
</flutter-tab-item>
<flutter-tab-item title="Fixed">
<div>This is Fixed</div>
</flutter-tab-item>
<flutter-tab-item title="Sticky">
<div>This is Sticky</div>
</flutter-tab-item>
</flutter-tab>