Skip to content

Instantly share code, notes, and snippets.

@PlugFox
Last active October 1, 2019 17:48
Show Gist options
  • Select an option

  • Save PlugFox/e3710bbb18d5b201a656811e5ef46ad8 to your computer and use it in GitHub Desktop.

Select an option

Save PlugFox/e3710bbb18d5b201a656811e5ef46ad8 to your computer and use it in GitHub Desktop.
<div id="app">
The MIT License
Copyright (c) 2019 @PlugFox
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
/*
* Playground for BLoC state managment by @PlugFox
* https://gist.github.com/PlugFox/e3710bbb18d5b201a656811e5ef46ad8
* https://dartpad.dartlang.org/e3710bbb18d5b201a656811e5ef46ad8
*
* BLoC design pattern:
* pub.dev/packages/bloc
* felangel.github.io/bloc
* github.com/felangel/bloc/tree/master/examples
*
* +----------------+
* | Presentation |
* | Layer |
* | (UI) |
* +--+----------+--+
* | ^
* Event | | States
* .dispatch(<Event>) | | .state.listen(...)
* v |
* +---+----------+---+
* | transformer |
* | BLoC |
* | mapEventToState()|
* +---+----------+---+
* | ^
* Async requests | | Async response
* Repository.fetch() | | await -> yield <State>
* v |
* +--+----------+--+
* | Business Logic |
* | Layer |
* | (Backend) |
* +----------------+
*
*/
import "dart:core";
import "dart:async";
import "dart:html";
import "dart:math";
/*
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* Simplified BLoC package
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*/
/// Takes a [Stream] of [Event]s as input
/// and transforms them into a [Stream] of [State]s as output.
abstract class Bloc<Event, State> {
final StreamController<Event> _eventSubject = StreamController<Event>();
final StreamController<State> _stateSubject = new StreamController<State>.broadcast();
/// Returns the [State] before any [Event]s have been `dispatched`.
State get initialState;
/// Returns [State] stream of the [Bloc]
Stream<State> get state => this._stateSubject.stream;
/// Returns the current [State] of the [Bloc].
State get currentState => _currentState;
State _currentState;
Bloc() {
this._currentState = initialState;
_bindStateSubject();
}
/// Takes an [Event] and triggers `mapEventToState`.
/// `Dispatch` may be called from the presentation layer or from within the [Bloc].
/// `Dispatch` notifies the [Bloc] of a new [Event].
/// If `dispose` has already been called, any subsequent calls to `dispatch` will
/// be delegated to the `onError` method which can be overriden at the [Bloc]
/// as well as the [BlocDelegate] level.
void dispatch(Event event) {
try {
onEvent(event);
this._eventSubject.sink.add(event);
} catch (error) {
_handleError(error);
}
}
/// Must be implemented when a class extends [Bloc].
/// Takes the incoming `event` as the argument.
/// `mapEventToState` is called whenever an [Event] is `dispatched` by the presentation layer.
/// `mapEventToState` must convert that [Event] into a new [State]
/// and return the new [State] in the form of a [Stream] which is consumed by the presentation layer.
Stream<State> mapEventToState(Event event);
/// Called whenever an [Exception] is thrown within `mapEventToState`.
/// By default all exceptions will be ignored and [Bloc] functionality will be unaffected.
/// The stacktrace argument may be `null` if the state stream received an error without a [StackTrace].
/// A great spot to handle exceptions at the individual [Bloc] level.
void onError(Object error, StackTrace stacktrace) => null;
/// Called whenever an [Event] is dispatched to the [Bloc].
/// A great spot to add logging/analytics at the individual [Bloc] level.
void onEvent(Event event) => null;
/// Closes the [Event] and [State] [Stream]s.
/// This method should be called when a [Bloc] is no longer needed.
/// Once `dispose` is called, events that are `dispatched` will not be
/// processed and will result in an error being passed to `onError`.
/// In addition, if `dispose` is called while [Event]s are still being processed,
void dispose() {
this._eventSubject.close();
this._stateSubject.close();
}
Stream<State> transform(
Stream<Event> events,
Stream<State> next(Event event),
) {
return events.asyncExpand(next);
}
void _bindStateSubject() {
Event currentEvent;
transform(_eventSubject.stream, (Event event) {
currentEvent = event;
return mapEventToState(currentEvent).handleError(_handleError);
}).forEach((State nextState) {
this._currentState = nextState;
this._stateSubject.add(nextState);
},
);
}
void _handleError(Object error, [StackTrace stacktrace]) {
onError(error, stacktrace);
}
}
/*
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* MyBloc singleton example
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*/
/// EVENTS
abstract class MyBlocEvent {
String get status;
@override
String toString() => this.status;
}
class SplitStringEvent extends MyBlocEvent {
String get status => 'Split string to char list';
final String payload;
SplitStringEvent(this.payload);
}
/// STATES
abstract class MyBlocState {
String get status;
@override
String toString() => this.status;
}
class IdlingState extends MyBlocState {
final String status;
IdlingState([this.status = 'Idle']);
}
class ProcessingState extends MyBlocState {
final String status;
ProcessingState([this.status = 'Processing in progress']);
}
class SplitStringState extends MyBlocState {
String get status => 'Gotcha char: $char';
int index;
String char;
SplitStringState({int index, String char})
: assert(char is String && index is int && char.length == 1 && index >= 0)
, this.index = index
, this.char = char;
}
class ErrorState extends MyBlocState {
String get status => this.error;
final String error;
ErrorState([this.error = 'Error']);
}
/// MyBLoC BLoC
class MyBLoC extends Bloc<MyBlocEvent, MyBlocState> {
@override
MyBlocState get initialState => IdlingState();
// Ability to listen to a reduced stream with one kind of states
Stream<bool> get isIdlingState => this.state
.where((MyBlocState state) => state is IdlingState || state is ProcessingState)
.map<bool>((MyBlocState state) => state is IdlingState);
Stream<SplitStringState> get splitStringStream => this.state
.where((MyBlocState state) => state is SplitStringState)
.cast<SplitStringState>();
// Event => States router
Stream<MyBlocState> mapEventToState(MyBlocEvent event) async* {
yield ProcessingState();
if (event is SplitStringEvent) {
yield* _splitStringEvent(event.payload);
}
yield IdlingState();
}
Stream<MyBlocState> _splitStringEvent(String payload) async* {
try {
Future<dynamic> sleep() => Future.delayed(Duration(seconds: 1));
for (int _i=0;_i<payload?.length ?? 0;_i++) {
await sleep();
yield SplitStringState(index: _i, char: payload[_i]);
}
} catch(e) {
yield ErrorState();
}
}
void dispose() {
super.dispose();
}
// SINGLETON +
// A single instance and the ability to call from anywhere
static final MyBLoC _internalBloc = MyBLoC._internal();
factory MyBLoC() => _internalBloc;
MyBLoC._internal() {}
// SINGLETON -
}
/*
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* Presentation layer
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*/
void runApp() =>
AppInstance()
..init()
..build();
class AppInstance {
final MyBLoC _myBloc;
final Widget _sourceField;
final Widget _outputList;
AppInstance()
: this._myBloc = MyBLoC()
, this._sourceField = TextField(key: 'source-field')
, this._outputList = ListView(key: 'output-list');
void onDispatch(_) => this._myBloc.dispatch(SplitStringEvent(this._sourceField.text ?? ''));
// Init BLoC listners
void init() =>
this._myBloc
..isIdlingState.forEach(_idleSwitcher)
..splitStringStream.forEach(_addChar);
// Build UI
void build() =>
App(
home: Scaffold(
children: <Widget>[
Header(title: 'BLoC sample by @PlugFox'),
Divider(),
Form(
key: 'source-form',
children: <Widget>[
Label('String to char list: '),
this._sourceField,
RaisedButton(key: 'dispatch-button', text: 'Dispatch', onClick: onDispatch),
],
),
Divider(),
this._outputList,
],
),
);
void _idleSwitcher(bool isIdle) {
final Map<String,String> _att = document.querySelector('form#source-form>fieldset')?.attributes;
if (_att is! Map<String,String>) {
return;
} else if (isIdle) {
_att.remove('disabled');
} else {
this._outputList.clear();
_att['disabled'] = 'true';
}
}
void _addChar(SplitStringState state) {
this._outputList.add(
Row(
children: <Widget>[
Text('Char #${state.index.toString()}: \'${state.char}\''),
],
),
);
}
}
class Widget {
final Element _element;
String get key => this._element.id;
String get text => this._element is InputElement ? (this._element as InputElement).value : this._element.innerText;
Widget(String tag, {String key, String text = ''})
: this._element = Element.tag(tag??'div')..id=key??_genKey()..innerText=text;
Widget.input(String type, {String key, String text = '', Function onClick})
: this._element = InputElement(type: type??'text')..id=key??_genKey()..value=text {
if (onClick != null) this._element.onClick.forEach(onClick);
}
Widget.fromId(String key)
: this._element = document.getElementById(key);
Element build() => this._element;
void add(Widget child) => this._element.children.add(child.build());
void addAll(List<Widget> children) => this._element.children.addAll(children.map((v) => v.build()));
void clear() => this._element.children.clear();
static String _genKey() => 'rnd_${Random().nextInt(16777215).toRadixString(16)}';
@override
String toString() => '<Widget #$key>';
@override
int get hashCode => this.key.hashCode;
@override
operator ==(Object obj) => obj is Widget && obj.key == this.key;
}
class App extends Widget {
App({String id = 'app', Widget home}) : super.fromId(id) {
this.add(home);
}
}
class Scaffold extends Widget {
Scaffold({List<Widget> children}) : super('div') {
this.addAll(children);
}
}
class Form extends Widget {
Form({String key, List<Widget> children}) : super('form', key: key) {
add(Widget('fieldset')..addAll(children));
}
}
class TextField extends Widget {
TextField({String key, String text, Function onClick}) : super.input('text', key: key, text: text);
}
class RaisedButton extends Widget {
RaisedButton({String key, String text, Function onClick}) : super.input('button', key: key, text: text, onClick: onClick);
}
class Label extends Widget {
Label(String text, {String key}) : super('span', key: key, text: text);
}
class Text extends Widget {
Text(String text, {String key}) : super('p', key: key, text: text);
}
class Header extends Widget {
Header({String key, String title = 'Title'}) : super('h3', key: key, text: title);
}
class ListView extends Widget {
ListView({String key, List<Row> rows}) : super('ul', key: key) {
rows?.forEach((Row row) => this.addRow(row));
}
addRow(Row row) => this.add(row);
}
class Row extends Widget {
Row({String key, List<Widget> children}) : super('li', key: key) {
this.addAll(children);
}
}
class Divider extends Widget {
Divider() : super('div') {
this.addAll(<Widget>[
Widget('br'),
Widget('hr'),
Widget('br'),
]);
}
}
/*
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* Main point of entry
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*/
void main() => runApp();
* {
margin: 0;
padding: 0;
font-size: 16px;
font-weight: bold;
letter-spacing: -.5px;
overflow: hidden;
}
#app {
max-width: 600px;
overflow: hidden;
text-align: center;
padding: 12px 0;
overflow: hidden;
}
fieldset {
padding: 12px 6px;
}
ul {
max-height: 200px;
overflow-y: auto;
overflow-x: hidden;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment