Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10,121 changes: 2,748 additions & 7,373 deletions dist/tiny-form-fields.esm.js

Large diffs are not rendered by default.

10,119 changes: 2,747 additions & 7,372 deletions dist/tiny-form-fields.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/tiny-form-fields.min.css

Large diffs are not rendered by default.

102 changes: 102 additions & 0 deletions e2e/equals-field.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { test, expect } from '@playwright/test';
import { addField } from './test-utils';

const fieldHandle = '.tff-field-container .tff-drag-handle-icon';

test('Equals(field) dropdown disabled when no other fields and excludes self', async ({ page }) => {
await page.goto('');

// Set a desktop viewport
await page.setViewportSize({ width: 2048, height: 800 });
await page.goto('');

await test.step('Create an email field', async () => {
await addField(page, 'Email', undefined, {
link: 'Email',
label: 'Enter email',
});
});

await test.step('Check EqualsField disabled with no other fields', async () => {
// Reopen field settings to configure
await page.locator(fieldHandle).last().click();
await expect(page.getByText('Email question title')).toHaveCount(1);

// Add field logic (field rule)
await page.getByRole('button', { name: 'Add field logic' }).click();
const fieldRule = page.locator('.tff-field-rule').last();

// EqualsField option should be disabled because there are no other fields
const equalsOption = fieldRule.locator(
'select.tff-comparison-type option[value="EqualsField"]'
);
await expect(equalsOption).toBeDisabled();

// Remove logic
await fieldRule.locator('.tff-show-or-hide').selectOption({ index: 0 });
await expect(fieldRule).not.toBeVisible();

// Close editor
await page.locator('.tff-close-button').click();
});

await test.step('Add another email field', async () => {
await addField(page, 'Email', undefined, {
link: 'Email',
label: 'Confirm email',
});
});

await test.step('Verify EqualsField dropdown excludes self', async () => {
await page.locator(fieldHandle).last().click();
await expect(page.getByText('Email question title')).toHaveCount(1);
await page.getByRole('button', { name: 'Add field logic' }).click();
const fieldRule = page.locator('.tff-field-rule').last();

// Add a condition and set its comparison to EqualsField
await fieldRule.locator('.tff-comparison-type').last().selectOption('EqualsField');
await expect(fieldRule.locator('.tff-comparison-value')).toBeDisabled();

// Remove logic
await fieldRule.locator('.tff-show-or-hide').selectOption({ index: 0 });
await expect(fieldRule).not.toBeVisible();
});

await test.step('Add a field that only shows when both email fields are equal', async () => {
await addField(page, 'Single-line free text', undefined, {
link: 'Single-line free text',
label: 'Show if emails match',
description: 'Show if emails match',
});

await page.locator(fieldHandle).last().click();
await expect(page.getByText('Single-line free text question title')).toHaveCount(1);
await page.getByRole('button', { name: 'Add field logic' }).click();
const fieldRule = page.locator('.tff-field-rule').last();

const equalsOption = fieldRule.locator(
'select.tff-comparison-type option[value="EqualsField"]'
);
await expect(equalsOption).toBeEnabled();
await fieldRule.locator('.tff-comparison-type').last().selectOption('EqualsField');

page.locator('select.tff-text-field.tff-question-title').last().selectOption('Enter email');
});

await test.step('Verify form field logic works', async () => {
const pagePromise = page.waitForEvent('popup');
await page.getByRole('link', { name: 'View form' }).click();
const newPage = await pagePromise;

await expect(newPage.getByLabel('Enter email')).toBeVisible();
await expect(newPage.getByLabel('Confirm email')).toBeVisible();
await expect(newPage.getByLabel('Show if emails match')).not.toBeVisible();

await newPage.getByLabel('Enter email').fill('test@example.com');
await newPage.getByLabel('Confirm email').fill('test@example.com');
await expect(newPage.getByLabel('Show if emails match')).toBeVisible();
});

// Close editor
await page.locator('.tff-close-button').click();
});
4 changes: 4 additions & 0 deletions go/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,10 @@ func isVisibilityRuleSatisfied(rule VisibilityRule, values url.Values) bool {
conditionMet = strings.Contains(fieldValue, condition.Comparison.Value)
case "EndsWith":
conditionMet = strings.HasSuffix(fieldValue, condition.Comparison.Value)
case "EqualsField":
// the value stored in Comparison.Value is the name of another field
comparisonValue := values.Get(condition.Comparison.Value)
conditionMet = fieldValue == comparisonValue
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm not sure (and _test.go didn't cover) what happens to checkboxes

Elm side is true for any value that is common between the two fields (intended or not)

Screenshot 2025-11-01 at 11 25 03 AM Screenshot 2025-11-01 at 11 27 08 AM

case "GreaterThan":
// Try to parse comparison value as float64
comparisonValue, comparisonErr := strconv.ParseFloat(condition.Comparison.Value, 64)
Expand Down
136 changes: 136 additions & 0 deletions go/validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1395,6 +1395,142 @@ func TestVisibilityRules(t *testing.T) {
},
expectedError: nil,
},
{
name: "HideWhen rule - email and confirm_email fields are equal - should pass",
formFields: `
[
{
"name": "email",
"type": {
"type": "ShortText",
"inputType": "Email",
"attributes": {
"pattern": "^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$",
"multiple": "true",
"inputmode": "text"
}
},
"label": "Email",
"presence": "Required"
},
{
"name": "confirm_email",
"type": {
"type": "ShortText",
"inputType": "Email",
"attributes": {
"type": "email",
"pattern": "^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$",
"multiple": "true"
}
},
"label": "Confirm Email",
"presence": "Required"
},
{
"type": {
"type": "ShortText",
"inputType": "Disable form submit",
"attributes": {
"type": "text",
"class": "size-0-invisible",
"value": "form-invalid",
"pattern": "form-ok"
}
},
"label": " ",
"presence": "Required",
"visibilityRule": [
{
"type": "HideWhen",
"conditions": [
{
"type": "Field",
"fieldName": "email",
"comparison": {
"type": "EqualsField",
"value": "confirm_email"
}
}
]
}
]
}
]`,
values: url.Values{
"email": []string{"email@example.com"},
"confirm_email": []string{"email@example.com"},
},
expectedError: nil,
},
{
name: "HideWhen rule - email and confirm_email fields are NOT equal - should fail",
formFields: `
[
{
"name": "email",
"type": {
"type": "ShortText",
"inputType": "Email",
"attributes": {
"pattern": "^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$",
"multiple": "true",
"inputmode": "text"
}
},
"label": "Email",
"presence": "Required"
},
{
"name": "confirm_email",
"type": {
"type": "ShortText",
"inputType": "Email",
"attributes": {
"type": "email",
"pattern": "^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$",
"multiple": "true"
}
},
"label": "Confirm Email",
"presence": "Required"
},
{
"type": {
"type": "ShortText",
"inputType": "Disable form submit",
"attributes": {
"type": "text",
"class": "size-0-invisible",
"value": "form-invalid",
"pattern": "form-ok"
}
},
"label": " ",
"presence": "Required",
"visibilityRule": [
{
"type": "HideWhen",
"conditions": [
{
"type": "Field",
"fieldName": "email",
"comparison": {
"type": "EqualsField",
"value": "confirm_email"
}
}
]
}
]
}
]`,
values: url.Values{
"email": []string{"email@example.com"},
"confirm_email": []string{"different@example.com"},
},
expectedError: ErrRequiredFieldMissing,
},
}

for _, tt := range scenarios {
Expand Down
6 changes: 6 additions & 0 deletions input.css
Original file line number Diff line number Diff line change
Expand Up @@ -875,3 +875,9 @@
.tff-button-secondary {
@apply bg-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50;
}

.tff-comparison-value:focus,
.tff-comparison-value:focus-visible {
outline: none !important;
box-shadow: none !important;
}
Loading
Loading