Skip to main content

QuillInputPopAppi

A modal popup widget that combines rich text editing capabilities with a clean text field interface, providing users with an expandable rich text editor experience through an alert dialog.

Overview

QuillInputPopAppi offers a space-efficient solution for rich text input by presenting a simple text field that opens a full-featured Quill editor in a modal dialog. This approach is perfect for forms, comments, and other scenarios where rich text editing is needed but screen space is limited.

Features

  • 📝 Modal Rich Text Editing - Full Quill editor in a popup dialog
  • 🎯 Compact Interface - Simple text field that expands to rich editor
  • 💾 Flexible Output - Support for both JSON and plain text formats
  • 📋 Prefill Support - Initialize with existing content
  • 🎨 Custom Styling - Configurable text field appearance
  • Built-in Validation - Automatic content validation
  • 🔄 Real-time Updates - Live content change callbacks
  • 📱 Responsive Design - Adaptive dialog sizing
  • 🎪 Custom Titles - Configurable dialog titles
  • 🖱️ Easy Interaction - Tap to open, save to close

Basic Usage

QuillInputPopAppi(
onChanged: (content) {
print('Content updated: $content');
},
outputType: 'text', // 'text' or 'data' (JSON)
textFieldStyle: TextStyle(fontSize: 16),
quillTitle: 'Rich Text Editor',
)

Properties

PropertyTypeDefaultDescription
onChangedFunction(String)requiredCallback when content changes
outputTypeStringrequiredOutput format: 'text' or 'data' (JSON)
textFieldStyleTextStyle?nullStyle for the text field display
quillTitleString?'Rich Text Editor'Title for the editor dialog
prefillJsonString?nullInitial content as JSON string
prefillStringString?nullInitial content as plain text

Examples

Basic Rich Text Input

class BasicRichInputExample extends StatefulWidget {

_BasicRichInputExampleState createState() => _BasicRichInputExampleState();
}

class _BasicRichInputExampleState extends State<BasicRichInputExample> {
String content = '';


Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Description',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
SizedBox(height: 8),
QuillInputPopAppi(
onChanged: (value) {
setState(() {
content = value;
});
},
outputType: 'text',
textFieldStyle: TextStyle(
fontSize: 14,
color: Colors.grey[700],
),
quillTitle: 'Edit Description',
prefillString: 'Tap to add description...',
),
SizedBox(height: 16),
Text('Current content: $content'),
],
),
);
}
}

Form Integration

class FormWithRichTextExample extends StatefulWidget {

_FormWithRichTextExampleState createState() => _FormWithRichTextExampleState();
}

class _FormWithRichTextExampleState extends State<FormWithRichTextExample> {
final _formKey = GlobalKey<FormState>();
String title = '';
String description = '';
String notes = '';


Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFormField(
decoration: InputDecoration(
labelText: 'Title',
border: OutlineInputBorder(),
),
validator: (value) {
if (value?.isEmpty ?? true) {
return 'Title is required';
}
return null;
},
onSaved: (value) => title = value ?? '',
),
SizedBox(height: 16),

// Rich text description
Text(
'Description',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 8),
Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey[300]!),
borderRadius: BorderRadius.circular(8),
),
child: QuillInputPopAppi(
onChanged: (value) {
description = value;
},
outputType: 'data', // JSON for rich content
textFieldStyle: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
quillTitle: 'Edit Description',
prefillString: 'Add detailed description...',
),
),
SizedBox(height: 16),

// Rich text notes
Text(
'Additional Notes',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 8),
QuillInputPopAppi(
onChanged: (value) {
notes = value;
},
outputType: 'text',
textFieldStyle: TextStyle(
fontSize: 14,
fontStyle: FontStyle.italic,
color: Colors.grey[500],
),
quillTitle: 'Additional Notes',
prefillString: 'Optional notes...',
),
SizedBox(height: 24),

ElevatedButton(
onPressed: () {
if (_formKey.currentState?.validate() ?? false) {
_formKey.currentState?.save();
submitForm();
}
},
child: Text('Submit'),
),
],
),
),
);
}

void submitForm() {
print('Title: $title');
print('Description: $description');
print('Notes: $notes');
// Submit to API
}
}

Comment System

class CommentInputExample extends StatefulWidget {
final Function(String) onSubmitComment;

const CommentInputExample({required this.onSubmitComment});


_CommentInputExampleState createState() => _CommentInputExampleState();
}

class _CommentInputExampleState extends State<CommentInputExample> {
String commentContent = '';


Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey[200]!),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
CircleAvatar(
radius: 16,
backgroundColor: Colors.blue,
child: Text('U', style: TextStyle(color: Colors.white)),
),
SizedBox(width: 12),
Text(
'Add a comment',
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 16,
),
),
],
),
SizedBox(height: 12),

QuillInputPopAppi(
onChanged: (value) {
setState(() {
commentContent = value;
});
},
outputType: 'text',
textFieldStyle: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
quillTitle: 'Write Comment',
prefillString: 'Share your thoughts...',
),

if (commentContent.trim().isNotEmpty) ...[
SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () {
setState(() {
commentContent = '';
});
},
child: Text('Cancel'),
),
SizedBox(width: 8),
ElevatedButton(
onPressed: () {
widget.onSubmitComment(commentContent);
setState(() {
commentContent = '';
});
},
child: Text('Post Comment'),
),
],
),
],
],
),
);
}
}

Blog Post Editor

class BlogPostInputExample extends StatefulWidget {

_BlogPostInputExampleState createState() => _BlogPostInputExampleState();
}

class _BlogPostInputExampleState extends State<BlogPostInputExample> {
String title = '';
String excerpt = '';
String content = '';
List<String> tags = [];


Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Create Blog Post'),
actions: [
TextButton(
onPressed: saveDraft,
child: Text('Save Draft'),
),
ElevatedButton(
onPressed: publishPost,
child: Text('Publish'),
),
],
),
body: SingleChildScrollView(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
decoration: InputDecoration(
labelText: 'Post Title',
border: OutlineInputBorder(),
),
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
onChanged: (value) => title = value,
),
SizedBox(height: 16),

Text(
'Excerpt',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
),
SizedBox(height: 8),
QuillInputPopAppi(
onChanged: (value) {
excerpt = value;
},
outputType: 'text',
textFieldStyle: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
quillTitle: 'Edit Excerpt',
prefillString: 'Brief description of your post...',
),
SizedBox(height: 16),

Text(
'Content',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
),
SizedBox(height: 8),
Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey[300]!),
borderRadius: BorderRadius.circular(8),
),
child: QuillInputPopAppi(
onChanged: (value) {
content = value;
},
outputType: 'data', // Rich content as JSON
textFieldStyle: TextStyle(
fontSize: 14,
color: Colors.grey[700],
),
quillTitle: 'Write Your Post',
prefillString: 'Start writing your blog post...',
),
),
SizedBox(height: 24),

// Preview section
if (content.isNotEmpty) ...[
Text(
'Preview',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
),
SizedBox(height: 8),
Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey[200]!),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (title.isNotEmpty)
Text(
title,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
if (excerpt.isNotEmpty) ...[
SizedBox(height: 8),
Text(
excerpt,
style: TextStyle(
fontSize: 14,
fontStyle: FontStyle.italic,
color: Colors.grey[600],
),
),
],
SizedBox(height: 12),
Text('Content: ${content.length} characters'),
],
),
),
],
],
),
),
);
}

void saveDraft() {
// Save as draft
print('Saving draft...');
}

void publishPost() {
if (title.isNotEmpty && content.isNotEmpty) {
// Publish post
print('Publishing post...');
}
}
}

Custom Styled Input

class CustomStyledInputExample extends StatefulWidget {

_CustomStyledInputExampleState createState() => _CustomStyledInputExampleState();
}

class _CustomStyledInputExampleState extends State<CustomStyledInputExample> {
String feedback = '';


Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.all(16),
padding: EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue[50]!, Colors.white],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.2),
spreadRadius: 2,
blurRadius: 8,
offset: Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.feedback, color: Colors.blue),
SizedBox(width: 8),
Text(
'Share Your Feedback',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.blue[800],
),
),
],
),
SizedBox(height: 16),

Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.blue[200]!),
),
child: QuillInputPopAppi(
onChanged: (value) {
setState(() {
feedback = value;
});
},
outputType: 'text',
textFieldStyle: TextStyle(
fontSize: 16,
color: Colors.grey[700],
height: 1.5,
),
quillTitle: '✨ Share Your Experience',
prefillString: 'Tell us about your experience...',
),
),

if (feedback.trim().isNotEmpty) ...[
SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Container(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(20),
),
child: Text(
'Submit Feedback',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
),
],
),
],
],
),
);
}
}

Advanced Features

Content Validation

class ValidatedRichInputExample extends StatefulWidget {

_ValidatedRichInputExampleState createState() => _ValidatedRichInputExampleState();
}

class _ValidatedRichInputExampleState extends State<ValidatedRichInputExample> {
String content = '';
String? errorMessage;


Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Product Description *',
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 16,
),
),
SizedBox(height: 8),

Container(
decoration: BoxDecoration(
border: Border.all(
color: errorMessage != null ? Colors.red : Colors.grey[300]!,
),
borderRadius: BorderRadius.circular(8),
),
child: QuillInputPopAppi(
onChanged: (value) {
setState(() {
content = value;
errorMessage = validateContent(value);
});
},
outputType: 'text',
textFieldStyle: TextStyle(
fontSize: 14,
color: errorMessage != null ? Colors.red[700] : Colors.grey[700],
),
quillTitle: 'Product Description',
prefillString: 'Describe your product...',
),
),

if (errorMessage != null) ...[
SizedBox(height: 4),
Text(
errorMessage!,
style: TextStyle(
color: Colors.red,
fontSize: 12,
),
),
],

SizedBox(height: 4),
Text(
'${content.length}/500 characters',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
],
);
}

String? validateContent(String content) {
if (content.trim().isEmpty) {
return 'Description is required';
}
if (content.length < 20) {
return 'Description must be at least 20 characters';
}
if (content.length > 500) {
return 'Description must be less than 500 characters';
}
return null;
}
}

Dynamic Content Loading

class DynamicContentExample extends StatefulWidget {
final String? existingContentId;

const DynamicContentExample({this.existingContentId});


_DynamicContentExampleState createState() => _DynamicContentExampleState();
}

class _DynamicContentExampleState extends State<DynamicContentExample> {
String content = '';
bool isLoading = true;


void initState() {
super.initState();
loadExistingContent();
}

Future<void> loadExistingContent() async {
if (widget.existingContentId != null) {
try {
// Simulate API call
await Future.delayed(Duration(seconds: 1));
final loadedContent = await fetchContent(widget.existingContentId!);
setState(() {
content = loadedContent;
isLoading = false;
});
} catch (e) {
setState(() {
isLoading = false;
});
}
} else {
setState(() {
isLoading = false;
});
}
}


Widget build(BuildContext context) {
if (isLoading) {
return Container(
height: 60,
child: Center(child: CircularProgressIndicator()),
);
}

return QuillInputPopAppi(
onChanged: (value) {
setState(() {
content = value;
});
autoSave(value);
},
outputType: 'data',
textFieldStyle: TextStyle(fontSize: 14),
quillTitle: 'Edit Content',
prefillJson: content,
);
}

Future<String> fetchContent(String id) async {
// Simulate API call
return '{"ops":[{"insert":"Loaded content from API\\n"}]}';
}

void autoSave(String content) {
// Implement auto-save functionality
print('Auto-saving: $content');
}
}

Output Formats

Text Output

When outputType is set to 'text', the widget returns plain text:

Hello World
This is plain text content.

JSON Output

When outputType is set to 'data', the widget returns Quill Delta JSON:

{
"ops": [
{"insert": "Hello "},
{"attributes": {"bold": true}, "insert": "World"},
{"insert": "\nThis is "},
{"attributes": {"italic": true}, "insert": "rich"},
{"insert": " text content.\n"}
]
}

Integration Patterns

State Management Integration

// Using Provider
class ContentProvider extends ChangeNotifier {
String _content = '';

String get content => _content;

void updateContent(String newContent) {
_content = newContent;
notifyListeners();
}
}

// In widget
Consumer<ContentProvider>(
builder: (context, provider, child) {
return QuillInputPopAppi(
onChanged: provider.updateContent,
outputType: 'data',
textFieldStyle: TextStyle(fontSize: 14),
prefillJson: provider.content,
);
},
)

Form Validation

class FormFieldWrapper extends FormField<String> {
FormFieldWrapper({
Key? key,
String? initialValue,
FormFieldValidator<String>? validator,
required ValueChanged<String> onChanged,
}) : super(
key: key,
initialValue: initialValue,
validator: validator,
builder: (FormFieldState<String> state) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
QuillInputPopAppi(
onChanged: (value) {
state.didChange(value);
onChanged(value);
},
outputType: 'text',
textFieldStyle: TextStyle(fontSize: 14),
prefillString: state.value ?? '',
),
if (state.hasError)
Padding(
padding: EdgeInsets.only(top: 4),
child: Text(
state.errorText!,
style: TextStyle(color: Colors.red, fontSize: 12),
),
),
],
);
},
);
}

Best Practices

  1. Content Validation: Always validate content before saving
  2. User Experience: Provide clear visual feedback for required fields
  3. Performance: Use appropriate output type for your use case
  4. Accessibility: Ensure proper labeling and keyboard navigation
  5. Auto-save: Implement auto-save for longer content
  6. Error Handling: Handle validation errors gracefully

Common Use Cases

  • Form Fields: Rich text input in forms and surveys
  • Comment Systems: Enhanced commenting with formatting
  • Content Creation: Blog posts, articles, and documentation
  • Feedback Forms: User feedback and reviews
  • Note Taking: Quick notes with formatting options
  • Message Composition: Rich messaging interfaces

Migration Notes

When upgrading from basic text inputs:

  1. Replace TextField with QuillInputPopAppi for rich text needs
  2. Update content handling to support chosen output format
  3. Implement proper validation for rich content
  4. Consider user experience for modal interactions

Technical Notes

  • Uses AlertDialog for modal presentation
  • Integrates QuillEditorAppi for rich text editing
  • Supports both JSON and plain text output
  • Includes built-in validation based on output type
  • Automatically handles content prefilling

Ready for full-screen rich text editing? Check out QuillEditorAppi for comprehensive editing experiences!