Flutter Widgets as HTMLElement
In a WebF application, any Flutter Widget can be transformed into its foundational unit — an HTMLElement. Web pages are fundamentally built from HTMLElement nodes.
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.
Explore the Demo
Checkout this demo that illustrates how to
construct a video player using the video_player
Flutter plugin, and use it as a custom element into the web 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, List<Widget> 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, override the initializeProperties
method
within WidgetElement
.
void initializeProperties(Map<String, BindingObjectProperty> properties) {
super.initializeProperties(properties);
properties['src'] = BindingObjectProperty(getter: () => '[VIDEO SRC]', setter: (src) {
// Action to be taken when JavaScript attempts to update the `src` property of your custom element instance.
});
}
BindingObjectProperty
provides both getter and setter callbacks.
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>
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.
To equip your custom element with additional methods, override the initializeMethods
methods within WidgetElement
:
void initializeMethods(Map<String, BindingObjectMethod> methods) {
super.initializeMethods(methods);
// Here, we're defining a method named 'play' that returns a Promise.
// For synchronous execution, opt for the `BindingObjectMethodSync` class.
methods['play'] = AsyncBindingObjectMethod(call: (args) async {
// This is the action that occurs when JavaScript invokes the `play()` method.
// 'args' will contain parameters passed from the JavaScript side.
// Implement your desired functionality for this method.
// For instance: await _controller.play();
// ...
// Dispatch an event to the JavaScript side to activate a callback.
dispatchEvent(Event('play'));
});
}
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 `play` method, as crafted 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>
Synchronous & Asynchronous Functions
WebF's binding system accommodates both synchronous and asynchronous functions.
Depending on your requirements, you can tailor your functions accordingly.
Here's comparative illustrating Dart methods and their corresponding JavaScript counterparts:
Dart | JavaScript |
---|---|
AsyncBindingObjectMethod | async function() {} |
BindingObjectMethodSync | function() { } |
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>
Embedding HTMLElements as Children of Custom Elements
Custom elements can seamlessly integrate with standard HTMLElements to construct more intricate components.
For instance, if you wish to design a complex component where the outer framework is crafted using Flutter widgets, while the inner content is structured using HTML and CSS, this combination allows for such versatility.
Another Demo
The demo below highlights the potential of blended embedding. When designing the UI, you're not confined to a single technical framework. If you possess a collection of existing Flutter widget components, you can seamlessly integrate them with WebF, making them accessible for web applications.
Standard HTML elements can be set as children within custom elements.
WebF's Flutter widget adapter will translate these child elements into a list of Flutter widgets, which are then passed
to the build()
function within the WidgetElement
class.
Consider a scenario where we've implemented a FlutterButton
class using Flutter widgets and registered it with WebF
under the tag name flutter-button
.
We might want this button to exhibit different behaviors for varied purposes, such as a red warning button or a green success button.
Consequently, this button element should accept both properties and attributes as well as children parameters.
// The success button
<flutter-button type="primary">Success</flutter-button>
// The error button
<flutter-button type="default">Fail</flutter-button>
On the Dart side, the text "Success" and "Fail" are converted into widget children and stored in
the List<Widget> children
parameter.
In this demonstration, we set these widgets as children of either the ElevatedButton
or the OutlinedButton
widget.
Thus, the text will be displayed within the Flutter buttons.
We also use the initializeProperties
method to register a type property, which allows for the customization of the
Flutter button based on its style.
For the Flutter button's onPressed
event handler, we dispatch a click event to JavaScript, enabling the web app to
process the click gesture and execute further actions.
class FlutterButton extends WidgetElement {
FlutterButton(BindingContext? context) : super(context);
handlePressed(BuildContext context) {
dispatchEvent(Event(EVENT_CLICK));
}
Map<String, dynamic> get defaultStyle => {'display': 'inline-block'};
Widget buildButton(BuildContext context, String type, Widget child) {
switch (type) {
case 'primary':
return ElevatedButton(
onPressed: () => handlePressed(context), child: child);
case 'default':
default:
return OutlinedButton(
onPressed: () => handlePressed(context), child: child);
}
}
void initializeProperties(Map<String, BindingObjectProperty> properties) {
super.initializeProperties(properties);
properties['type'] = BindingObjectProperty(
getter: () => type, setter: (value) => type = value);
}
String get type => getAttribute('type') ?? 'default';
set type(value) {
internalSetAttribute('type', value?.toString() ?? '');
}
Widget build(BuildContext context, List<Widget> children) {
return buildButton(context, type,
children.isNotEmpty ? children[0] : SizedBox.fromSize(size: Size.zero));
}
}