DEV Community

loading...
Cover image for Build a chat application in Dart 2 (Part 2)

Build a chat application in Dart 2 (Part 2)

graphicbeacon profile image Jermaine Oppong Originally published at creativebracket.com ・7 min read

In Part 1 we constructed the UI for our Chat sign in screen and wrote a basic flow to transition to the Chatroom UI once the username is successfully validated.

In this part, we will refactor our working solution and implement a basic router to handle transitioning between views. Here's the diagram again of the chat flow:

Diagram of chat flow

We have a bit to cover, so without further ado lets begin!


1. Encapsulate logic for sign in UI

Currently we have our application logic inside web/main.dart, which will be a maintenance nightware should we continue to build on that logic! So we are going to manage the logic for each screen in a View class.

Create the directory below inside the web folder with the listed files:

views/
  chat_room.dart
  chat_signin.dart
  view.dart
Enter fullscreen mode Exit fullscreen mode

Now in view.dart, create an interface which will be used as a blueprint for our View classes:

abstract class View {
  void onEnter(); // Run this when the view is loaded in
  void onExit();  // Run this when we exit the view
  void prepare(); // Prepare the view template, register event handlers etc... before mounting on the page
  void render();  // Render the view on the screen
}
Enter fullscreen mode Exit fullscreen mode

At this point some of you may be wondering why we are using abstract class instead of the interface keyword? Answer: The Dart team made it so! The rationale is that classes are implicit interfaces so to simplify things they stuck with abstract classes. This means that we can either extend or implement an abstract class.

Let's use this interface to implement our view inside chat_signin.dart:

// Absolute imports
import 'dart:html';

// Relative imports
import './view.dart';

class ChatSigninView implements View {
  ChatSigninView() : _contents = DocumentFragment() {
    onEnter();
  }

  /// Properties
  DocumentFragment _contents;
  DivElement chatSigninBox;
  ParagraphElement validationBox;
  InputElement nameField;
  ButtonElement submitBtn;
  HttpRequest _response;

  @override
  void onEnter() {
    prepare();
    render();
  }

  @override
  void onExit() {}

  @override
  void prepare() {}

  @override
  void render() {}
}
Enter fullscreen mode Exit fullscreen mode

Before the constructor body is run, we initiate _contents with a new DocumentFragment. We will populate it with our template and define our event handlers before inserting into the page.

Let's implement the prepare() method to use _contents:

@override
void prepare() {
  _contents.innerHtml = '''
  <div id="ChatSignin">
      <h1 class="title">Chatter 🎯</h1>
      <div class="columns">
        <div class="column is-6">
          <div class="field">
            <label class="label">Please enter your name</label>
            <div class="control is-expanded has-icons-left">
              <input class="input is-medium" type="text" placeholder="Enter your name and hit ENTER" />
              <span class="icon is-medium is-left">
                <i class="fas fa-user"></i>
              </span>
            </div>
            <p class="help is-danger"></p>
          </div>
          <div class="field">
            <div class="control">
              <button class="button is-medium is-primary">
                Join chat
              </button>
            </div>
          </div>
        </div>
      </div>
    </div>
    ''';

  chatSigninBox = _contents.querySelector('#ChatSignin');
  validationBox = chatSigninBox.querySelector('p.help');
  nameField = chatSigninBox.querySelector('input[type="text"]');
  submitBtn = chatSigninBox.querySelector('button');

  _addEventListeners(); // TODO: Implement this method
}
Enter fullscreen mode Exit fullscreen mode

And now to implement _addEventListeners() and its related methods:

class ChatSigninView implements View {
  ...
  ...
  void _addEventListeners() {
    // Event listeners on form controls
    nameField.addEventListener('input', _inputHandler);
    submitBtn.addEventListener('click', _clickHandler);
  }

  void _inputHandler(evt) {
    if (nameField.value.trim().isNotEmpty) {
      nameField.classes
        ..removeWhere((className) => className == 'is-danger')
        ..add('is-success');
      validationBox.text = '';
    } else {
      nameField.classes
        ..removeWhere((className) => className == 'is-success')
        ..add('is-danger');
    }
  }

  void _clickHandler(evt) async {
    // Validate name field
    if (nameField.value.trim().isEmpty) {
      nameField.classes.add('is-danger');
      validationBox.text = 'Please enter your name';
      return;
    }

    submitBtn.disabled = true;

    // Submit name to backend via POST
    try {
      _response = await HttpRequest.postFormData(
        'http://localhost:9780/signin',
        {
          'username': nameField.value,
        },
      );

      // Handle success response and switch view
      onExit();
    } catch (e) {
      // Handle failure response
      submitBtn
        ..disabled = false
        ..text = 'Failed to join chat. Try again?';
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Most of the logic above has been moved from web/main.dart from Part 1 of the series.

The _inputHandler() private method is responsible for validating the input text field and adding the appropriate classes. The _clickHandler() private method will validate and submit the form, POSTing to the endpoint at http://localhost:9780/signin. The result is assigned to the _response instance variable. We then call the onExit() method, which will pass the response data to the next view.

Let's render the prepared document fragment to the screen:

@override
void render() {
  querySelector('#app')
    ..innerHtml = ''
    ..append(_contents);
}
Enter fullscreen mode Exit fullscreen mode

And define our exit strategy:

@override
void onExit() {
  nameField.removeEventListener('input', _inputHandler);
  submitBtn.removeEventListener('click', _clickHandler);

  // Swap view to chat room
  // TODO: Transition to Chat room screen
}
Enter fullscreen mode Exit fullscreen mode

To see this view in the browser, update web/main.dart:

import './views/chat_signin.dart';

void main() {
  ChatSigninView();
}
Enter fullscreen mode Exit fullscreen mode

Change the <body> markup of web/index.html as follows:

<section class="section">
  <div class="container" id="app">
    <!-- Views will be rendered here -->
  </div>
</section>
Enter fullscreen mode Exit fullscreen mode

Run the webdev server and visit http://localhost:8080 in the browser:

webdev serve --live-reload
Enter fullscreen mode Exit fullscreen mode

Chat signin view

2. Encapsulate logic for Chat room UI

Define a ChatRoomView class inside web/views/chat_room.dart:

// Absolute imports
import 'dart:html';

// Relative imports
import './view.dart';

class ChatRoomView implements View {
  ChatRoomView(this.params)
      : _contents = DocumentFragment() {
    onEnter();
  }

  /// Properties
  Map params;
  DocumentFragment _contents;
  DivElement chatRoomBox;
  DivElement chatRoomLog;
  InputElement messageField;
  ButtonElement sendBtn;

  @override
  void onEnter() {
    prepare();
    render();
  }

  @override
  void onExit() {
    _removeEventListeners(); // TODO: Implement this method

    // TODO: Transition to chat sign in screen
  }

  @override
  void prepare() {
  _contents.innerHtml = '''
    <div id="ChatRoom">
        <h1 class="title">Chatroom</h1>
        <div class="tile is-ancestor">
          <div class="tile is-8 is-vertical is-parent">
            <div class="tile is-child box">
              <div id="ChatRoomLog"></div>
            </div>
            <div class="tile is-child">
              <div class="field has-addons">
                <div class="control is-expanded has-icons-left">
                  <input id="ChatRoomMessageInput" class="input is-medium" type="text" placeholder="Enter message" />
                  <span class="icon is-medium is-left">
                    <i class="fas fa-keyboard"></i>
                  </span>
                </div>
                <div class="control">
                  <button id="ChatRoomSendBtn" class="button is-medium is-primary">
                    Send&nbsp;&nbsp;
                    <span class="icon is-medium">
                      <i class="fas fa-paper-plane"></i>
                    </span>
                  </button>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
      ''';

    chatRoomBox = _contents.querySelector('#ChatRoom');
    chatRoomLog = chatRoomBox.querySelector('#ChatRoomLog');
    messageField = chatRoomBox.querySelector('#ChatRoomMessageInput');
    sendBtn = chatRoomBox.querySelector('#ChatRoomSendBtn');

    _addEventListeners(); // TODO: Implement this method next
  }

  @override
  void render() {
    querySelector('#app')
      ..innerHtml = ''
      ..append(_contents);
  }
}
Enter fullscreen mode Exit fullscreen mode

And now to implement _addEventListeners() and _removeEventListeners():

void _addEventListeners() {
  sendBtn.disabled = true;

  /// Event listeners
  messageField.addEventListener('input', _messageFieldInputHandler);
  sendBtn.addEventListener('click', _sendBtnClickHandler);
}

void _removeEventListeners() {
  messageField.removeEventListener('input', _messageFieldInputHandler);
  sendBtn.removeEventListener('click', _sendBtnClickHandler);
}

void _messageFieldInputHandler(e) {
  // Disable the send button if message input is empty
  sendBtn.disabled = messageField.value.isEmpty;
}

void _sendBtnClickHandler(e) {
  // TODO: Broadcast message to other chat users
  messageField.value = '';
}
Enter fullscreen mode Exit fullscreen mode

Now that the logic for both views are encapsulated, we need to be able to transition between the two. Now you can do this easily by replacing the TODO: Transition to * screen with instantiating the class for the view you want to launch, so ChatRoomView() or ChatSigninView().

I'm not too fond of that approach since it means importing one class into the other and vice versa. I would rather delegate this to a separate class to handle that concern.

That brings us to Step #3 below...

3. Implement a Router class for handling view transition

In web/router.dart define a Router class:

import './views/view.dart';

// Type definition to label a Function that returns a `View` type
typedef ViewInstantiateFn = View Function(Map data);

class Router {
  Router() : _routes = [];

  List<Map<String, ViewInstantiateFn>> _routes;

  register(String path, ViewInstantiateFn viewInstance) {
    // The key `path` is a computed property
    // It could also be written as {'$path': viewInstance}
    _routes.add({path: viewInstance});
  }

  go(String path, {Map params = null}) {
    // Find the matching `Map` object in _routes
    // and invoke it's `View` object instance
    _routes.firstWhere(
      (Map<String, ViewInstantiateFn> route) => route.containsKey(path),
      orElse: () => null,
    )[path](params ?? {});
  }
}

Router router = Router();
Enter fullscreen mode Exit fullscreen mode

The Router class contains a list of routes under the _routes instance variable. Each route is a Map containing a key name which is the path and the value of that key is a function that returns a View object. We've created a type definition called ViewInstantiateFn to represent the structure of that function.

To add a route we will call the register() method, passing it a path and a function to instantiate our View. To transition to the view we will call the go() method, passing it a path and a map of parameters to be consumed by the View.

The last line of the file exports a Router instance.

Let's use this class in web/main.dart:

import './router.dart';
import './views/chat_room.dart';
import './views/chat_signin.dart';

void main() {
  router
    ..register('/', (_) => ChatSigninView())
    ..register('/chat-room', (params) => ChatRoomView(params))
    ..go('/');
}
Enter fullscreen mode Exit fullscreen mode

Go to web/views/chat_signin.dart and replace // TODO: Transition to Chat room screen with a call to router.go():

import '../router.dart'; // <-- Remember to import this
..
..

  @override
  void onExit() {
    nameField.removeEventListener('input', _inputHandler);
    submitBtn.removeEventListener('click', _clickHandler);

    // Swap view to chat room
    router.go('/chat-room', params: {'username': _response.responseText}); // <-- Added this line
  }
Enter fullscreen mode Exit fullscreen mode

Go to web/views/chat_room.dart and replace // TODO: Transition to chat sign in screen in onExit() method:

import '../router.dart'; // <-- Remember to import this
..
..

  @override
  void onExit() {
    _removeEventListeners();

    router.go('/'); // <-- Added this line
  }
Enter fullscreen mode Exit fullscreen mode

Now run the backend server in a separate terminal:

dart bin/server.dart
Enter fullscreen mode Exit fullscreen mode

Here's what we should now have:

Working solution

Add this css rule in web/styles.css to chat room message log bigger:

#ChatRoomLog {
  height: 300px;
  overflow-y: scroll;
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

And this concludes Part 2 of the series. In Part 3, we will implement the chat conversation logic and complete this series.

As always, I hope this was insightful and you learnt something new today. And the working solution is here.

Like, share and follow me 😍 for more content on Dart.

Further reading

  1. dart:html library
  2. How to use JavaScript libraries in your Dart applications
  3. Full-Stack Web Development with Dart

Discussion (5)

pic
Editor guide
Collapse
vinceramces profile image
Vince Ramces Oliveros
  @override
  void prepare() {
  _contents.innerHtml = '''
    <div id="ChatRoom">
        <h1 class="title">Chatroom</h1>
        <div class="tile is-ancestor">
          <div class="tile is-8 is-vertical is-parent">
            <div class="tile is-child box">
              <div id="ChatRoomLog"></div>
            </div>
            <div class="tile is-child">
              <div class="field has-addons">
                <div class="control is-expanded has-icons-left">
                  <input id="ChatRoomMessageInput" class="input is-medium" type="text" placeholder="Enter message" />
                  <span class="icon is-medium is-left">
                    <i class="fas fa-keyboard"></i>
                  </span>
                </div>
                <div class="control">
                  <button id="ChatRoomSendBtn" class="button is-medium is-primary">
                    Send&nbsp;&nbsp;
                    <span class="icon is-medium">
                      <i class="fas fa-paper-plane"></i>
                    </span>
                  </button>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
      ''';

This was harder for me to read the tags.
although this was the same as document.innerHTML in Javascript, it was still painful to read. are there any alternative solution of not storing the multi-line string html tags?

Collapse
graphicbeacon profile image
Jermaine Oppong Author • Edited

Hey Vince,

Thanks for the feedback.

Off the top of my head you could try putting the string in a separate html file and lazy-loading it in.

Alternatively, you could move this back to web/index.html like it was in Part 1 and toggle its visibility 👎🏾. Perhaps even better you could try moving it to web/index.html in a <template> tag and have logic to extract and render the contents whenever you need. EDIT: There is a TemplateElement class to help with that.

Hope that helps.

Collapse
ratulotron profile image
Ratul Minhaz

Thanks for the second part Jermaine! Eagerly waiting for the last part of the series!

Collapse
graphicbeacon profile image
Jermaine Oppong Author

Hi Ratul, Part 3 is now here.

Collapse
ratulotron profile image
Ratul Minhaz

Thanks a ton! I really enjoyed this series, I hope you do some more posts on Dart on the backend.