Skip to content

Commit 49b4f5a

Browse files
authored
Merge pull request #3 from iamgerwin/feat/issue-2-flexible-field-support
feat: add flexible field support for nova-flexible-content integration
2 parents 7aa9a83 + 94ec3a6 commit 49b4f5a

5 files changed

Lines changed: 527 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,26 @@
22

33
All notable changes to `nova-dependency-container` will be documented in this file.
44

5+
## [1.0.5] - 2025-11-25
6+
7+
### Added
8+
- Added support for [whitecube/nova-flexible-content](https://github.com/whitecube/nova-flexible-content) Flexible field layouts ([#2](https://github.com/iamgerwin/nova-dependency-container/issues/2))
9+
- Added context-aware dependency resolution for nested field structures
10+
- Added automatic detection of Flexible field attribute prefixes
11+
- Added support for multiple Flexible attribute formats (double underscore and bracket notation)
12+
- Added comprehensive documentation for Flexible field support (`docs/flexible-field-support.md`)
13+
14+
### Changed
15+
- Enhanced `findFieldByAttribute()` method to support prefix-based field lookups
16+
- Enhanced `getFieldValue()` method to check multiple attribute formats
17+
- Enhanced `handleFieldChanged()` to cache both full and base attribute values
18+
19+
### Technical
20+
- Implemented `getFlexibleContextPrefix()` for detecting Flexible field context
21+
- Implemented `extractBaseAttribute()` for parsing prefixed attribute names
22+
- Implemented `getFlexibleAttributeFormats()` for generating alternative attribute patterns
23+
- Updated both `FormField.vue` and `DetailField.vue` components
24+
525
## [1.0.4] - 2025-11-25
626

727
### Fixed

README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ A Laravel Nova field container allowing fields to depend on other field values.
1414
- **Conditional Field Display**: Show/hide fields based on other field values
1515
- **Multiple Dependency Types**: Support for various comparison operators
1616
- **Complex Logic**: Chain multiple conditions together
17+
- **Flexible Field Support**: Works with [whitecube/nova-flexible-content](https://github.com/whitecube/nova-flexible-content) layouts
1718
- **Nova 4 & 5 Compatible**: Works with Laravel Nova 4.x and 5.x (tested with Nova 4.35.x and Nova 5.7.5)
1819
- **Laravel 12 Ready**: Full support for Laravel 11.x and 12.x
1920
- **PHP 8.3 Support**: Modern PHP features and type safety
@@ -187,6 +188,41 @@ CustomTextField::make('Special Field')
187188
->dependsOn('type', 'custom')
188189
```
189190

191+
### Using with Flexible Fields
192+
193+
`NovaDependencyContainer` works seamlessly with [whitecube/nova-flexible-content](https://github.com/whitecube/nova-flexible-content) Flexible fields:
194+
195+
```php
196+
use Iamgerwin\NovaDependencyContainer\NovaDependencyContainer;
197+
use Whitecube\NovaFlexibleContent\Flexible;
198+
use Laravel\Nova\Fields\Select;
199+
use Laravel\Nova\Fields\Text;
200+
201+
Flexible::make('Overlay Items')
202+
->addLayout('Overlay Item', 'overlay_item', [
203+
Select::make('Type')
204+
->options([
205+
'Default' => 'Default',
206+
'Location' => 'Location',
207+
'Contact Us' => 'Contact Us',
208+
]),
209+
210+
NovaDependencyContainer::make([
211+
Text::make('Recipient Email', 'recipient_email')
212+
->rules('nullable', 'email', 'max:255'),
213+
])->dependsOn('type', 'Contact Us'),
214+
215+
NovaDependencyContainer::make([
216+
Text::make('Location Name', 'location_name'),
217+
Text::make('Address', 'address'),
218+
])->dependsOn('type', 'Location'),
219+
]),
220+
```
221+
222+
The package automatically detects the Flexible field context and resolves field attributes correctly, even with dynamically prefixed attribute names.
223+
224+
For more details, see the [Flexible Field Support documentation](docs/flexible-field-support.md).
225+
190226
## Advanced Examples
191227

192228
### Multi-Step Form

docs/flexible-field-support.md

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
# Flexible Field Support
2+
3+
This document describes how `NovaDependencyContainer` works with [whitecube/nova-flexible-content](https://github.com/whitecube/nova-flexible-content) Flexible fields.
4+
5+
## Overview
6+
7+
Starting from version 1.0.5, `NovaDependencyContainer` supports conditional field visibility inside Flexible field layouts. This enables dynamic form behavior within repeatable content blocks.
8+
9+
## The Problem
10+
11+
When fields are placed inside a Flexible layout, their attribute names are automatically prefixed by the Flexible field component. For example:
12+
13+
- A field named `type` inside a Flexible layout becomes `overlay_items__0__type`
14+
- The index (`0`, `1`, `2`, etc.) changes based on the layout position
15+
16+
This prefixing caused the original dependency detection to fail because it looked for exact attribute matches.
17+
18+
## The Solution
19+
20+
The package now implements **context-aware dependency resolution** that:
21+
22+
1. Detects when a field is inside a Flexible layout context
23+
2. Automatically resolves relative field names to their prefixed equivalents
24+
3. Supports multiple Flexible attribute formats (double underscore and bracket notation)
25+
4. Falls back to suffix matching for edge cases
26+
27+
## Usage
28+
29+
Use `NovaDependencyContainer` inside Flexible layouts exactly as you would elsewhere:
30+
31+
```php
32+
use Iamgerwin\NovaDependencyContainer\NovaDependencyContainer;
33+
use Whitecube\NovaFlexibleContent\Flexible;
34+
use Laravel\Nova\Fields\Select;
35+
use Laravel\Nova\Fields\Text;
36+
37+
Flexible::make('Overlay Items')
38+
->addLayout('Overlay Item', 'overlay_item', [
39+
Select::make('Type')
40+
->options([
41+
'Default' => 'Default',
42+
'Location' => 'Location',
43+
'Contact Us' => 'Contact Us',
44+
]),
45+
46+
NovaDependencyContainer::make([
47+
Text::make('Recipient Email', 'recipient_email')
48+
->rules('nullable', 'email', 'max:255'),
49+
])->dependsOn('type', 'Contact Us'),
50+
51+
NovaDependencyContainer::make([
52+
Text::make('Location Name', 'location_name'),
53+
Text::make('Address', 'address'),
54+
])->dependsOn('type', 'Location'),
55+
]),
56+
```
57+
58+
## How It Works
59+
60+
### Attribute Resolution
61+
62+
When you specify `dependsOn('type', 'Contact Us')`, the package:
63+
64+
1. First attempts an exact match for `type`
65+
2. If not found, detects the Flexible context prefix from sibling fields
66+
3. Resolves to the prefixed attribute (e.g., `overlay_items__0__type`)
67+
4. Checks multiple format variations for compatibility
68+
69+
### Supported Attribute Formats
70+
71+
The package recognizes these Flexible field attribute patterns:
72+
73+
| Format | Example |
74+
|--------|---------|
75+
| Double underscore | `overlay_items__0__type` |
76+
| Bracket notation | `overlay_items[0][type]` |
77+
| Single underscore | `overlay_items_0_type` |
78+
79+
### Event Handling
80+
81+
When a field value changes inside a Flexible layout:
82+
83+
1. The change event includes the full prefixed attribute
84+
2. The package extracts the base attribute name
85+
3. Both the full and base attribute values are cached
86+
4. Dependencies are re-evaluated against all matching fields
87+
88+
## Multiple Flexible Groups
89+
90+
Dependencies work correctly across multiple instances of the same layout. Each layout group maintains its own context, so changing `type` in one Overlay Item only affects the dependent fields in that same group.
91+
92+
```php
93+
// Layout instance 0: type = 'Contact Us' -> shows recipient_email
94+
// Layout instance 1: type = 'Location' -> shows location_name, address
95+
// Layout instance 2: type = 'Default' -> hides all dependent fields
96+
```
97+
98+
## Limitations
99+
100+
### Cross-Group Dependencies
101+
102+
Currently, dependencies are resolved within the same Flexible group context. Cross-group dependencies (e.g., a field in group 0 depending on a field in group 1) are not supported.
103+
104+
### Deeply Nested Flexible Fields
105+
106+
The package supports one level of Flexible field nesting. Deeply nested Flexible fields (Flexible inside Flexible) may not resolve correctly.
107+
108+
### Detail View
109+
110+
Flexible field support works best on form views (create/edit). Detail view support is included but may have limitations depending on how the Flexible content renders field data.
111+
112+
## Troubleshooting
113+
114+
### Dependency Not Triggering
115+
116+
If a dependency isn't working inside a Flexible layout:
117+
118+
1. **Check the field attribute name**: Ensure you're using the simple attribute name (e.g., `type`) not the prefixed version
119+
2. **Verify the field exists**: The dependent field must be in the same Flexible layout group
120+
3. **Check the console**: Browser dev tools may show helpful debugging information
121+
122+
### Field Visibility Stuck
123+
124+
If a field remains hidden when it should be visible:
125+
126+
1. Try selecting a different option and then back to the triggering value
127+
2. Ensure the comparison value matches exactly (including case sensitivity)
128+
129+
## Technical Details
130+
131+
### Context Detection
132+
133+
The Flexible context is detected by examining:
134+
135+
1. The container's own `attribute` property
136+
2. Sibling field `attribute` properties
137+
3. Parent component structure
138+
139+
### Regex Patterns Used
140+
141+
```javascript
142+
// Double underscore format
143+
/^(.+__\d+__)/
144+
145+
// Bracket format
146+
/^(.+\[\d+\]\[)/
147+
148+
// Base attribute extraction (double underscore)
149+
/^.+__\d+__(.+)$/
150+
151+
// Base attribute extraction (bracket)
152+
/^.+\[\d+\]\[(.+)\]$/
153+
```
154+
155+
## Version History
156+
157+
- **1.0.5**: Added Flexible field support (this feature)
158+
- **1.0.4**: Nova 4.35.x compatibility fixes
159+
- **1.0.0**: Initial release

resources/js/components/DetailField.vue

Lines changed: 127 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,13 +112,137 @@ export default {
112112
},
113113
114114
findFieldByAttribute(attribute) {
115-
const parent = this.$parent;
116-
if (parent && parent.fields) {
117-
return parent.fields.find(f => f.attribute === attribute);
115+
const allFields = this.getAllFields();
116+
117+
// First try exact match
118+
const exactMatch = allFields.find(f => f.attribute === attribute);
119+
if (exactMatch) {
120+
return exactMatch;
121+
}
122+
123+
// For Flexible fields: resolve attribute relative to current context
124+
const contextPrefix = this.getFlexibleContextPrefix();
125+
if (contextPrefix) {
126+
// Try to find field with same prefix (within same Flexible group)
127+
const prefixedAttribute = `${contextPrefix}${attribute}`;
128+
const prefixedMatch = allFields.find(f => f.attribute === prefixedAttribute);
129+
if (prefixedMatch) {
130+
return prefixedMatch;
131+
}
132+
133+
// Also try alternative Flexible attribute formats
134+
const alternativeFormats = this.getFlexibleAttributeFormats(contextPrefix, attribute);
135+
for (const format of alternativeFormats) {
136+
const match = allFields.find(f => f.attribute === format);
137+
if (match) {
138+
return match;
139+
}
140+
}
141+
}
142+
143+
// Fallback: find any field that ends with the attribute name (for nested contexts)
144+
const suffixMatch = allFields.find(f => {
145+
if (!f.attribute) return false;
146+
const attr = f.attribute;
147+
// Match patterns like: prefix__attribute, prefix[index][attribute]
148+
return attr.endsWith(`__${attribute}`) ||
149+
attr.endsWith(`][${attribute}]`) ||
150+
attr.endsWith(`[${attribute}]`);
151+
});
152+
153+
return suffixMatch || null;
154+
},
155+
156+
/**
157+
* Get all fields from the parent context.
158+
* Handles both standard Nova detail views and nested Flexible field contexts.
159+
*/
160+
getAllFields() {
161+
// Walk up the component tree to find fields
162+
let parent = this.$parent;
163+
let maxDepth = 10;
164+
165+
while (parent && maxDepth-- > 0) {
166+
// Check for fields array directly on parent
167+
if (parent.fields && Array.isArray(parent.fields)) {
168+
return parent.fields;
169+
}
170+
// Check for fields in parent's refs
171+
if (parent.$refs?.fields && Array.isArray(parent.$refs.fields)) {
172+
return parent.$refs.fields.map(f => f.field || f);
173+
}
174+
parent = parent.$parent;
175+
}
176+
177+
return [];
178+
},
179+
180+
/**
181+
* Detect the Flexible field context prefix from the container's own field attribute.
182+
* Flexible fields use prefixes like: flexible_key__index__ or flexible_key[index]
183+
*/
184+
getFlexibleContextPrefix() {
185+
// Check if this container has a prefixed attribute (indicating it's inside a Flexible field)
186+
const ownAttribute = this.field?.attribute || '';
187+
188+
// Pattern 1: Double underscore format (e.g., "overlay_items__0__field_name")
189+
const underscoreMatch = ownAttribute.match(/^(.+__\d+__)/);
190+
if (underscoreMatch) {
191+
return underscoreMatch[1];
192+
}
193+
194+
// Pattern 2: Bracket format (e.g., "overlay_items[0][field_name]")
195+
const bracketMatch = ownAttribute.match(/^(.+\[\d+\]\[)/);
196+
if (bracketMatch) {
197+
return bracketMatch[1];
198+
}
199+
200+
// Try to detect from parent/sibling field attributes
201+
const siblingFields = this.field?.fields || [];
202+
for (const sibling of siblingFields) {
203+
if (sibling.attribute) {
204+
const siblingUnderscoreMatch = sibling.attribute.match(/^(.+__\d+__)/);
205+
if (siblingUnderscoreMatch) {
206+
return siblingUnderscoreMatch[1];
207+
}
208+
const siblingBracketMatch = sibling.attribute.match(/^(.+\[\d+\]\[)/);
209+
if (siblingBracketMatch) {
210+
return siblingBracketMatch[1];
211+
}
212+
}
118213
}
214+
119215
return null;
120216
},
121217
218+
/**
219+
* Generate alternative attribute formats for Flexible fields.
220+
*/
221+
getFlexibleAttributeFormats(prefix, attribute) {
222+
const formats = [];
223+
224+
// Extract the base key and index from the prefix
225+
const underscoreMatch = prefix.match(/^(.+)__(\d+)__$/);
226+
const bracketMatch = prefix.match(/^(.+)\[(\d+)\]\[$/);
227+
228+
if (underscoreMatch) {
229+
const [, key, index] = underscoreMatch;
230+
// Generate bracket format alternative
231+
formats.push(`${key}[${index}][${attribute}]`);
232+
// Single underscore variant
233+
formats.push(`${key}_${index}_${attribute}`);
234+
}
235+
236+
if (bracketMatch) {
237+
const [, key, index] = bracketMatch;
238+
// Generate underscore format alternative
239+
formats.push(`${key}__${index}__${attribute}`);
240+
formats.push(`${key}_${index}_${attribute}`);
241+
}
242+
243+
return formats;
244+
},
245+
122246
isEmpty(value) {
123247
return value === null ||
124248
value === undefined ||

0 commit comments

Comments
 (0)