How to use an WYSIWYG editor in Flutter

How to use an WYSIWYG editor in Flutter

WYSIWYG (What you see is what you get) type editors provide many functionalities to format your text, Like making some words bold, italic or making some sentence as a title or creating a web link all this are possible. These are also known as Rich Text Editor. There are plenty of options available for the web using JavaScript.

Nowadays many social media apps provide this feature in their apps. In this tutorial, I will explain how you can implement a WYSIWYG editor in Flutter. To WYSIWYG editor in Flutter is fairly easy to implement & operate. I will create a note-taking android application to demonstrate that.

Prerequisite.

To follow along with this tutorial you must be familiar with the Flutter framework(which I guess you are) to build a cross-platform mobile app.

Project Setup.

First of all, create a new Flutter project using this following command on your terminal,

>flutter create <YOUR_PROJECT_NAME>

Now open this project in any of your favorite code editors. Now we will use one plugin to which provides this WYSIWYG editor in Flutter.

To add a new plugin to your project open pubspec.yaml file & under the dependencies section add zefyr like this,

Dependencies Listing

Dependencies Listing

Understanding the Basics Of Zefyr.

As the official documentation says Zefyr is a “Soft and gentle rich text editing for Flutter applications”. Zefyr uses a document-based model for representing the formatted text. It uses its own platform-independent document model called Notus. Notus provides you the building blocks of a WYSIWYG editor which you can even use outside the Zefyr plugin.

Notus documents are based on Quill.js Delta format. Which is actually a great format. This format uses a JSON like structure to represent the document which in turn can be easily stored in database & rendered on the web pages using the Quill.js.

In this tutorial, you will mainly be going to deal with the NotusDocument class provided by the Notus library which comes with the Zefyr plugin. Read more about it here.

NotusDocument provides some great methods like following,

  1. toPlainText() – Which returns the all the text as plain text format, which can be used for display purpose in our app.
  2. toJson() – Returns the JSON representation fo the document which you can store in your database.
It provides some more methods which you can use, to know more about them please check out the official documentation.

Create a Model.

Since I am going to make a note-taking application, So first let’s create a model which represent our notes. I am storing this model inside the /lib/models directory.

note_model.dart

import 'package:zefyr/zefyr.dart';

class Note {
  final String title;

  final NotusDocument document;

  Note({this.title, this.document});

}

As you can we have barely two members in the class. One is a title which will be our note’s title & another is document.

The document is the type of NotusDocument. I am directly storing the NotusDocument in our model so that we have access to the different methods provided by it.

Creating the Basic Layout.

On our main/home page I want to display all the notes I have taken, so I will be using a Stateful widget for that so that we can store our notes. To store our notes I will be using the List of type Note. By using the ListView I am showing all the notes,

main.dart

import 'package:flutter/material.dart';
import 'editor_page.dart';
import 'models/note_model.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.orange,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  List<Note> _notesList = List();

  void _add(Note note) {
    setState(() {
      // Insert new notes at the start
      _notesList.insert(0, note);
    });
  }

  void _update(int index, Note updatedNote) {
    setState(() {
      _notesList.removeAt(index);
      _notesList.insert(index, updatedNote);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(
          'MyNotes',
          style: TextStyle(color: Colors.white),
        ),
        centerTitle: true,
      ),
      body: Container(
        child: _notesList.length == 0
            ? Center(
                child: Text("Tap on + button to add new notes"),
              )
            : _getNotesList(),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          Navigator.push(
            context,
            MaterialPageRoute(
                builder: (BuildContext context) => EditorPage(add: _add)),
          );
        },
        tooltip: 'Add New Notes',
        child: Icon(
          Icons.add,
          color: Colors.white,
        ),
      ),
    );
  }

  Widget _getNotesList() {
    return ListView.builder(
      itemCount: _notesList.length,
      itemBuilder: (BuildContext context, int index) {
        return ListTile(
          leading: Icon(Icons.note),
          title: Text(
            _notesList[index].title,
            style: TextStyle(fontSize: 22, fontWeight: FontWeight.w500),
          ),
          subtitle: Text(
            _notesList[index].document.toPlainText(),
            maxLines: 1,
          ),
          onTap: () {
            Navigator.push(
              context,
              MaterialPageRoute(
                builder: (BuildContext context) => EditorPage(
                  update: _update,
                  noteIndex: index,
                  note: _notesList[index],
                ),
              ),
            );
          },
        );
      },
    );
  }
}

You can see in the code that I have also created two methods namely _add() & _update() which will add new notes & update the previously added notes respectively.

I will be passing down these two methods in our editor page where we will be creating & formating our text so that we can invoke these two methods.

Home Page

Home Page

 

Creating the Editor Page.

Our editor page will have two fields one for title & other for the description of the note we want to create.

Zefyr provides us with two options for text editing fields. One is ZefyrEditor & other is ZefyrField. ZefyrEdior takes the full screen & you can’t use other widgets along with it whereas ZefyrField provides more flexibility & can be used with other widgets. I will be using the ZefyField because I need to provide one default text field for the title on our page.

The ZefyrField requires two mandatory parameters one is controller & other is focusNode. The controller is of type ZefyrController which handles different events & gives to use different methods.

The focusNode helps in capturing & maintaining the focus on the field.

editor_page.dart

import 'package:flutter/material.dart';
import 'package:mynotesapp/models/note_model.dart';
import 'package:zefyr/zefyr.dart';

class EditorPage extends StatefulWidget {
  final Function add;
  final Function update;
  final int noteIndex;
  final Note note;

  EditorPage({this.add, this.update, this.noteIndex, this.note});

  @override
  _EditorPageState createState() => _EditorPageState();
}

class _EditorPageState extends State<EditorPage> {
  ZefyrController _editorController;

  TextEditingController _titleController;

  /// Zefyr editor like any other input field requires a focus node.
  FocusNode _focusNode;

  NotusDocument _document;

  @override
  void initState() {
    super.initState();

    _document = _loadDocument();
    _titleController = _loadTitle();
    _editorController = ZefyrController(_document);
    _focusNode = FocusNode();
  }

  NotusDocument _loadDocument() {
    // If in edit mode then load the provided document
    return widget.note != null ? widget.note.document : NotusDocument();
  }

  TextEditingController _loadTitle() {
    // If in edit mode then load the provided title
    return widget.note != null
        ? TextEditingController(text: widget.note.title)
        : TextEditingController();
  }

  @override
  Widget build(BuildContext context) {
    double screenHeight = MediaQuery.of(context).size.height;

    // To make the editor height responsive
    double editorHeight = screenHeight * 0.65;

    final editor = ZefyrField(
      height: editorHeight, // set the editor's height
      controller: _editorController,
      focusNode: _focusNode,
      autofocus: false,
      decoration: InputDecoration(
        border: OutlineInputBorder(
          borderRadius: BorderRadius.circular(5.0),
        ),
      ),
      physics: ClampingScrollPhysics(),
    );

    final titleField = TextField(
      controller: _titleController,
      decoration: InputDecoration(
        hintText: 'Enter Title Here...',
        border: OutlineInputBorder(
          borderRadius: BorderRadius.circular(5.0),
        ),
      ),
    );

    final form = Padding(
      padding: EdgeInsets.all(10),
      child: SingleChildScrollView(
        child: Column(
          children: <Widget>[
            titleField,
            SizedBox(
              height: 10,
            ),
            editor
          ],
        ),
      ),
    );

    return Scaffold(
      appBar: AppBar(
        title: Text(
          "Create New Notes",
          style: TextStyle(color: Colors.white),
        ),
        centerTitle: true,
        leading: IconButton(
          icon: Icon(Icons.chevron_left),
          color: Colors.white,
          onPressed: () => Navigator.pop(context),
        ),
        actions: <Widget>[
          IconButton(
            icon: Icon(Icons.save),
            color: Colors.white,
            onPressed: () => _saveDocument(context),
          ),
          IconButton(
            icon: Icon(Icons.clear_all),
            color: Colors.white,
            onPressed: () => _clearDocument(context),
          ),
        ],
      ),
      body: ZefyrScaffold(child: form),
    );
  }

  void _saveDocument(BuildContext context) {
    final NotusDocument doc = _editorController.document;

    final String title = _titleController.text;

    final Note note = Note(title: title, document: doc);

    // Check if we need to add new or edit old one
    if (widget.noteIndex == null && widget.note == null) {
      widget.add(note);
    } else {
      widget.update(widget.noteIndex, note);
    }

    Navigator.pop(context);
  }

  void _clearDocument(BuildContext context) async {
    bool confirmed = await _getConfirmationDialog(context);
    if (confirmed) {
      _editorController.replaceText(
          0, _editorController.document.length - 1, '');
    }
  }

  Future<bool> _getConfirmationDialog(BuildContext context) async {
    return showDialog<bool>(
      context: context,
      barrierDismissible:
          false, // dialog is dismissible with a tap on the barrier
      builder: (BuildContext context) {
        return AlertDialog(
          title: Text('Confirm?'),
          content: Row(
            children: <Widget>[
              Expanded(
                child: Text('Are you sure you want to clear the contents?'),
              )
            ],
          ),
          actions: <Widget>[
            FlatButton(
              child: Text('Cancel'),
              onPressed: () {
                Navigator.of(context).pop(false);
              },
            ),
            FlatButton(
              child: Text('Yes'),
              onPressed: () {
                Navigator.of(context).pop(true);
              },
            ),
          ],
        );
      },
    );
  }
}

One thing to remember here is Zefyr uses its own Scaffold which is mandatory to use with the Zefyr otherwise it won’t work properly.

Now you can add a title & description then tap on the save icon it will be saved in the list & you will be redirected to the main page.

Last Words.

I have created a bare minimum app to teach you how to implement a WYSIWYG editor in Flutter. I hope you have enjoyed the tutorial & get to learn something. If you have any suggestions or if you face any problem please write it down in the comment box…

Happy Coding…:)

Download Project From GitHub
Ropali Munshi
Ropali Munshi
Ropali Munshi is fullstack PHP Developer. He is passionate developer who loves to learn and expirement with new programming languages , libraries and frameworks. Nowdays he is more into the JavaScript realm.

Leave a Reply

Your email address will not be published. Required fields are marked *