Skip to content

Frontend Components

The Netforce frontend is built with React and provides generic, reusable components that automatically render user interfaces based on backend model definitions and XML layouts. This model-driven approach means you rarely need to write custom frontend code.

Architecture Overview

┌─────────────────────────────────────────┐
│        Generic React Components        │
├─────────────────────────────────────────┤
│  Form │ List │ Field │ Menu │ Board    │
├─────────────────────────────────────────┤
│           XML Layout Parser            │
├─────────────────────────────────────────┤
│            JSON-RPC Client             │
└─────────────────────────────────────────┘

Core Components

Form Component (form.jsx)

Renders record forms based on XML layouts from the backend.

// Usage (typically automatic via routing)
<Form 
    model="account.invoice" 
    active_id={123}
    layout="invoice_form"
    context={{type: "out"}}
    readonly={false}
/>

Features: - Automatically parses XML form layouts - Handles field validation and onChange events
- Manages form state and data loading - Supports nested One2Many fields - Conditional field visibility/readonly

List Component (list.jsx)

Displays record lists with sorting, filtering, and pagination.

// Usage
<List 
    model="account.invoice"
    layout="invoice_list"
    condition={[["state", "=", "draft"]]}
    limit={50}
/>

Features: - Column sorting and filtering - Inline editing capabilities - Row selection and bulk actions - Drag-and-drop reordering (when enabled) - Pagination and lazy loading

Field Components

Individual field components handle different data types:

Text Fields

  • field_char.jsx - Single-line text input
  • field_text.jsx - Multi-line textarea

Numeric Fields

  • field_integer.jsx - Integer input with validation
  • field_decimal.jsx - Decimal input with precision
  • field_float.jsx - Float number input

Date/Time Fields

  • field_date.jsx - Date picker
  • field_datetime.jsx - Date and time picker
  • field_date_range.jsx - Date range selector

Selection Fields

  • field_selection.jsx - Dropdown selection
  • field_boolean.jsx - Checkbox
  • field_boolean_select.jsx - Yes/No dropdown

Relational Fields

  • field_many2one.jsx - Autocomplete relation picker
  • field_one2many.jsx - Embedded list for related records
  • field_many2many.jsx - Multi-selection widget

Special Fields

  • field_file.jsx - File upload component
  • field_json.jsx - JSON editor
  • field_html.jsx - Rich text editor

Field Component Architecture

Many2One Field Example

// field_many2one.jsx simplified structure
class FieldMany2One extends React.Component {
    state = {
        show_menu: false,
        search_results: []
    };

    componentDidMount() {
        this.load_missing_name();
    }

    load_missing_name() {
        // Load display name for ID-only values
        var val = this.props.data[this.props.name];
        if (typeof val === 'number') {
            var field = ui_params.get_field_by_path(this.props.model, this.props.name);
            rpc.execute(field.relation, "name_get", [[val]], {}, (err, data) => {
                this.props.data[this.props.name] = [val, data[0][1]];
                this.forceUpdate();
            });
        }
    }

    render() {
        var field = ui_params.get_field_by_path(this.props.model, this.props.name);
        var val = this.props.data[this.props.name];
        var display_name = Array.isArray(val) ? val[1] : val;

        return (
            <div className="field-many2one">
                <input 
                    value={display_name || ''}
                    onChange={this.on_search}
                    onFocus={this.show_dropdown}
                />
                {this.state.show_menu && (
                    <div className="dropdown-menu">
                        {this.state.search_results.map(item => (
                            <div key={item[0]} onClick={() => this.select_item(item)}>
                                {item[1]}
                            </div>
                        ))}
                    </div>
                )}
            </div>
        );
    }
}

Field Props Interface

All field components receive these standard props:

{
    model: "account.invoice",           // Model name
    name: "customer_id",                // Field name
    data: {...},                        // Record data object
    readonly: false,                    // Read-only state
    required: true,                     // Required field
    context: {...},                     // Additional context
    onchange: function,                 // Change handler
    attrs: {...}                        // Dynamic attributes
}

Layout Parsing

XML to React Conversion

The frontend parses XML layouts and converts them to React components:

<!-- XML Layout -->
<form model="account.invoice">
    <field name="number" span="3"/>
    <field name="customer_id" span="9" onchange="customer_changed"/>
</form>
// Resulting React Structure
<Form model="account.invoice">
    <div className="row">
        <div className="col-md-3">
            <FieldChar name="number" {...props} />
        </div>
        <div className="col-md-9">
            <FieldMany2One 
                name="customer_id" 
                onchange="customer_changed"
                {...props} 
            />
        </div>
    </div>
</Form>

Dynamic Field Rendering

Field components are selected dynamically based on field type:

// form.jsx - Dynamic field rendering
render_field(field_el) {
    const field_name = field_el.getAttribute("name");
    const field_def = ui_params.get_field_by_path(this.props.model, field_name);

    let Component;
    switch (field_def.type) {
        case "char": Component = FieldChar; break;
        case "text": Component = FieldText; break;
        case "integer": Component = FieldInteger; break;
        case "decimal": Component = FieldDecimal; break;
        case "boolean": Component = FieldBoolean; break;
        case "date": Component = FieldDate; break;
        case "datetime": Component = FieldDateTime; break;
        case "selection": Component = FieldSelection; break;
        case "many2one": Component = FieldMany2One; break;
        case "one2many": Component = FieldOne2Many; break;
        case "many2many": Component = FieldMany2Many; break;
        default: Component = FieldChar;
    }

    return <Component
        model={this.props.model}
        name={field_name}
        data={this.state.data}
        readonly={this.is_readonly(field_el)}
        {...this.get_field_props(field_el)}
    />;
}

Data Flow

Component Lifecycle

  1. Mount: Component loads XML layout from backend
  2. Parse: XML is parsed and field components are identified
  3. Load: Record data is fetched via JSON-RPC
  4. Render: Components render with current data
  5. Interact: User changes trigger onChange handlers
  6. Update: Changes are sent to backend via JSON-RPC

State Management

// Form component state example
class Form extends React.Component {
    state = {
        data: null,              // Current record data
        layout_el: null,         // Parsed XML layout
        active_id: null,         // Record ID
        loading: false,          // Loading state
        errors: {},              // Validation errors
        dirty: false             // Has unsaved changes
    };

    load_data() {
        // Fetch record data
        const field_names = this.get_field_names_from_layout();
        rpc.execute(this.props.model, "read", [[this.state.active_id]], {
            fields: field_names,
            context: this.props.context
        }, (err, data) => {
            this.setState({data: data[0], loading: false});
        });
    }

    save_data() {
        // Save changes to backend
        rpc.execute(this.props.model, "write", [[this.state.active_id]], this.state.data, {
            context: this.props.context
        }, (err, result) => {
            this.setState({dirty: false});
        });
    }
}

Customization

Custom Field Components

Create custom field components for specialized input types:

// field_custom.jsx
import React from 'react';

class FieldCustom extends React.Component {
    render() {
        const value = this.props.data[this.props.name];

        return (
            <div className="field-custom">
                <label>{this.get_field_string()}</label>
                <input
                    type="text"
                    value={value || ''}
                    onChange={(e) => this.on_change(e.target.value)}
                    readOnly={this.props.readonly}
                />
            </div>
        );
    }

    on_change(new_value) {
        this.props.data[this.props.name] = new_value;
        if (this.props.onchange) {
            this.props.onchange();
        }
        this.forceUpdate();
    }

    get_field_string() {
        const field = ui_params.get_field_by_path(this.props.model, this.props.name);
        return field.string;
    }
}

export default FieldCustom;

Custom Form Components

Override form behavior for specific models:

// custom_invoice_form.jsx
import React from 'react';
import Form from './form';

class CustomInvoiceForm extends Form {
    render() {
        // Custom rendering logic
        return (
            <div className="custom-invoice-form">
                <div className="custom-header">
                    <h2>Invoice #{this.state.data?.number}</h2>
                </div>
                {super.render()}
                <div className="custom-footer">
                    <button onClick={this.send_email}>Send Email</button>
                </div>
            </div>
        );
    }

    send_email = () => {
        // Custom method
        rpc.execute(this.props.model, "send_email", [this.state.active_id]);
    }
}

UI Parameters

The ui_params module provides access to model and field definitions:

import ui_params from '../ui_params';

// Get field definition
const field = ui_params.get_field_by_path("account.invoice", "customer_id");
console.log(field.type);      // "many2one"
console.log(field.relation);  // "contact"
console.log(field.string);    // "Customer"

// Get model info
const model = ui_params.get_model("account.invoice");
console.log(model.string);    // "Invoice"

// Find layout
const layout = ui_params.find_layout({
    model: "account.invoice",
    type: "form"
});

// Check permissions
const access = ui_params.check_model_access("account.invoice", "write");

Styling and Themes

CSS Classes

Components use Bootstrap-based CSS classes:

/* Standard field styling */
.field-wrapper {
    margin-bottom: 15px;
}

.field-label {
    font-weight: bold;
    margin-bottom: 5px;
}

.field-input {
    width: 100%;
    padding: 8px;
    border: 1px solid #ccc;
    border-radius: 4px;
}

.field-readonly {
    background-color: #f5f5f5;
    cursor: not-allowed;
}

.field-required .field-label:after {
    content: " *";
    color: red;
}

.field-error {
    border-color: #d9534f;
}

.field-error-message {
    color: #d9534f;
    font-size: 12px;
    margin-top: 5px;
}

Responsive Grid

The framework uses a 12-column grid system:

<!-- XML span attributes map to Bootstrap columns -->
<field name="field1" span="12"/>  <!-- col-md-12 (full width) -->
<field name="field2" span="6"/>   <!-- col-md-6 (half width) -->  
<field name="field3" span="4"/>   <!-- col-md-4 (third width) -->
<field name="field4" span="3"/>   <!-- col-md-3 (quarter width) -->

Performance Optimization

Component Optimization

class OptimizedFieldComponent extends React.Component {
    shouldComponentUpdate(nextProps, nextState) {
        // Only re-render if relevant props changed
        const currentValue = this.props.data[this.props.name];
        const nextValue = nextProps.data[nextProps.name];

        return currentValue !== nextValue || 
               this.props.readonly !== nextProps.readonly ||
               this.props.required !== nextProps.required;
    }

    render() {
        // Expensive render logic
        return <div>...</div>;
    }
}

Lazy Loading

// Large forms with lazy-loaded sections
class LazySection extends React.Component {
    state = { loaded: false, data: null };

    componentDidMount() {
        // Load data when section becomes visible
        if (this.props.visible) {
            this.loadData();
        }
    }

    componentDidUpdate(prevProps) {
        if (!prevProps.visible && this.props.visible && !this.state.loaded) {
            this.loadData();
        }
    }

    loadData() {
        // Fetch section-specific data
        rpc.execute(this.props.model, "get_section_data", [this.props.record_id], {}, 
            (err, data) => {
                this.setState({ loaded: true, data });
            }
        );
    }
}

Error Handling

Field Validation

class ValidatedField extends React.Component {
    state = { errors: [] };

    validate(value) {
        const errors = [];

        if (this.props.required && !value) {
            errors.push("This field is required");
        }

        if (this.props.pattern && !this.props.pattern.test(value)) {
            errors.push("Invalid format");
        }

        this.setState({ errors });
        return errors.length === 0;
    }

    render() {
        return (
            <div className={`field-wrapper ${this.state.errors.length ? 'field-error' : ''}`}>
                <input 
                    onChange={(e) => this.onChange(e.target.value)}
                    onBlur={(e) => this.validate(e.target.value)}
                />
                {this.state.errors.map((error, i) => (
                    <div key={i} className="field-error-message">{error}</div>
                ))}
            </div>
        );
    }
}

Best Practices

1. Component Structure

  • Keep components focused on single responsibility
  • Use consistent prop interfaces
  • Handle loading and error states properly
  • Implement proper cleanup in componentWillUnmount

2. Performance

  • Use shouldComponentUpdate or React.memo for expensive components
  • Lazy-load data and components when appropriate
  • Avoid unnecessary re-renders with proper state management
  • Use keys properly in lists

3. User Experience

  • Provide immediate feedback for user actions
  • Handle loading states gracefully
  • Show clear error messages
  • Support keyboard navigation
  • Implement proper form validation

4. Accessibility

  • Use semantic HTML elements
  • Provide proper ARIA labels
  • Support keyboard navigation
  • Ensure sufficient color contrast
  • Test with screen readers

Next Steps