Include System - Tai-Kimura/SwiftJsonUI GitHub Wiki

Include System in SwiftJsonUI

The Include System enables powerful component composition and reusability in SwiftJsonUI, allowing you to build complex UIs from modular JSON components.

Table of Contents

Basic Include Usage

Simple Include

Include another JSON file as a component:

{
  "type": "View",
  "child": [
    {
      "include": "header.json"
    },
    {
      "type": "Label",
      "text": "Main Content"
    },
    {
      "include": "footer.json"
    }
  ]
}

Include with Binding ID

Assign a binding_id to the included component for unique binding context:

{
  "type": "View",
  "child": [
    {
      "include": "user_card.json",
      "binding_id": "user_card_main"
    }
  ]
}

Shared Data System

How shared_data Works

shared_data allows the parent file and included file to share the same data values. The keys in shared_data become available as data variables in the included component.

Example: Sharing User Data

In the parent file:

{
  "data": [
    {
      "name": "currentUser",
      "class": "User"
    }
  ],
  "type": "View",
  "child": [
    {
      "include": "user_card.json",
      "shared_data": {
        "currentUser": "@{currentUser}"
      }
    }
  ]
}

In the included file (user_card.json):

{
  "type": "View",
  "background": "@{currentUser.isPremium ? #FFD700 : #FFFFFF}",
  "child": [
    {
      "type": "Image",
      "src": "@{currentUser.avatarUrl}",
      "width": 60,
      "height": 60
    },
    {
      "type": "Label",
      "text": "@{currentUser.name}",
      "fontColor": "@{currentUser.isPremium ? #FFD700 : #000000}"
    }
  ]
}

Using Variables for Different Data Mapping

If you want to use different variable names in the included component, use the variables system:

In the included file (user_card.json) with variables:

{
  "variables": [
    {
      "name": "user",
      "class": "User"
    }
  ],
  "type": "View",
  "background": "@{user.isPremium ? #FFD700 : #FFFFFF}",
  "child": [
    {
      "type": "Image",
      "src": "@{user.avatarUrl}",
      "width": 60,
      "height": 60
    },
    {
      "type": "Label",
      "text": "@{user.name}",
      "fontColor": "@{user.isPremium ? #FFD700 : #000000}"
    }
  ]
}

In the parent file:

{
  "data": [
    {
      "name": "currentUser",
      "class": "User"
    }
  ],
  "type": "View",
  "child": [
    {
      "include": "user_card.json",
      "shared_data": {
        "user": "@{currentUser}"
      }
    }
  ]
}

Static Shared Data

You can also pass static values:

{
  "include": "button_template.json",
  "shared_data": {
    "buttonText": "Click Me",
    "buttonColor": "#007AFF",
    "isEnabled": true
  }
}

Dynamic Shared Data

Pass bound data using expressions. The keys in shared_data become available in the included component:

{
  "data": [
    {
      "name": "products",
      "class": "Array"
    },
    {
      "name": "selectedIndex",
      "class": "Int",
      "defaultValue": 0
    },
    {
      "name": "user",
      "class": "User"
    },
    {
      "name": "settings",
      "class": "Settings"
    }
  ],
  "child": [
    {
      "include": "product_detail.json",
      "shared_data": {
        "product": "@{products[selectedIndex]}",
        "user": "@{user}",
        "settings": "@{settings}"
      }
    }
  ]
}

In product_detail.json, you can directly use:

{
  "type": "View",
  "child": [
    {
      "type": "Label",
      "text": "@{product.name}"
    },
    {
      "type": "Label",
      "text": "@{product.price}",
      "visibility": "@{user.isLoggedIn ? visible : gone}"
    },
    {
      "type": "Label",
      "text": "@{settings.currency}"
    }
  ]
}

Binding ID for Dynamic Updates

Purpose of binding_id

The binding_id creates a unique binding context for included components, enabling:

  • Independent state management
  • Dynamic updates without affecting other instances
  • Multiple instances of the same component with different data

Example: Multiple Instances

{
  "data": [
    {
      "name": "users",
      "class": "Array"
    }
  ],
  "type": "View",
  "child": [
    {
      "include": "user_row.json",
      "binding_id": "user_1",
      "shared_data": {
        "user": "@{users[0]}"
      }
    },
    {
      "include": "user_row.json",
      "binding_id": "user_2",
      "shared_data": {
        "user": "@{users[1]}"
      }
    },
    {
      "include": "user_row.json",
      "binding_id": "user_3",
      "shared_data": {
        "user": "@{users[2]}"
      }
    }
  ]
}

Collection Views with Includes

For collection views, cellClasses only accepts className. Use separate cell JSON files that are registered with the collection:

{
  "type": "Collection",
  "cellClasses": [
    {
      "className": "UserCell"
    },
    {
      "className": "HeaderCell"
    }
  ],
  "headerClasses": [
    {
      "className": "SectionHeader"
    }
  ],
  "footerClasses": [
    {
      "className": "SectionFooter"
    }
  ]
}

The cell layouts are defined separately and registered with the collection view through the class names.

Advanced Patterns

Nested Includes

Components can include other components:

main.json:

{
  "data": [
    {
      "name": "currentUserId",
      "class": "String"
    }
  ],
  "type": "View",
  "child": [
    {
      "include": "dashboard.json",
      "shared_data": {
        "currentUserId": "@{currentUserId}"
      }
    }
  ]
}

dashboard.json:

{
  "type": "View",
  "child": [
    {
      "include": "dashboard_header.json",
      "shared_data": {
        "title": "Dashboard",
        "currentUserId": "@{currentUserId}"
      }
    },
    {
      "include": "dashboard_stats.json",
      "shared_data": {
        "currentUserId": "@{currentUserId}"
      }
    }
  ]
}

dashboard_header.json:

{
  "type": "View",
  "child": [
    {
      "type": "Label",
      "text": "@{title}"
    },
    {
      "type": "Label",
      "text": "User ID: @{currentUserId}"
    }
  ]
}

Conditional Includes

Use visibility to conditionally show included components:

{
  "type": "View",
  "child": [
    {
      "type": "View",
      "visibility": "@{user.isPremium ? visible : gone}",
      "child": [
        {
          "include": "premium_features.json",
          "shared_data": {
            "user": "@{user}"
          }
        }
      ]
    },
    {
      "type": "View",
      "visibility": "@{!user.isPremium ? visible : gone}",
      "child": [
        {
          "include": "upgrade_prompt.json"
        }
      ]
    }
  ]
}

Template Pattern

Create reusable templates by sharing data:

Parent file:

{
  "data": [
    {
      "name": "saveButton",
      "class": "Dictionary",
      "defaultValue": "[\"text\": \"Save\", \"icon\": \"save_icon\", \"action\": \"saveData\", \"style\": \"primary\"]"
    },
    {
      "name": "cancelButton",
      "class": "Dictionary",
      "defaultValue": "[\"text\": \"Cancel\", \"action\": \"cancelAction\", \"style\": \"secondary\"]"
    }
  ],
  "type": "View",
  "child": [
    {
      "include": "button_template.json",
      "shared_data": {
        "buttonData": "@{saveButton}"
      }
    },
    {
      "include": "button_template.json",
      "shared_data": {
        "buttonData": "@{cancelButton}"
      }
    }
  ]
}

button_template.json:

{
  "type": "Button",
  "text": "@{buttonData.text}",
  "onclick": "@{buttonData.action}",
  "style": "@{buttonData.style}",
  "child": [
    {
      "type": "Image",
      "src": "@{buttonData.icon}",
      "visibility": "@{buttonData.icon != null ? visible : gone}",
      "width": 20,
      "height": 20
    }
  ]
}

Form Builder Pattern

Build dynamic forms using shared data:

Parent file:

{
  "data": [
    {
      "name": "emailField",
      "class": "Dictionary",
      "defaultValue": "[\"label\": \"Email\", \"placeholder\": \"Enter email\", \"type\": \"email\"]"
    },
    {
      "name": "passwordField",
      "class": "Dictionary",
      "defaultValue": "[\"label\": \"Password\", \"placeholder\": \"Enter password\", \"type\": \"password\"]"
    }
  ],
  "type": "View",
  "child": [
    {
      "include": "form_field.json",
      "shared_data": {
        "field": "@{emailField}"
      }
    },
    {
      "include": "form_field.json",
      "shared_data": {
        "field": "@{passwordField}"
      }
    }
  ]
}

form_field.json:

{
  "type": "View",
  "child": [
    {
      "type": "Label",
      "text": "@{field.label}",
      "visibility": "@{field.label != null ? visible : gone}"
    },
    {
      "type": "TextField",
      "hint": "@{field.placeholder}",
      "input": "@{field.type}",
      "visibility": "@{field.type != 'select' ? visible : gone}"
    },
    {
      "type": "SelectBox",
      "items": "@{field.options}",
      "visibility": "@{field.type == 'select' ? visible : gone}"
    }
  ]
}

File Organization

Recommended Structure

Layouts/
├── components/
│   ├── common/
│   │   ├── header.json
│   │   ├── footer.json
│   │   └── loading.json
│   ├── forms/
│   │   ├── form_field.json
│   │   ├── form_section.json
│   │   └── form_validation.json
│   └── cards/
│       ├── user_card.json
│       ├── product_card.json
│       └── info_card.json
├── screens/
│   ├── home.json
│   ├── profile.json
│   └── settings.json
└── templates/
    ├── button_template.json
    ├── list_item_template.json
    └── dialog_template.json

Best Practices

1. Variable Naming

  • Use descriptive variable names
  • Follow consistent naming conventions
  • Document required vs optional variables

2. Default Values

  • Always provide sensible defaults
  • Use null for optional variables
  • Consider edge cases

3. Component Granularity

  • Keep components focused and single-purpose
  • Avoid deeply nested includes (max 3 levels)
  • Balance reusability with complexity

4. Data Flow

  • Pass only necessary data
  • Use binding_id for stateful components
  • Minimize data coupling

5. Performance

  • Cache frequently used components
  • Lazy load heavy components
  • Use conditional includes wisely

Error Handling

Missing Variables

If a variable is not provided and has no default:

{
  "variables": [
    {
      "name": "requiredField",
      "class": "String"
      // No defaultValue - will error if not provided
    }
  ]
}

File Not Found

Handle missing includes gracefully:

{
  "type": "View",
  "child": [
    {
      "include": "optional_component.json",
      "visibility": "@{fileExists ? visible : gone}"
    }
  ]
}

Debugging Tips

  1. Check file paths: Ensure include paths are correct
  2. Verify variables: Match variable names and types
  3. Test defaults: Run without shared_data first
  4. Use unique binding_ids: Avoid conflicts
  5. Log shared_data: Debug data passing

Examples

Complete User Profile Component

profile_screen.json:

{
  "data": [
    {
      "name": "user",
      "class": "User"
    },
    {
      "name": "isEditing",
      "class": "Bool",
      "defaultValue": false
    }
  ],
  "type": "SafeAreaView",
  "child": [
    {
      "include": "components/profile_header.json",
      "binding_id": "profile_header",
      "shared_data": {
        "user": "@{user}",
        "showEditButton": true
      }
    },
    {
      "include": "components/profile_info.json",
      "binding_id": "profile_info",
      "shared_data": {
        "user": "@{user}",
        "isEditable": "@{isEditing}"
      }
    },
    {
      "include": "components/profile_actions.json",
      "binding_id": "profile_actions",
      "shared_data": {
        "userId": "@{user.id}"
      }
    }
  ]
}

Dynamic Tab System

tab_container.json:

{
  "data": [
    {
      "name": "selectedTab",
      "class": "Int",
      "defaultValue": 0
    }
  ],
  "type": "View",
  "child": [
    {
      "type": "View",
      "orientation": "horizontal",
      "child": [
        {
          "include": "tab_button.json",
          "shared_data": {
            "title": "Home",
            "index": 0,
            "isSelected": "@{selectedTab == 0}"
          }
        },
        {
          "include": "tab_button.json",
          "shared_data": {
            "title": "Profile",
            "index": 1,
            "isSelected": "@{selectedTab == 1}"
          }
        }
      ]
    },
    {
      "include": "tab_content.json",
      "binding_id": "tab_content",
      "shared_data": {
        "tabIndex": "@{selectedTab}"
      }
    }
  ]
}

See Also