📝 Fractal Platform Form Builder Guide

Master the art of opening and managing forms with elegant chains

0. Introduction to Form Builder

The FormBuilderExtensions class provides a powerful, fluent API for opening and managing forms in Fractal Platform. It uses a pattern called "method chaining" that makes your code readable and expressive.

What You'll Learn

Basic Concept: The Form Workflow

Start with Data Source
Declare Intention (Want)
Configure Dimensions
Open Form
Handle Result
💡 Key Principle: Forms in Fractal Platform are not just UI elements - they're intelligent data management workflows. You describe WHAT you want to do, and the system handles HOW to do it.

1. Basic Form Opening

Opening a Form from a Client

The simplest way to open a form is directly from a FractalPlatformClient:

// Open form for the first document
Client.OpenForm(result =>
{
    if (result.Result)
    {
        // User clicked OK/Save
        Console.WriteLine("Form saved successfully!");
    }
    else
    {
        // User clicked Cancel
        Console.WriteLine("Form was cancelled");
    }
});

Opening a Specific Document

// Open form for document with ID 5
Client.OpenForm(docID: 5, handleResult: result =>
{
    Console.WriteLine($"Document ID: {result.DocID}");
});

Opening with JSON Data

// Open form with specific data shape
Client.OpenForm("{'Name':$,'Age':$}", result =>
{
    if (result.Result)
    {
        // Access the collection with changes
        var collection = result.Collection;
    }
});

Opening from a Query

// Open form for query results
DocsWhere("Users", "{'Name':'Bob'}")
    .OpenForm(result =>
    {
        Console.WriteLine("Form opened for Bob");
    });

Opening from a Collection

// Get collection first, then open form
var collection = Client.GetCollection();
collection.OpenForm(result =>
{
    Console.WriteLine("Collection form opened");
});
💡 Understanding FormResult: The result parameter in your callback contains:
  • Result - true if user saved, false if cancelled
  • Collection - the data collection with any changes
  • DocID - the document ID that was edited
  • NeedReloadData - true if backend data changed
  • IsSubForm - true if this was a nested form

2. Understanding Want Patterns

The "Want" pattern is the core of FormBuilder. It lets you declare your intentions about what should happen when the form is saved.

Available Want Types

Want Type Purpose When to Use
WantNotModifyExistingDocuments View-only or no save Showing data without changing it
WantModifyExistingDocuments Update existing data Editing existing records
WantCreateNewDocumentFor Create new document Adding new records
WantCreateNewDocumentForArray Add to array field Adding items to lists
WantMergeDocumentFor Merge data into existing Combining documents
WantUnmergeDocumentFor Remove merged data Separating documents

Pattern 1: Not Modifying Documents

// Open form in read-only mode
Client.WantNotModifyExistingDocument()
    .OpenForm(result =>
    {
        // Changes won't be saved to database
    });

Pattern 2: Modifying Existing Documents

// Open form and save changes to existing document
Client.WantModifyExistingDocument()
    .OpenForm(result =>
    {
        if (result.Result)
        {
            // Changes are automatically saved!
            Console.WriteLine("Document updated in database");
        }
    });

Pattern 3: Creating New Documents

// Create new user
Client.WantCreateNewDocumentFor("Users")
    .OpenForm(result =>
    {
        if (result.Result)
        {
            // New document created in Users collection
            Console.WriteLine($"New user created with ID: {result.TargetDocID}");
        }
    });

Using Want with Queries

// Find specific documents and modify them
DocsWhere("Users", "{'Name':'Bob'}")
    .WantModifyExistingDocuments()
    .OpenForm(result =>
    {
        // Bob's document is updated
    });
⚠️ Important: The Want pattern determines how the system handles form data when saved. Choose the right pattern based on your use case!
💡 Pro Tip: You can chain Want methods with other query operations like AndWhere, OrWhere, OrderBy, etc. The Want is just declaring your intention - filtering still works normally!

3. Working with Dimensions

Dimensions are layers of data that control different aspects of your form: UI layout, theme, and the actual document data.

Available Dimension Types

Dimension Purpose Example Use
UI Form layout and structure Hide fields, change labels, add sections
Theme Visual appearance Colors, fonts, light/dark mode
Document The actual data User records, product info, etc.
Patch Track changes Undo/redo, change history
Filter Data filtering Show subset of data

Setting a Dimension

Use SetDimension to replace dimension data completely:

// Set UI dimension to hide Age field
DocsWhere("Users", "{'Name':'Bob'}")
    .SetUIDimension("{'Age':{'Visible':false}}")
    .WantModifyExistingDocuments()
    .OpenForm(result =>
    {
        // Form opens without Age field visible
    });

Extending a Dimension

Use ExtendDimension to merge new data with existing:

// Add UI rules while keeping existing ones
Client.WantModifyExistingDocument()
    .ExtendUIDimension("{'Name':{'ReadOnly':true}}")
    .OpenForm(result =>
    {
        // Name field is read-only in the form
    });

Resetting a Dimension

Use ResetDimension to remove dimension data:

// Remove custom UI settings
Client.WantNotModifyExistingDocument()
    .ResetDimension(DimensionType.UI)
    .OpenForm(result =>
    {
        // Form uses default UI layout
    });

Setting Theme Dimension

// Set dark theme
Client.WantNotModifyExistingDocument()
    .SetThemeDimension(ThemeType.Dark)
    .OpenForm(result =>
    {
        // Form appears in dark theme
    });

// Or with JSON for more control
Client.WantNotModifyExistingDocument()
    .SetThemeDimension("{'DefaultTheme':'Dark','ChooseThemeOnLoginPage':true}")
    .OpenForm(result => { });

Chaining Multiple Dimensions

// Configure both UI and Theme
Client.WantModifyExistingDocument()
    .SetUIDimension("{'Age':{'Visible':false}}")
    .SetThemeDimension(ThemeType.Light)
    .OpenForm(result =>
    {
        // Light theme + Age hidden
    });

Working with Document Dimension

// Extend document with additional data
DocsWhere("Users", "{'Name':'Bob'}")
    .ExtendDocument("{'TempField':'Some value'}")
    .WantNotModifyExistingDocuments()
    .OpenForm(result =>
    {
        // Form shows additional TempField (not saved to DB)
    });

// Set entire document shape
Client.WantNotModifyExistingDocument()
    .SetDocument("{'Name':'','Age':0,'Email':''}")
    .OpenForm(result =>
    {
        // Form uses this exact document structure
    });
✅ Best Practice: Use ExtendDimension when you want to keep existing configuration and just add to it. Use SetDimension when you want complete control and need to replace everything.
💡 Dimension Persistence: When a form opens and closes, dimensions are automatically cleaned up. If you need data to persist, handle it in the result callback.

4. Modifying Existing Documents

The Patch System

When you use WantModifyExistingDocuments, the system tracks all changes in a "patch". When the user saves, the patch is automatically applied to the database.

// Edit Bob's data
DocsWhere("Users", "{'Name':'Bob'}")
    .WantModifyExistingDocuments()
    .OpenForm(result =>
    {
        if (result.Result)
        {
            // All changes user made are saved to database
            // The patch was automatically applied
        }
    });

Modifying with UI Customization

// Edit with custom UI
DocsWhere("Users", "{'Name':'Bob'}")
    .SetUIDimension("{'Weight':{'Label':'Weight (kg)'}}")
    .WantModifyExistingDocuments()
    .OpenForm(result =>
    {
        if (result.Result)
        {
            Console.WriteLine("User updated!");
        }
    });

Modifying Multiple Documents

// Edit all managers
DocsWhere("Users", "{'IsManager':true}")
    .WantModifyExistingDocuments()
    .OpenForm(result =>
    {
        if (result.Result)
        {
            // All matching documents are updated
            var count = result.Collection.DocumentStorage
                .GetDocIDs(context).Count;
            Console.WriteLine($"Updated {count} managers");
        }
    });

Checking if Data Needs Reload

DocsWhere("Users", "{'Name':'Bob'}")
    .WantModifyExistingDocuments()
    .OpenForm(result =>
    {
        if (result.Result && result.NeedReloadData)
        {
            // Backend data changed, refresh your UI
            RefreshUserList();
        }
    });
💡 How Patches Work:
  1. User opens form (patch tracking starts)
  2. User makes changes (patch records each change)
  3. User clicks Save (patch is applied to database)
  4. Form closes (patch is cleaned up)

5. Creating New Documents

Creating in a Target Collection

// Create new user
Client.WantCreateNewDocumentFor("Users")
    .OpenForm(result =>
    {
        if (result.Result)
        {
            // New document created in Users collection
            Console.WriteLine($"Created user with ID: {result.TargetDocID}");
        }
    });

Creating with Initial Data

// Create user with pre-filled data
Client.WantCreateNewDocumentFor("Users")
    .SetDocument("{'Name':'New User','Age':0,'Email':''}")
    .OpenForm(result =>
    {
        if (result.Result)
        {
            Console.WriteLine("User created with template");
        }
    });

Creating from Query Results

// Use existing data as template for new document
DocsWhere("Users", "{'Name':'Bob'}")
    .WantCreateNewDocumentFor("Users")
    .OpenForm(result =>
    {
        if (result.Result)
        {
            // Bob's data used as template, new user created
        }
    });

Adding to Array Fields

Use WantCreateNewDocumentForArray to add items to arrays:

// Add new job to Bob's Jobs array
Client.WantCreateNewDocumentForArray(
    collName: "Users",
    json: "{'Jobs':[@0,$]}",  // @0 means insert at position 0
    targetDocID: 1)  // Bob's document ID
    .SetDocument("{'Company':'','Position':''}")
    .OpenForm(result =>
    {
        if (result.Result)
        {
            // New job added to Bob's Jobs array
        }
    });

Adding Child Objects

// Add new child to Bob
Client.WantCreateNewDocumentForArray(
    "Users",
    "{'Childs':[@0,$]}",
    1)
    .SetDocument("{'Name':'','Age':0}")
    .SetUIDimension("{'Name':{'Label':'Child Name'}}")
    .OpenForm(result =>
    {
        if (result.Result)
        {
            Console.WriteLine("Child added to Bob's record");
        }
    });
💡 Array Position: The format [@0,$] means "insert at position 0 and use the form data ($)". You can use any index or "Last" to append to the end.
⚠️ Important: WantCreateNewDocumentForArray actually creates a temporary document, shows it in a form, then adds the form data to the specified array field when saved. The temporary document is not saved as a separate record.

6. Merging & Unmerging Documents

What is Merging?

Merging combines data from one document into another. It's useful for scenarios like:

Basic Merge Operation

// Merge form data into target document
Client.WantMergeDocumentFor(
    collName: "Users",
    targetDocID: 2)  // Ted's document
    .SetDocument("{'Weight':80,'Car':{'Model':'Toyota'}}")
    .OpenForm(result =>
    {
        if (result.Result)
        {
            // Form data merged into Ted's document
            // Ted now has Weight: 80 and Car.Model: Toyota
        }
    });

Merging with Query

// Use Bob's data to merge into Ted
DocsWhere("Users", "{'Name':'Bob'}")
    .WantMergeDocumentFor("Users", 2)
    .OpenForm(result =>
    {
        if (result.Result)
        {
            // Bob's form data merged into Ted
        }
    });

Selective Merging with UI

// Show only specific fields to merge
DocsWhere("Users", "{'Name':'Bob'}")
    .WantMergeDocumentFor("Users", 2)
    .SetUIDimension("{'Name':{'Visible':false},'Age':{'Visible':false}}")
    .OpenForm(result =>
    {
        if (result.Result)
        {
            // Only visible fields are merged
        }
    });

Unmerge Operation

Unmerge removes previously merged data:

// Remove specific fields from document
Client.WantUnmergeDocumentFor(
    collName: "Users",
    targetDocID: 2)
    .SetDocument("{'Weight':$,'Car':{'Model':$}}")
    .OpenForm(result =>
    {
        if (result.Result)
        {
            // Weight and Car.Model removed from Ted
        }
    });
✅ Merge vs Update:
  • Merge - Adds/replaces fields without touching others
  • Update - Modifies existing document through patch
  • Unmerge - Removes specific fields

7. Advanced Method Chaining

Understanding the Chain

The beauty of FormBuilder is how methods chain together. Each method returns a WantInfo object that has more methods available.

Query/Client
Want Pattern
Dimensions (optional)
More Dimensions (optional)
Query Operations (optional)
OpenForm

Complex Chain Example

// Complex workflow in one chain
DocsWhere("Users", "{'IsManager':true}")
    .AndWhere("{'Weight':GreatOrEqual(70)}")
    .OrderBy("{'Name':$}")
    .Take(5)
    .SetUIDimension("{'Weight':{'Label':'Weight (kg)','ReadOnly':true}}")
    .SetThemeDimension(ThemeType.Dark)
    .WantModifyExistingDocuments()
    .OpenForm(result =>
    {
        // Managers with weight >= 70, sorted by name, first 5
        // Dark theme, weight field read-only
        // Changes saved to database
    });

Using BaseQuery Methods in Chain

WantInfo has all BaseQuery methods available:

Client.WantNotModifyExistingDocument()
    .AndWhere("{'Age':GreatOrEqual(18)}")
    .OrderByDesc("{'Age':$}")
    .Take(10)
    .OpenForm(result =>
    {
        // Top 10 adults, sorted by age descending
    });

// You can even use Count, Select, etc.
var count = Client.WantNotModifyExistingDocument()
    .AndWhere("{'IsManager':true}")
    .Count();  // Returns number without opening form

Multiple Dimension Changes

// Chain multiple dimension operations
DocsWhere("Users", "{'Name':'Bob'}")
    .SetUIDimension("{'Age':{'Visible':false}}")
    .ExtendUIDimension("{'Weight':{'Label':'Weight (kg)'}}")
    .SetThemeDimension(ThemeType.Light)
    .ExtendDocument("{'TempNote':'Review this user'}")
    .WantModifyExistingDocuments()
    .OpenForm(result =>
    {
        // All dimension changes applied
    });

Creating ToCollection Without Opening Form

// Get collection without opening form
var collection = DocsWhere("Users", "{'Name':'Bob'}")
    .SetUIDimension("{'Age':{'Visible':false}}")
    .WantNotModifyExistingDocuments()
    .ToCollection();

// Now you can work with collection directly
var storage = collection.GetStorage();
💡 Pro Tip: The order of chaining matters for readability but not functionality. However, it's best practice to put Want patterns before dimensions, and both before OpenForm.

8. Conditional Form Opening

Skip Opening Based on Condition

Use SkipOpenFormIf to conditionally show a form:

// Only open form if user has permission
bool hasPermission = CheckUserPermission();

Client.SkipOpenFormIf(!hasPermission)
    .OpenForm(result =>
    {
        if (!hasPermission)
        {
            // Form was skipped, but callback still runs
            Console.WriteLine("Permission denied");
        }
        else
        {
            // Form was shown and user interacted with it
        }
    });

Skip with Query

bool isReadOnly = true;

DocsWhere("Users", "{'Name':'Bob'}")
    .SkipOpenFormIf(isReadOnly)
    .WantModifyExistingDocuments()
    .OpenForm(result =>
    {
        // Callback runs even if skipped
    });

Skip with Collection

var collection = Client.GetCollection();
bool hasData = collection.DocumentStorage
    .GetDocIDs(context).Count > 0;

collection.SkipOpenFormIf(!hasData)
    .OpenForm(result =>
    {
        if (!hasData)
        {
            Console.WriteLine("No data to display");
        }
    });

Dynamic Skip Decision

// Check if document exists first
var exists = DocsWhere("Users", "{'Name':'Bob'}")
    .Exists();

DocsWhere("Users", "{'Name':'Bob'}")
    .SkipOpenFormIf(!exists)
    .WantModifyExistingDocuments()
    .OpenForm(result =>
    {
        if (!exists)
        {
            Console.WriteLine("Bob not found");
        }
        else if (result.Result)
        {
            Console.WriteLine("Bob updated");
        }
    });
⚠️ Important: When a form is skipped, the callback still runs! The form just isn't shown to the user. Check the condition in your callback to handle both cases.
💡 Use Cases for Skip:
  • Checking permissions before showing forms
  • Validating data exists before editing
  • Batch operations where some items should be skipped
  • Conditional workflows based on user settings

9. Real-World Examples

Example 1: User Profile Editor

// Complete user profile editing workflow
public void EditUserProfile(uint userId)
{
    DocsWhere("Users", userId)
        .SkipOpenFormIf(!canEdit)
        .SetUIDimension(@"{
            'Salary': {'Visible': " + hasPermission.ToString().ToLower() + @"},
            'SSN': {'Visible': " + hasPermission.ToString().ToLower() + @"}
        }")
        .WantModifyExistingDocuments()
        .OpenForm(result =>
        {
            if (!canEdit)
            {
                ShowNotification("Access denied");
            }
            else if (result.Result)
            {
                ShowNotification("Data updated");
                LogActivity($"User {userId} edited by {CurrentUser.ID}");
            }
        });
}

Example 7: Multi-Step Wizard

public void EmployeeOnboardingWizard()
{
    // Step 1: Basic Information
    Client.WantCreateNewDocumentFor("Employees")
        .SetDocument("{'Name':'','Email':'','Phone':''}")
        .SetUIDimension("{'Title':'Step 1: Basic Information'}")
        .OpenForm(result1 =>
        {
            if (result1.Result)
            {
                var employeeId = result1.TargetDocID;
                
                // Step 2: Department and Position
                DocsWhere("Employees", employeeId)
                    .ExtendDocument("{'Department':'','Position':'','Manager':''}")
                    .SetUIDimension("{'Title':'Step 2: Department Assignment'}")
                    .WantModifyExistingDocuments()
                    .OpenForm(result2 =>
                    {
                        if (result2.Result)
                        {
                            // Step 3: Benefits and Salary
                            DocsWhere("Employees", employeeId)
                                .ExtendDocument("{'Salary':0,'BenefitsPlan':''}")
                                .SetUIDimension("{'Title':'Step 3: Compensation'}")
                                .WantModifyExistingDocuments()
                                .OpenForm(result3 =>
                                {
                                    if (result3.Result)
                                    {
                                        ShowNotification("Employee onboarding complete!");
                                        SendWelcomeEmail(employeeId);
                                    }
                                });
                        }
                    });
            }
        });
}

Example 8: Smart Data Loading

public void EditUserWithFallback(string username)
{
    // Try to find user, create if not exists
    var exists = DocsWhere("Users", $"{{'Name':'{username}'}}")
        .Exists();

    if (exists)
    {
        // User exists - edit them
        DocsWhere("Users", $"{{'Name':'{username}'}}")
            .WantModifyExistingDocuments()
            .OpenForm(result =>
            {
                if (result.Result)
                {
                    ShowNotification("User updated");
                }
            });
    }
    else
    {
        // User doesn't exist - create them
        Client.WantCreateNewDocumentFor("Users")
            .SetDocument($"{{'Name':'{username}'}}")
            .OpenForm(result =>
            {
                if (result.Result)
                {
                    ShowNotification("New user created");
                }
            });
    }
}
✅ Best Practices from Examples:
  • Always validate data before showing forms
  • Use clear UI labels and required field markers
  • Provide user feedback in result callbacks
  • Log important operations for audit trails
  • Handle both success and cancel cases
  • Chain operations for complex workflows

🎓 Summary & Quick Reference

Key Concepts Learned

Common Patterns Cheat Sheet

Task
Pattern
View data only
WantNotModifyExistingDocument()
Edit existing
WantModifyExistingDocuments()
Create new
WantCreateNewDocumentFor(collection)
Add to array
WantCreateNewDocumentForArray(collection, path, docId)
Customize UI
SetUIDimension(json) or ExtendUIDimension(json)
Change theme
SetThemeDimension(ThemeType.Dark)
Conditional open
SkipOpenFormIf(condition)

The Perfect Chain Template

// 1. Start with data source
DocsWhere("Collection", filter)

// 2. Add query refinements (optional)
    .AndWhere(additionalFilter)
    .OrderBy(sortField)
    .Take(limit)

// 3. Configure dimensions (optional)
    .SetUIDimension(uiConfig)
    .SetThemeDimension(theme)
    .ExtendDocument(extraData)

// 4. Declare intention
    .WantModifyExistingDocuments()

// 5. Optional: Conditional skip
    .SkipOpenFormIf(condition)

// 6. Open and handle result
    .OpenForm(result =>
    {
        if (result.Result)
        {
            // Success!
        }
    });