Agent D
 · Co-founder @ Moneyly

Rich Text HTML Editor for Flutterflow Apps

INTRODUCTION <<WARNING THIS IS A VERY LONG POST 🚨🚨>>
So one of the features we have needed in the FF community for a while now is a Rich Text editor, that allows our users to type in styled text, embed images, videos, etc., and then we get the HTML output (which we can save to our backend, or send to a PDF generator to make PDF), because of this I made two:

I will start with the Editor A, it will be QUILL HTML EDITOR

Then finish with Editor B which will be HTML EDITOR ENHANCED

OUTLINE OF THE CODE

Both Editor A and Editor B setups will follow this outline, so you implementation will always be the same:

  1. Create a custom action for declaring a map of global Key variables

  2. Add this custom action as a "final action" to your main.dart file.

  3. Create the Custom Widget

  4. Create the custom actions you will need (in this post we will create two - Getting the HTML content [this action outputs HTML text string, so we can then easily save to Firebase/Supabase/FFAppState using normal FF actions] and Setting the HTML content [action accepts a string which we can bring from anywhere Firebase/Supabase/FFAppState]).

  5. Step 4 will make use of the variable key you made in step 1 as an input to be able to uniquely identify each instance of the editor you put on a page. So if you have two editors on the same page, you will be able to specify a button to save the content of editor number 1 and another to save the content of editor number 2, or Set the content of editor 1 and that of editor 2 and so on...

NOTE : For people building mobile (iOS and Android), take note that Editor B (HTML Enhanced) needs extra setup for image embed to work on iOS See here 👉🏾 in Github.

QUILL EDITOR FLUTTER

(WARNING !!! ⚠️ Quill Editor will NOT work in Flutterflow apps that are using the Webview widget on another screen. Also, the Quill Editor project has bugs that the author has not addressed with new ones coming up randomly, therefore, you may get random errors and crashes. So, this is NOT my recommended editor (I myself use the second one). Plus, if you're using Webview elsewhere in your app, just scroll down to use the Second editor ⚠️).

First of all create an empty custom widget and name it QuillEditor. Just create the widget and save the basic boiler plate code, after saving remove the underscores in the places shown in RED in the picture below 👇🏾

Screenshot of FF editor. You set the name of the new widget to QuillEditor or htmlEditorAdvanced, click code sign (yellow arrow) and then import the boilerplate code. Next save widget. After saving remove the initial underscore in the places I underlined red


Next, open Custom Action tab and create a Custom Action, no dependencies no inputs, name the action declareGlobalKeys and paste this code inside to replace the content (of course avoid the FF imports). 👇🏾

import '/custom_code/widgets/quill_editor.dart';

// Declare GlobalKeys
final GlobalKey<QuillEditorState> editorKeyA = GlobalKey<QuillEditorState>();
final GlobalKey<QuillEditorState> editorKeyB = GlobalKey<QuillEditorState>();
final GlobalKey<QuillEditorState> editorKeyC = GlobalKey<QuillEditorState>();


// Create a Map to store these keys
final Map<String, GlobalKey<QuillEditorState>> editorKeys = {
  'editorA': editorKeyA,
  'editorB': editorKeyB,
  'editorC': editorKeyC,
};


Future declareGlobalKeys() async {}

Save and compile the code. STAGE 1 COMPLETE! 😎✨

Now go to your main.dart file under “custom files” (it’s just below custom actions) click on it. The check the far right of the screen you will see a blue plus sign “⊕” under final action (bottom right). Click it and select the  declareGlobalKeys action you just made from the list it brings. Then click save (top right). Voila! Now go back to your empty custom widget.

FF Editor showing main.dart. Click on the orange arrow then go to the right and click on green arrow, then select declareGlobalKeys from the list. It will look like purple arrow when selected. Next slick save (top right).


CREATING THE WIDGET: QUILL EDITOR WIDGET

QuillEditor 

Go back to your empty widget. Add a new parameter, call it editorKeyValue, make it type String, uncheck nullable checkbox. Now re-import the boiler plate code (from top right corner).

Add a dependency quill_html_editor: ^2.1.9 [DO NOT use a higher version than this, lower version? Sure, but not higher! Because FF’s other native dependencies will conflict with it]. So click the green circle to import the dependency.

Next paste this code in the code space, just replace everything below FF imports in there with this:

import 'package:quill_html_editor/quill_html_editor.dart';
import '/custom_code/actions/declare_global_keys.dart';

class QuillEditor extends StatefulWidget {
  factory QuillEditor({
    double? width,
    double? height,
    required String editorKeyValue,
  }) {
    return QuillEditor._internal(
      key: editorKeys[editorKeyValue],
      width: width,
      height: height,
      editorKeyValue: editorKeyValue,
    );
  }

  QuillEditor._internal({
    Key? key,
    this.width,
    this.height,
    required this.editorKeyValue,
  }) : super(key: key);

  final double? width;
  final double? height;
  final String editorKeyValue;

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

class QuillEditorState extends State<QuillEditor> {
  QuillEditorController? controller;
  bool _hasFocus = false;

  @override
  void initState() {
    controller = QuillEditorController();
    controller?.onTextChanged((text) {
      debugPrint('listening to $text');
    });
    super.initState();
  }

  @override
  void dispose() {
    controller?.dispose(); // Release any resources
    super.dispose();
  }

  final customToolBarList = [
    ToolBarStyle.bold,
    ToolBarStyle.italic,
    ToolBarStyle.align,
    ToolBarStyle.color,
    ToolBarStyle.background,
    ToolBarStyle.listBullet,
    ToolBarStyle.listOrdered,
    ToolBarStyle.clean,
    ToolBarStyle.addTable,
    ToolBarStyle.editTable,
    ToolBarStyle.video,
    ToolBarStyle.headerOne,
    ToolBarStyle.headerTwo,
    ToolBarStyle.image,
    ToolBarStyle.directionLtr,
    ToolBarStyle.directionRtl,
    ToolBarStyle.size,
    ToolBarStyle.clearHistory,
    ToolBarStyle.blockQuote,
  ];

  final _toolbarColor = Colors.grey.shade200;
  final _backgroundColor = Colors.white70;
  final _toolbarIconColor = Colors.black87;
  final _editorTextStyle = const TextStyle(
      color: Colors.black, fontWeight: FontWeight.normal, fontFamily: 'Roboto');
  final _hintTextStyle = const TextStyle(
      fontSize: 18, color: Colors.black12, fontWeight: FontWeight.normal);

  @override
  Widget build(BuildContext context) {
    return Container(
      width: widget.width,
      height: widget.height,
      child: Scaffold(
        backgroundColor: Colors.white,
        resizeToAvoidBottomInset: true,
        body: Column(
          children: [
            ToolBar(
              toolBarColor: _toolbarColor,
              padding: const EdgeInsets.all(8),
              iconSize: 25,
              iconColor: _toolbarIconColor,
              activeIconColor: Colors.greenAccent.shade400,
              controller: controller!,
              crossAxisAlignment: WrapCrossAlignment.start,
              direction: Axis.horizontal,
            ),
            Expanded(
              child: QuillHtmlEditor(
                text: "<h1>Hello</h1>Quill html editor example for FF  😊",
                hintText: 'Hint text goes here',
                controller: controller!,
                isEnabled: true,
                minHeight: widget.height ?? 200.0,
                textStyle: _editorTextStyle,
                hintTextStyle: _hintTextStyle,
                hintTextAlign: TextAlign.start,
                padding: const EdgeInsets.only(left: 10, top: 10),
                hintTextPadding: const EdgeInsets.only(left: 20),
                backgroundColor: _backgroundColor,
                onFocusChanged: (focus) {
                  debugPrint('has focus $focus');
                  setState(() {
                    _hasFocus = focus;
                  });
                },
                onTextChanged: (text) => debugPrint('widget text change $text'),
                onEditorCreated: () {
                  debugPrint('Editor has been loaded');
                  controller?.setText('Testing text on load');
                },
                onEditorResized: (height) =>
                    debugPrint('Editor resized $height'),
                onSelectionChanged: (sel) =>
                    debugPrint('index ${sel.index}, range ${sel.length}'),
              ),
            ),
          ],
        ),
      ),
    );
  }
}
///Use THIS instead of ToolBar(...), if you want a scrollable toolbar with all the icons in a single line.

ToolBar.scroll(
         toolBarColor: _toolbarColor,
         controller: controller!,
         direction: Axis.horizontal,
    ),

Then click on Save (top right). Then compile the code. You should have no errors

Now go into the FF builder and select QuillEditor under custom components.


Place two or three (if you like) on the same page and for each one specify a width and height and an editorKeyValue, for this put 'editorA', 'editorB' and 'editorC' respectively, and run the app in test mode, run mode, or live whichever you want, and you will see two/three editors distinct show up.

Your HTML editor up and running!! Yaaay you genius!, Embed picture, YouTube/Vimeo videos, links etc. NOTE: 5 of the toolbar icons may not show in test mode, they will still work but may not show, don't worry, it's due to network fetching, as you can see in this live mode screenshot, they all show just fine


[NOTE!! If you wanna test more than 3 editors on the same page, there is no limit, simply go back to step one, when you are declaring global key variables, declare A to whatever letter you need it to; and when you are designing the page just specify a unique value to each one - ‘editorA’ to ‘editorZ’ (if you want 26 different editors on a page)].

Now we have two working Editors on the same page. Lets declare actions that will uniquely get the text content of each one and set the content of each one. The GET method will return an HTML string which you can use normally like any other action to do anything you want (Firebase/Supabase/FFAppState). While the SET method will take in a String which you can supply to it from anywhere you see fit!

GET THE HTML TEXT FROM ANY SPECIFIED QUILL EDITOR WIDGET

Create a Custom Action, name it getTextQuillEditor set it up like this 👇🏾. So your action takes in a paramenter called editorKeyValue (which is the editorA/editorB/editorC you gave to the widget you wann get text from as keyValue when you added to the page).

GET TEXT will accept one parameter which will be one out of 'editorA' or 'editorB' or 'editorC' depending on whichever you assigned to the widget you wanna use this action on when you were adding it to your FF canvas.


Save the boilerplate code. Next, you add this code to the editor replacing everything inside, below the FF imports with this 👇🏾


import 'package:quill_html_editor/quill_html_editor.dart';
import '/custom_code/actions/declare_global_keys.dart';
import '/custom_code/widgets/quill_editor.dart';

Future<String?> getTextQuillEditor(String editorKeyValue) async {
  GlobalKey<QuillEditorState>? thisWidgetKey = editorKeys[editorKeyValue];

  if (thisWidgetKey?.currentState != null) {
    final String? htmlText =
        await thisWidgetKey!.currentState!.controller!.getText();
    return htmlText;
  } else {
   // FF community please handle the error how you see fit; this is just an example 
    throw Exception(
        "The GlobalKey for editorKeyValue: $editorKeyValue is not available");
  }
}

Save and Compile!


SET THE HTML TEXT FROM ANY SPECIFIED QUILL EDITOR WIDGET

Create a Custom Action, name it setTextQuillEditor set it up like this 👇🏾

SET TEXT will accept TWO parameters one is 'editorA' or 'editorB' or 'editorC' depending on whichever you assigned to the widget just like GET TEXT. The second is the string of the content you wanna set.


After setting it up like above, save boilerplate code, and then replace everything below the FF imports with this 👇🏾

import 'package:quill_html_editor/quill_html_editor.dart';
import '/custom_code/actions/declare_global_keys.dart';
import '/custom_code/widgets/quill_editor.dart';

Future setTextQuillEditor(
  String? contentToSet,
  String editorKeyValue,
) async {
  GlobalKey<QuillEditorState>? thisWidgetKey = editorKeys[editorKeyValue];

  if (thisWidgetKey?.currentState != null && contentToSet != null) {
    thisWidgetKey!.currentState!.controller!.setText(contentToSet);
  } else {
    // Handle the error appropriately; this is just an example.
    throw Exception(
        "The GlobalKey for editorKeyValue: $editorKeyValue is not available or the text to set is empty");
  }
}

AND THAT IS IT!!! You did it you amazing human!! You now have a fully functional Rich Text Editor in your FF App. Now let us go test the actions. Set it up like this, and let button GET Content A have an action with an input for editorA and button GET Content B have an action with an input for editorB, same with SET Content A and B.

FF Builder showing two Quill Editors on the same page with 2 sets of buttons: SET Content A and B GET Content A and B

Then run it in test mode/run mode/live mode


SET Text action takes in a String

GET Text action returns a String

I think most of us know how to save things to Firebase/Supabase/FFAppState using FF actions from here, so let me stop here.

HTML EDITOR ENHANCED

The process is the same 5 steps as we saw before

STEP 1 CREATE A HtmlEditorEnhanced AND CREATE GLOBAL KEYS in a custom action called declaringGlobalKeys (I am using another name as that other name is taken by me doing the first Quill Editor example)

I am calling it declaringGlobalKeys so you can take note that if you copy and paste this code, then set the name to declaringGlobalKeys too. Custom Action declaringGlobalKeys👇🏾

import '/custom_code/widgets/html_editor_enhanced.dart';

// Declare GlobalKeys
final GlobalKey<HtmlEditorEnhancedState> editorKeyA =
    GlobalKey<HtmlEditorEnhancedState>();
final GlobalKey<HtmlEditorEnhancedState> editorKeyB =
    GlobalKey<HtmlEditorEnhancedState>();
final GlobalKey<HtmlEditorEnhancedState> editorKeyC =
    GlobalKey<HtmlEditorEnhancedState>();

// Create a Map to store these keys
final Map<String, GlobalKey<HtmlEditorEnhancedState>> editorKeys = {
  'editorA': editorKeyA,
  'editorB': editorKeyB,
  'editorC': editorKeyC,
};

Future declaringGlobalKeys() async {}

Next Create the widget HtmlEditorEnhanced. Widget code 👇🏾

import 'package:html_editor_enhanced/html_editor.dart';
import '/custom_code/actions/declaring_global_keys.dart';
import 'package:file_picker/file_picker.dart';

class HtmlEditorEnhanced extends StatefulWidget {
  factory HtmlEditorEnhanced({
    double? width,
    double? height,
    required String editorKeyValue,
  }) {
    return HtmlEditorEnhanced._internal(
      key: editorKeys[editorKeyValue],
      width: width,
      height: height,
      editorKeyValue: editorKeyValue,
    );
  }

  HtmlEditorEnhanced._internal({
    Key? key,
    this.width,
    this.height,
    required this.editorKeyValue,
  }) : super(key: key);

  final double? width;
  final double? height;
  final String editorKeyValue;

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

class HtmlEditorEnhancedState extends State<HtmlEditorEnhanced> {
  late HtmlEditorController controller;

  @override
  void initState() {
    super.initState();
    controller = HtmlEditorController();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      width: widget.width,
      height: widget.height,
      child: HtmlEditor(
        controller: controller,
        // Set up additional options as needed, this is just a basic check the docs
        htmlEditorOptions: HtmlEditorOptions(
          hint: "Your text here...",
          shouldEnsureVisible: true,
          autoAdjustHeight: true,
          adjustHeightForKeyboard: true,
          spellCheck: true,
          // ... other options will come here, add a comma after each
        ),
        htmlToolbarOptions: HtmlToolbarOptions(
          toolbarPosition: ToolbarPosition.aboveEditor,
          toolbarType: ToolbarType.nativeGrid,
          defaultToolbarButtons: [
            StyleButtons(),
            FontSettingButtons(),
            FontButtons(),
            ColorButtons(),
            ListButtons(),
            ParagraphButtons(),
            InsertButtons(),
            OtherButtons(),
          ],
          // Customize the toolbar further from here
        ),
      ),
    );
  }
}

// for ToolbarType, use 'ToolbarType.nativeScrollable' or ToolbarType.nativeExpandable to change the layout of the Toolbar icons

Dependency

html_editor_enhanced: ^2.5.1

Now compile the code, (that is after declaring the variable you just saved, do not compile, it is after you have created this widget like this, you now compile.

You will get no errors.

Next, ADD THE declaringGlobalKeys ACTION to your main.dart as a final action.

That's it Widget is ready! Now just create the two actions you need 👇🏾

GET TEXT ACTION (has one param called non-nullable editorKeyValue and returns a nullable String)


import 'package:html_editor_enhanced/html_editor.dart';
import '/custom_code/actions/declaring_global_keys.dart';
import 'package:file_picker/file_picker.dart';
import '/custom_code/widgets/html_editor_enhanced.dart';

Future<String?> getTextHtmlEditorEnhanced(String editorKeyValue) async {
  GlobalKey<HtmlEditorEnhancedState>? thisWidgetKey =
      editorKeys[editorKeyValue];

  if (thisWidgetKey?.currentState != null) {
    final String? htmlText =
        await thisWidgetKey!.currentState!.controller!.getText();
    return htmlText;
  } else {
    // FF community please handle the error how you see fit; this is just an example from me!!!
    throw Exception(
        "The GlobalKey for editorKeyValue: $editorKeyValue is not available");
  }
}

Dependency is same

html_editor_enhanced: ^2.5.1


SET TEXT ACTION (has two parameters called editorKeyValue and contentToSet which is nullable)

import 'package:html_editor_enhanced/html_editor.dart';
import '/custom_code/actions/declaring_global_keys.dart';
import 'package:file_picker/file_picker.dart';
import '/custom_code/widgets/html_editor_enhanced.dart';

Future setTextHtmlEditorEnhanced(
  String? contentToSet,
  String editorKeyValue,
) async {
  GlobalKey<HtmlEditorEnhancedState>? thisWidgetKey =
      editorKeys[editorKeyValue];

  if (thisWidgetKey?.currentState != null && contentToSet != null) {
    thisWidgetKey!.currentState!.controller.setText(contentToSet);
  } else {
    // Handle the error appropriately FF community; this is just an example.
    throw Exception(
        "The GlobalKey for editorKeyValue: $editorKeyValue is not available or the text to set is empty");
  }
}

Dependency is same

html_editor_enhanced: ^2.5.1

THAT'S ALL!! TWO FULLY FUNCTIONAL NO-LIMITS Rich Text Editors for FF!

My Side Note : Quill Editor will be better if you are using the input to generate PDF and forms (as it converts all input to HTML on the fly the other doesn't [until the user uses a formatting]. HtmlEnhanced will be better for writing apps, blogging apps, journals etc., (it has a spellchecker and word counter) and also has methods for cleaning text with a draggable canvas size).

I hope many find this helpful and very easy to follow. It's ~5am over here so I need to get some sleep. Seriously!

I have only personally tested both on web (Chrome, FireFox, Edge & Opera, all in normal, incognito, and addblocker on).

Happy Building Folks! 🐱‍🏍🚀💙🎉

Serge Middendorf

Robin Müller I saw you asked for this 2 months ago. Same with Micke Alm Sailor Boy Jason Wilhelm Gerardo Sarabia and Andrew Daniels you asked for it for a journaling app.

36
123 replies