Skip to content

Commit 91bdb31

Browse files
authored
Fixed nested fields rendering. (#14)
1 parent 287e13b commit 91bdb31

2 files changed

Lines changed: 182 additions & 27 deletions

File tree

src/Renderer/TangibleFieldsRenderer.php

Lines changed: 56 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -152,26 +152,43 @@ protected function render_section( array $section ): string {
152152
}
153153

154154
/**
155-
* Extract field configs from a nested item.
155+
* Build field configs from a nested item, preserving structure.
156+
*
157+
* For sections, this creates an accordion config that includes the label.
158+
* For tabs, this extracts the fields from each tab.
156159
*
157160
* @param array $item The item structure.
158-
* @return array Array of field configs.
161+
* @return array Array of field configs (may include accordion wrappers).
159162
*/
160163
protected function extract_fields_from_item( array $item ): array {
161164
$fields = [];
162165

163166
if ( $item['type'] === 'section' ) {
167+
// Build section fields including any nested items.
168+
$section_fields = [];
164169
foreach ( $item['fields'] ?? [] as $field ) {
165-
$fields[] = $this->build_field_config( $field );
170+
$section_fields[] = $this->build_field_config( $field );
166171
}
167172
foreach ( $item['items'] ?? [] as $nested ) {
168-
$fields = array_merge( $fields, $this->extract_fields_from_item( $nested ) );
173+
$section_fields = array_merge( $section_fields, $this->extract_fields_from_item( $nested ) );
169174
}
175+
176+
// Wrap in an accordion to preserve the section label.
177+
$fields[] = [
178+
'type' => 'accordion',
179+
'label' => $item['label'],
180+
'value' => true, // Expanded by default.
181+
'fields' => $section_fields,
182+
];
170183
} elseif ( $item['type'] === 'tabs' ) {
171184
foreach ( $item['tabs'] ?? [] as $tab ) {
172185
foreach ( $tab['fields'] ?? [] as $field ) {
173186
$fields[] = $this->build_field_config( $field );
174187
}
188+
// Include nested items from tabs.
189+
foreach ( $tab['items'] ?? [] as $nested ) {
190+
$fields = array_merge( $fields, $this->extract_fields_from_item( $nested ) );
191+
}
175192
}
176193
}
177194

@@ -252,41 +269,53 @@ protected function render_sidebar( array $sidebar ): string {
252269
/**
253270
* Render an action button.
254271
*
272+
* Action buttons are rendered as plain HTML for server-side functionality,
273+
* ensuring they work regardless of JavaScript state.
274+
*
255275
* @param string $action The action identifier.
256276
* @return string The rendered HTML.
257277
*/
258278
protected function render_action( string $action ): string {
259-
$fields = tangible_fields();
260-
261-
$button_config = match ( $action ) {
279+
$config = match ( $action ) {
262280
'save' => [
263-
'type' => 'button',
264-
'label' => 'Save',
265-
'button_type' => 'submit',
266-
'name' => 'action',
267-
'value' => 'save',
268-
'class' => 'button button-primary',
281+
'label' => __( 'Save', 'tangible-object' ),
282+
'type' => 'submit',
283+
'name' => 'action',
284+
'value' => 'save',
285+
'class' => 'button button-primary',
286+
'onclick' => '',
269287
],
270288
'delete' => [
271-
'type' => 'button',
272-
'label' => 'Delete',
273-
'button_type' => 'submit',
274-
'name' => 'action',
275-
'value' => 'delete',
276-
'class' => 'button button-link-delete',
277-
'onclick' => "return confirm('Are you sure you want to delete this item?');",
289+
'label' => __( 'Delete', 'tangible-object' ),
290+
'type' => 'submit',
291+
'name' => 'action',
292+
'value' => 'delete',
293+
'class' => 'button button-link-delete',
294+
'onclick' => "return confirm('" . esc_js( __( 'Are you sure you want to delete this item?', 'tangible-object' ) ) . "');",
278295
],
279296
default => [
280-
'type' => 'button',
281-
'label' => ucfirst( $action ),
282-
'button_type' => 'button',
283-
'name' => 'action',
284-
'value' => $action,
285-
'class' => 'button',
297+
'label' => ucfirst( $action ),
298+
'type' => 'button',
299+
'name' => 'action',
300+
'value' => $action,
301+
'class' => 'button',
302+
'onclick' => '',
286303
],
287304
};
288305

289-
return $fields->render_element( $action . '_button', $button_config );
306+
$onclick_attr = ! empty( $config['onclick'] )
307+
? ' onclick="' . esc_attr( $config['onclick'] ) . '"'
308+
: '';
309+
310+
return sprintf(
311+
'<button type="%s" name="%s" value="%s" class="%s"%s>%s</button>',
312+
esc_attr( $config['type'] ),
313+
esc_attr( $config['name'] ),
314+
esc_attr( $config['value'] ),
315+
esc_attr( $config['class'] ),
316+
$onclick_attr,
317+
esc_html( $config['label'] )
318+
);
290319
}
291320

292321
/**

tests/phpunit/data-view.php

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1324,6 +1324,132 @@ public function test_tangible_fields_renderer_list_view_handles_repeater(): void
13241324
$this->assertStringContainsString( '2 item(s)', $html );
13251325
}
13261326

1327+
public function test_layout_sections_within_tabs_structure(): void {
1328+
$dataset = new DataSet();
1329+
$dataset->add_string( 'name' );
1330+
$dataset->add_string( 'email' );
1331+
1332+
$layout = new \Tangible\EditorLayout\Layout( $dataset );
1333+
$layout->tabs( function( $tabs ) {
1334+
$tabs->tab( 'General', function( $tab ) {
1335+
$tab->section( 'Contact Information', function( $section ) {
1336+
$section->field( 'name' );
1337+
$section->field( 'email' );
1338+
} );
1339+
} );
1340+
} );
1341+
1342+
$structure = $layout->get_structure();
1343+
1344+
// Verify the structure has tabs with nested sections.
1345+
$this->assertCount( 1, $structure['items'] );
1346+
$this->assertEquals( 'tabs', $structure['items'][0]['type'] );
1347+
1348+
$tab = $structure['items'][0]['tabs'][0];
1349+
$this->assertEquals( 'General', $tab['label'] );
1350+
$this->assertArrayHasKey( 'items', $tab );
1351+
$this->assertCount( 1, $tab['items'] );
1352+
1353+
$section = $tab['items'][0];
1354+
$this->assertEquals( 'section', $section['type'] );
1355+
$this->assertEquals( 'Contact Information', $section['label'] );
1356+
$this->assertCount( 2, $section['fields'] );
1357+
}
1358+
1359+
public function test_layout_sidebar_actions_structure(): void {
1360+
$dataset = new DataSet();
1361+
$dataset->add_string( 'name' );
1362+
1363+
$layout = new \Tangible\EditorLayout\Layout( $dataset );
1364+
$layout->section( 'Main', function( $section ) {
1365+
$section->field( 'name' );
1366+
} );
1367+
$layout->sidebar( function( $sidebar ) {
1368+
$sidebar->actions( [ 'save', 'delete' ] );
1369+
} );
1370+
1371+
$structure = $layout->get_structure();
1372+
1373+
// Verify the sidebar has actions.
1374+
$this->assertArrayHasKey( 'sidebar', $structure );
1375+
$this->assertArrayHasKey( 'actions', $structure['sidebar'] );
1376+
$this->assertEquals( [ 'save', 'delete' ], $structure['sidebar']['actions'] );
1377+
}
1378+
1379+
public function test_renderer_extract_fields_preserves_section_structure(): void {
1380+
$dataset = new DataSet();
1381+
$dataset->add_string( 'name' );
1382+
$dataset->add_string( 'email' );
1383+
1384+
$layout = new \Tangible\EditorLayout\Layout( $dataset );
1385+
$layout->tabs( function( $tabs ) {
1386+
$tabs->tab( 'General', function( $tab ) {
1387+
$tab->section( 'Contact Info', function( $section ) {
1388+
$section->field( 'name' );
1389+
$section->field( 'email' );
1390+
} );
1391+
} );
1392+
} );
1393+
1394+
$renderer = new \Tangible\Renderer\TangibleFieldsRenderer();
1395+
1396+
// Set up the renderer's internal state via reflection.
1397+
$layoutProp = new \ReflectionProperty( $renderer, 'layout' );
1398+
$layoutProp->setAccessible( true );
1399+
$layoutProp->setValue( $renderer, $layout );
1400+
1401+
$dataProp = new \ReflectionProperty( $renderer, 'data' );
1402+
$dataProp->setAccessible( true );
1403+
$dataProp->setValue( $renderer, [ 'name' => '', 'email' => '' ] );
1404+
1405+
// Test the protected method.
1406+
$method = new \ReflectionMethod( $renderer, 'extract_fields_from_item' );
1407+
$method->setAccessible( true );
1408+
1409+
$structure = $layout->get_structure();
1410+
$section_item = $structure['items'][0]['tabs'][0]['items'][0];
1411+
1412+
$result = $method->invoke( $renderer, $section_item );
1413+
1414+
// Should return an accordion config that preserves the section label.
1415+
$this->assertCount( 1, $result );
1416+
$this->assertEquals( 'accordion', $result[0]['type'] );
1417+
$this->assertEquals( 'Contact Info', $result[0]['label'] );
1418+
$this->assertTrue( $result[0]['value'] ); // Expanded by default.
1419+
$this->assertArrayHasKey( 'fields', $result[0] );
1420+
$this->assertCount( 2, $result[0]['fields'] );
1421+
}
1422+
1423+
public function test_renderer_action_buttons_output_html(): void {
1424+
$renderer = new \Tangible\Renderer\TangibleFieldsRenderer();
1425+
1426+
// Test the protected method.
1427+
$method = new \ReflectionMethod( $renderer, 'render_action' );
1428+
$method->setAccessible( true );
1429+
1430+
// Test save button.
1431+
$save_html = $method->invoke( $renderer, 'save' );
1432+
$this->assertStringContainsString( '<button', $save_html );
1433+
$this->assertStringContainsString( 'type="submit"', $save_html );
1434+
$this->assertStringContainsString( 'name="action"', $save_html );
1435+
$this->assertStringContainsString( 'value="save"', $save_html );
1436+
$this->assertStringContainsString( '>Save</button>', $save_html );
1437+
1438+
// Test delete button.
1439+
$delete_html = $method->invoke( $renderer, 'delete' );
1440+
$this->assertStringContainsString( '<button', $delete_html );
1441+
$this->assertStringContainsString( 'type="submit"', $delete_html );
1442+
$this->assertStringContainsString( 'value="delete"', $delete_html );
1443+
$this->assertStringContainsString( '>Delete</button>', $delete_html );
1444+
$this->assertStringContainsString( 'onclick=', $delete_html );
1445+
1446+
// Test custom action.
1447+
$custom_html = $method->invoke( $renderer, 'archive' );
1448+
$this->assertStringContainsString( '<button', $custom_html );
1449+
$this->assertStringContainsString( 'value="archive"', $custom_html );
1450+
$this->assertStringContainsString( '>Archive</button>', $custom_html );
1451+
}
1452+
13271453
/**
13281454
* ==========================================================================
13291455
* DataView with TangibleFieldsRenderer Tests

0 commit comments

Comments
 (0)