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 inputfield_text.jsx- Multi-line textarea
Numeric Fields¶
field_integer.jsx- Integer input with validationfield_decimal.jsx- Decimal input with precisionfield_float.jsx- Float number input
Date/Time Fields¶
field_date.jsx- Date pickerfield_datetime.jsx- Date and time pickerfield_date_range.jsx- Date range selector
Selection Fields¶
field_selection.jsx- Dropdown selectionfield_boolean.jsx- Checkboxfield_boolean_select.jsx- Yes/No dropdown
Relational Fields¶
field_many2one.jsx- Autocomplete relation pickerfield_one2many.jsx- Embedded list for related recordsfield_many2many.jsx- Multi-selection widget
Special Fields¶
field_file.jsx- File upload componentfield_json.jsx- JSON editorfield_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¶
- Mount: Component loads XML layout from backend
- Parse: XML is parsed and field components are identified
- Load: Record data is fetched via JSON-RPC
- Render: Components render with current data
- Interact: User changes trigger onChange handlers
- 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
shouldComponentUpdateor 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¶
- Explore the API Reference for RPC communication details
- Review Models to understand backend data structure
- Check Layouts for XML layout definitions
- Try the Quick Start Tutorial to build a complete interface