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
Property | Type | Default | Description |
---|---|---|---|
onChanged | Function(String) | required | Callback when content changes |
outputType | String | required | Output format: 'text' or 'data' (JSON) |
textFieldStyle | TextStyle? | null | Style for the text field display |
quillTitle | String? | 'Rich Text Editor' | Title for the editor dialog |
prefillJson | String? | null | Initial content as JSON string |
prefillString | String? | null | Initial 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
- Content Validation: Always validate content before saving
- User Experience: Provide clear visual feedback for required fields
- Performance: Use appropriate output type for your use case
- Accessibility: Ensure proper labeling and keyboard navigation
- Auto-save: Implement auto-save for longer content
- 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
Related Widgets
- QuillEditorAppi - For full-screen rich text editing
- JsonInputFieldAppi - For JSON editing
- TextFieldAppi - For simple text input
Migration Notes
When upgrading from basic text inputs:
- Replace
TextField
withQuillInputPopAppi
for rich text needs - Update content handling to support chosen output format
- Implement proper validation for rich content
- 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!