Demo 2a
Create, Read, Update, Delete
What We Will Learn
- How to add a row to a table
- How to update a row
- How to delete a row
The most important demo for custom forms
What Does the Plugin Do?
-
Get plot ID from parent plugin
-
Populate the menu with rows for the plot from the table
entries
-
New Entry Button: Creates a new, empty entry
-
Menu Selection and New Entry Button: Loads entry values into the form
-
Save Entry Button: Updates entry from the form
Plugin Architecture: Three Files, Three Roles
-
FormDataModel — d2a_data_model.qml
“What fields does the form have?”
A static JS array of field descriptors (name, label). Drives the Repeater — no logic, no runtime state.
-
View — d2a_plugin_component.qml
“How does the form look?”
Layout and widget bindings. A Repeater iterates FormDataModel.fields to create one FormField per entry. Each input reads from and writes back to the Controller.
-
Controller — d2a_form_controller.qml
“What values do the fields hold, and how are they saved?”
Owns the runtime edit buffer (fieldValues) and the dropdown list (entryListModel). Handles all QField API calls: create, load, save, delete.
Model
The Model configures inputs to the form. It is used by the Repeater to dynamically create input widgets for each layer attribute in the entries table.
QtObject{
property var fields: [{"name":"name", "label":"Entry Name"},
{"name":"s1", "label":"Thoughts"},
{"name":"s2", "label":"Feelings"}]
}
Plugin Component with Model and Controller
The Plugin Component is our View. It contains the Model and Controller.
Item{
id: pluginFrame
// not a singleton (has state), must be instantiated
FormController{
id: formController
}
//singleton (static) FormDataModel instance is shared across all plugin instances
property var fieldModel: FormDataModel.fields
}
- View uses Model to build Form
- Controller contains processing code
Model-View Binding using Repeater
The Repeater element in QML allows you to dynamically create multiple instances of a component based on a model. This is useful for creating forms where the number of sections or fields can vary.
component FormField: Rectangle{
id: formField
property var fieldData
RowLayout {
id: fieldRow
Text {
text: fieldData.label
}
TextField {
id: input
text: controller.fieldValues[fieldData.name]
onTextChanged: {
controller.updateField(fieldData.name, text)
}
}
}
}
Repeater {
model: fieldModel
delegate: FormField {
fieldData: modelData
}
}
- Configuration defined in DataModel
- Repeater iterates and creates input objects dynamically
⚠️ Warning
Never bypass the QField API
Never access through the data provider
Why?
- QField Sync is based on QGIS Offline Editing
- Direct changes are not detected
- Synchronization will overwrite your data
Always use FeatureUtils & LayerUtils
CREATE: Insert Feature
// 1. Start editing
layer.startEditing()
// 2. Create empty feature
var newFeature = FeatureUtils.createFeature(layer)
// 3. Set attributes
newFeature.setAttribute("f_uid", StringUtils.createUuid())
newFeature.setAttribute("plot_id", plotId)
newFeature.setAttribute("log_date", new Date().toISOString())
// 4. Add feature to layer
LayerUtils.addFeature(layer, newFeature)
// 5. Commit changes
try{
layer.commitChanges()
} catch (e) {
layer.rollBack()
}
READ: Get Feature
var expression = "plot_id = '" + plotId + "'"
var iterator = LayerUtils.createFeatureIteratorFromExpression(layer, expression)
if (iterator.hasNext()) {
currentFeature = iterator.next()
iterator.close() // Always close as soon as you are done with the iterator
return currentFeature
}
iterator.close() // Always close
ALWAYS close iterator
UPDATE: Update Feature
// 1. Start editing
layer.startEditing()
// 2. get the feature id and the fields from your feature
var fid = currentFeature.id
var fields = currentFeature.fields
var fieldName = 'plot_id'
var value = 'new_plot_id_value'
// 3. Get field index and change value
var fieldIndex = fields.indexOf(fieldName)
if (fieldIndex >= 0) {
layer.changeAttributeValue(fid, fieldIndex, value)
}
// 4. Commit changes
try{
layer.commitChanges()
} catch (e) {
layer.rollBack()
}
DELETE: Delete Feature
// 1. Start editing
// 1. Start editing
layer.startEditing()
// 2. Delete feature
var featureId = feature.id
var success = layer.deleteFeature(featureId)
if (!success) {
// 3a. On error: Rollback
layer.rollBack()
return false
}
// 3b. On success: Commit
var commitSuccess = layer.commitChanges()
if (!commitSuccess) {
layer.rollBack()
}
Important Warning: Field Index
Never use hard-coded indices
Index in QGIS is not the same as index in QField.
❌ WRONG:
value = feature.attribute(3)
✅ CORRECT:
value = feature.attribute("plot_id")
// Or for updates:
var fieldIndex = fields.indexOf("plot_id")
layer.changeAttributeValue(fid, fieldIndex, value)
Exercises for the Next 60 Minutes
Tasks:
- Exercise 1: Deploy and test the Demo2a plugin
- Exercise 2: Add an element to the form using the Model
- Exercise 3: Remove an element from the Form
- Exercise 4: Insert a Default value for the header description, using the Model and View
- Exercise 5: Build your own plugin.