Dot Notation with Models
Working with nested data in Domma Models using dot notation for forms
Using Dot Notation in Forms
While Domma Models store flat key-value pairs, you can work with nested data structures by combining Models with utility functions. This enables dot notation in forms through _.get() and _.set() utilities.
Key Concept:
Example 1: Basic Nested Data Access
// Create model with nested object values
const userModel = M.create({
profile: { type: 'object', default: {} },
address: { type: 'object', default: {} }
});
// Set nested data
userModel.set('profile', {
firstName: 'John',
lastName: 'Doe',
bio: 'Software developer'
});
userModel.set('address', {
street: '123 Main St',
city: 'New York',
zip: '10001'
});
// Access nested properties using utilities
const city = _.get(userModel.get('address'), 'city');
console.log('City:', city); // 'New York'
// Update nested property
const address = userModel.get('address');
_.set(address, 'city', 'Boston');
userModel.set('address', address); // Trigger change events
Try It:
Model State:
Click "Initialize Model" to start
<pre class="code-block language-javascript mb-4">// Create model with nested object values
const userModel = M.create({
profile: { type: 'object', default: {} },
address: { type: 'object', default: {} }
});
// Set nested data
userModel.set('profile', {
firstName: 'John',
lastName: 'Doe',
bio: 'Software developer'
});
userModel.set('address', {
street: '123 Main St',
city: 'New York',
zip: '10001'
});
// Access nested properties using utilities
const city = _.get(userModel.get('address'), 'city');
console.log('City:', city); // 'New York'
// Update nested property
const address = userModel.get('address');
_.set(address, 'city', 'Boston');
userModel.set('address', address); // Trigger change events
</pre>
<div class="grid grid-cols-2 gap-4">
<div>
<h4 class="font-semibold mb-2">Try It:</h4>
<button type="button" class="btn btn-primary mb-2" id="ex1-init">Initialize Model</button>
<button type="button" class="btn btn-success mb-2" id="ex1-update">Update City to Boston</button>
<button type="button" class="btn btn-info" id="ex1-get">Get Nested Values</button>
</div>
<div>
<h4 class="font-semibold mb-2">Model State:</h4>
<div class="nested-display" id="ex1-display">Click "Initialize Model" to start</div>
</div>
</div>/**
* Storing and Accessing Nested Data - from public/showcase/models/dot-notation.
*/
export function mount(root) {
// Example 1: Basic Nested Data
let ex1Model;
$(root).find('#ex1-init').on('click', () => {
ex1Model = M.create({
profile: {type: 'object', default: {}},
address: {type: 'object', default: {}}
});
ex1Model.set('profile', {
firstName: 'John',
lastName: 'Doe',
bio: 'Software developer'
});
ex1Model.set('address', {
street: '123 Main St',
city: 'New York',
zip: '10001'
});
ex1Model.onChange(() => {
$(root).find('#ex1-display').text(JSON.stringify(ex1Model.toJSON(), null, 2));
});
$(root).find('#ex1-display').text(JSON.stringify(ex1Model.toJSON(), null, 2));
});
$(root).find('#ex1-update').on('click', () => {
if (!ex1Model) {
E.toast('Please initialize the model first');
return;
}
const address = ex1Model.get('address');
_.set(address, 'city', 'Boston');
ex1Model.set('address', address);
});
$(root).find('#ex1-get').on('click', () => {
if (!ex1Model) {
E.toast('Please initialize the model first');
return;
}
const city = _.get(ex1Model.get('address'), 'city');
const firstName = _.get(ex1Model.get('profile'), 'firstName');
E.toast(`First Name: ${firstName}\nCity: ${city}`);
});
}Example 2: Form Binding with Dot Notation
// Helper function for binding nested properties
function bindNested(model, field, path, selector, options = {}) {
M.bind(model, field, selector, {
...options,
format: (data) => _.get(data, path, ''),
parse: (value) => {
const data = _.cloneDeep(model.get(field) || {});
_.set(data, path, value);
return data;
}
});
}
// Create form model
const formModel = M.create({
userData: { type: 'object', default: {} }
});
// Bind form inputs with dot notation paths
bindNested(formModel, 'userData', 'user.name', '#user-name', { twoWay: true });
bindNested(formModel, 'userData', 'user.email', '#user-email', { twoWay: true });
bindNested(formModel, 'userData', 'address.city', '#address-city', { twoWay: true });
Form Inputs:
Live Model Data:
{}
<pre class="code-block language-javascript mb-4">// Helper function for binding nested properties
function bindNested(model, field, path, selector, options = {}) {
M.bind(model, field, selector, {
...options,
format: (data) => _.get(data, path, ''),
parse: (value) => {
const data = _.cloneDeep(model.get(field) || {});
_.set(data, path, value);
return data;
}
});
}
// Create form model
const formModel = M.create({
userData: { type: 'object', default: {} }
});
// Bind form inputs with dot notation paths
bindNested(formModel, 'userData', 'user.name', '#user-name', { twoWay: true });
bindNested(formModel, 'userData', 'user.email', '#user-email', { twoWay: true });
bindNested(formModel, 'userData', 'address.city', '#address-city', { twoWay: true });
</pre>
<div class="grid grid-cols-2 gap-4">
<div>
<h4 class="font-semibold mb-3">Form Inputs:</h4>
<div class="form-group">
<label class="form-label">User Name (user.name)</label>
<input type="text" id="user-name" class="form-input" placeholder="Enter name">
</div>
<div class="form-group">
<label class="form-label">Email (user.email)</label>
<input type="email" id="user-email" class="form-input" placeholder="Enter email">
</div>
<div class="form-group">
<label class="form-label">City (address.city)</label>
<input type="text" id="address-city" class="form-input" placeholder="Enter city">
</div>
</div>
<div>
<h4 class="font-semibold mb-3">Live Model Data:</h4>
<div class="nested-display" id="ex2-display">{}</div>
</div>
</div>/**
* Helper Function for Nested Binding - from public/showcase/models/dot-notation.
*/
export function mount(root) {
function bindNested(model, field, path, selector, options = {}) {
// Ensure we have a DOM element, not a jQuery object
const element = typeof selector === 'string' ?
$(selector)[0] :
(selector.jquery ? selector[0] : selector);
return M.bind(model, field, element, {
...options,
format: (data) => _.get(data, path, ''),
parse: (value) => {
const data = _.cloneDeep(model.get(field) || {});
_.set(data, path, value);
return data;
}
});
}
// Example 2: Form Binding
const ex2Model = M.create({
userData: {type: 'object', default: {}}
});
// Bind inputs with nested paths
bindNested(ex2Model, 'userData', 'user.name', '#user-name', {twoWay: true});
bindNested(ex2Model, 'userData', 'user.email', '#user-email', {twoWay: true});
bindNested(ex2Model, 'userData', 'address.city', '#address-city', {twoWay: true});
// Display updates
ex2Model.onChange(() => {
$(root).find('#ex2-display').text(JSON.stringify(ex2Model.get('userData'), null, 2));
});
}Example 3: Complete Registration Form
Serialized Form Data (with dot notation):
Submit the form to see the data structure
<form id="registration-form">
<div class="form-grid mb-4">
<div class="form-group">
<label class="form-label">First Name *</label>
<input type="text" name="personal.firstName" class="form-input" placeholder="John" required="">
</div>
<div class="form-group">
<label class="form-label">Last Name *</label>
<input type="text" name="personal.lastName" class="form-input" placeholder="Doe" required="">
</div>
<div class="form-group">
<label class="form-label">Email *</label>
<input type="email" name="personal.email" class="form-input" placeholder="john@example.com" required="">
</div>
<div class="form-group">
<label class="form-label">Phone</label>
<input type="tel" name="personal.phone" class="form-input" placeholder="+1 234 567 8900">
</div>
<div class="form-group">
<label class="form-label">Street Address</label>
<input type="text" name="address.street" class="form-input" placeholder="123 Main St">
</div>
<div class="form-group">
<label class="form-label">City *</label>
<input type="text" name="address.city" class="form-input" placeholder="New York" required="">
</div>
<div class="form-group">
<label class="form-label">State/Province</label>
<input type="text" name="address.state" class="form-input" placeholder="NY">
</div>
<div class="form-group">
<label class="form-label">ZIP/Postal Code</label>
<input type="text" name="address.zip" class="form-input" placeholder="10001">
</div>
<div class="form-group">
<label class="form-label">Country *</label>
<select name="address.country" class="form-select" required="">
<option value="">Select Country</option>
<option value="US">United States</option>
<option value="CA">Canada</option>
<option value="GB">United Kingdom</option>
<option value="AU">Australia</option>
<option value="OTHER">Other</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Account Type</label>
<select name="account.type" class="form-select">
<option value="personal">Personal</option>
<option value="business">Business</option>
<option value="premium">Premium</option>
</select>
</div>
<div class="form-group">
<label class="form-label">
<input type="checkbox" name="account.newsletter" value="true">
Subscribe to newsletter
</label>
</div>
<div class="form-group">
<label class="form-label">
<input type="checkbox" name="account.terms" value="true" required="">
I agree to the terms and conditions *
</label>
</div>
</div>
<div class="flex gap-2">
<button type="submit" class="btn btn-primary">Submit Registration</button>
<button type="button" class="btn btn-secondary" id="reset-form">Reset Form</button>
<button type="button" class="btn btn-info" id="populate-form">Populate Sample Data</button>
</div>
</form>
<div class="validation-message mt-4" id="validation-message" style="display: none;"></div>
<div class="mt-4">
<h4 class="font-semibold mb-2">Serialized Form Data (with dot notation):</h4>
<div class="nested-display" id="ex3-display">Submit the form to see the data structure</div>
</div>/**
* Full Form with Validation - from public/showcase/models/dot-notation.
*/
export function mount(root) {
function bindNested(model, field, path, selector, options = {}) {
// Ensure we have a DOM element, not a jQuery object
const element = typeof selector === 'string' ?
$(selector)[0] :
(selector.jquery ? selector[0] : selector);
return M.bind(model, field, element, {
...options,
format: (data) => _.get(data, path, ''),
parse: (value) => {
const data = _.cloneDeep(model.get(field) || {});
_.set(data, path, value);
return data;
}
});
}
// Utility to serialize form with dot notation
function serializeFormWithDotNotation(formSelector) {
const data = {};
$(formSelector).find('input, select, textarea').each(function () {
const element = $(this);
const name = element.attr('name');
if (!name) return;
let value;
if (element.attr('type') === 'checkbox') {
value = element.prop('checked');
} else if (element.attr('type') === 'radio') {
if (element.prop('checked')) {
value = element.val();
} else {
return;
}
} else {
value = element.val();
}
_.set(data, name, value);
});
return data;
}
// Example 3: Registration Form
const registrationModel = M.create({
formData: {
type: 'object',
default: {},
validate: (data) => {
const errors = [];
if (!_.get(data, 'personal.firstName')) {
errors.push('First name is required');
}
if (!_.get(data, 'personal.lastName')) {
errors.push('Last name is required');
}
if (!_.get(data, 'personal.email')?.includes('@')) {
errors.push('Valid email is required');
}
if (!_.get(data, 'address.city')) {
errors.push('City is required');
}
if (!_.get(data, 'address.country')) {
errors.push('Country is required');
}
if (!_.get(data, 'account.terms')) {
errors.push('You must agree to the terms and conditions');
}
return errors.length > 0 ? errors.join(', ') : true;
}
}
});
// Bind all form fields
$('#registration-form input, #registration-form select').each(function () {
const element = this; // Use the DOM element directly
const name = element.getAttribute('name');
if (!name) return;
bindNested(registrationModel, 'formData', name, element, {twoWay: true});
});
// Handle form submission
$(root).find('#registration-form').on('submit', (e) => {
e.preventDefault();
const validation = registrationModel.validate();
const validationMsg = $(root).find('#validation-message');
if (validation.valid) {
const data = serializeFormWithDotNotation('#registration-form');
$(root).find('#ex3-display').text(JSON.stringify(data, null, 2));
validationMsg
.removeClass('validation-error')
.addClass('validation-success')
.text('✓ Form is valid! Check the serialized data below.')
.show();
} else {
validationMsg
.removeClass('validation-success')
.addClass('validation-error')
.text('✗ Validation failed: ' + validation.errors[0].error)
.show();
}
});
// Reset form
$(root).find('#reset-form').on('click', () => {
$(root).find('#registration-form')[0].reset();
registrationModel.set('formData', {});
$(root).find('#ex3-display').text('Submit the form to see the data structure');
$(root).find('#validation-message').hide();
});
// Populate sample data
$(root).find('#populate-form').on('click', () => {
const sampleData = {
personal: {
firstName: 'Jane',
lastName: 'Smith',
email: 'jane.smith@example.com',
phone: '+1 555 123 4567'
},
address: {
street: '456 Oak Avenue',
city: 'San Francisco',
state: 'CA',
zip: '94102',
country: 'US'
},
account: {
type: 'premium',
newsletter: true,
terms: true
}
};
// Set values in form using dot notation paths
function setFormValues(obj, prefix = '') {
for (const [key, value] of Object.entries(obj)) {
const path = prefix ? `${prefix}.${key}` : key;
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
setFormValues(value, path);
} else {
const input = $(`[name="${path}"]`)[0];
if (input) {
if (input.type === 'checkbox') {
input.checked = value;
} else {
input.value = value;
}
}
}
}
}
setFormValues(sampleData);
// Update model
registrationModel.set('formData', sampleData);
$(root).find('#validation-message').hide();
});
}Example 4: Form Auto-Serialization
// Utility to serialize form with dot notation support
function serializeFormWithDotNotation(formSelector) {
const data = {};
$(formSelector).find('input, select, textarea').each(function() {
const element = $(this);
const name = element.attr('name');
if (!name) return;
let value;
if (element.attr('type') === 'checkbox') {
value = element.prop('checked');
} else if (element.attr('type') === 'radio') {
if (element.prop('checked')) {
value = element.val();
} else {
return; // Skip unchecked radio
}
} else {
value = element.val();
}
// Use _.set to handle dot notation
_.set(data, name, value);
});
return data;
}
// Usage
const formData = serializeFormWithDotNotation('#my-form');
// Returns nested object: { user: { name: '...', email: '...' }, address: { ... } }
Pro Tip:
Best Practices
- Use consistent naming: Stick to a naming convention like
section.fieldthroughout your forms - Validate nested data: Use custom validation functions that can check nested properties with
_.get() - Create reusable helpers: Build utility functions like
bindNested()for common patterns - Leverage persistence: Models with nested data can still use the
persistoption for localStorage - Handle undefined paths: Always provide default values when using
_.get()to avoid errors - Trigger change events: After modifying nested data, always
set()the parent object to trigger model change events