diff --git a/playground/app.vue b/playground/app.vue
index a285411..07e33f4 100644
--- a/playground/app.vue
+++ b/playground/app.vue
@@ -33,12 +33,14 @@ const valueForm = useState('v3', randomHash)
+
Dynamic:
+
Form:
@@ -48,13 +50,25 @@ const valueForm = useState('v3', randomHash)
:href="$route.path"
data-turbo-frame="form-frame dynamic-frame"
data-testid="link"
- >Visit link
+ >data-turbo-frame="form-frame dynamic-frame"
+ data-turbo-frame="dynamic-frame _self"
+
+
@@ -84,5 +98,7 @@ span {
grid-column: 1 / 3;
white-space: nowrap;
margin-top: 10px;
+ display: grid;
+ gap: 5px;
}
\ No newline at end of file
diff --git a/src/index.ts b/src/index.ts
index 2eaa079..351eb71 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -28,13 +28,27 @@ const refreshFrames = (initiator: HTMLElement, response: any, submitter?: HTMLEl
const targetFrame = initiator.dataset.turboFrame || ''
// Find frames to refresh
+ let selfFrameTargetIndex = -1
const frames = targetFrame
.split(' ')
- .filter(Boolean)
- .map(id => document.querySelector(`turbo-frame#${id}`))
+ .filter((value, index, array) => value && array.indexOf(value) === index) // Remove duplicates
+ .map((id, index) => {
+ if (id === '_self') {
+ // Support data-turbo-frame="_self"
+ selfFrameTargetIndex = index
+ return initiator.closest('turbo-frame')
+ } else {
+ // Support data-turbo-frame="frame_id"
+ return document.querySelector(`turbo-frame#${id}`)
+ }
+ })
- // Turbo will refresh the first frame natively
- frames.shift()
+ if (targetFrame.includes('_self') && selfFrameTargetIndex >= 0)
+ // If _self is provided, Turbo will refresh it natively
+ frames.splice(selfFrameTargetIndex, 1)
+ else
+ // Turbo will refresh the first frame natively
+ frames.shift()
// Refresh other frames
frames.forEach(frame => {
diff --git a/test/module.spec.ts b/test/module.spec.ts
index 9aeba82..8b8700d 100644
--- a/test/module.spec.ts
+++ b/test/module.spec.ts
@@ -9,49 +9,101 @@ describe('Test suite', async () => {
})
describe('Native Turbo', async () => {
- test('Form submission', testTemplate({
- target: 'form',
- updates: { static: false, dynamic: false, form: true },
- }))
-
- test('Link navigation', testTemplate({
- target: 'link',
- updates: { static: false, dynamic: false, form: true },
- }))
+ describe('Form submission', async () => {
+ test('#form should only refresh _self', testTemplate({
+ target: 'form',
+ updates: { static: false, dynamic: false, form: true },
+ }))
+
+ test('#form-self should only refresh _self', testTemplate({
+ target: 'form-self',
+ updates: { static: false, dynamic: false, form: true },
+ }))
+ })
+
+ describe('Link navigation', async () => {
+ test('#link should only refresh _self', testTemplate({
+ target: 'link',
+ updates: { static: false, dynamic: false, form: true },
+ }))
+
+ test('#link-self should only refresh _self', testTemplate({
+ target: 'link-self',
+ updates: { static: false, dynamic: false, form: true },
+ }))
+ })
})
- describe('Module enabled', async () => {
- test('Form submission', testTemplate({
- target: 'form',
- updates: { static: false, dynamic: true, form: true },
- setup: page => page.evaluate('window.$module.enable()'),
- }))
-
- test('Link navigation', testTemplate({
- target: 'link',
- updates: { static: false, dynamic: true, form: true },
- setup: page => page.evaluate('window.$module.enable()'),
- }))
+ describe('With module enabled', async () => {
+ describe('Form submission', async () => {
+ test('#form should refresh _self + dynamic', testTemplate({
+ target: 'form',
+ updates: { static: false, dynamic: true, form: true },
+ setup: page => page.evaluate('window.$module.enable()'),
+ }))
+
+ test('#form-self should refresh _self + dynamic', testTemplate({
+ target: 'form-self',
+ updates: { static: false, dynamic: true, form: true },
+ setup: page => page.evaluate('window.$module.enable()'),
+ }))
+ })
+
+ describe('Link navigation', async () => {
+ test('#link should refresh _self + dynamic', testTemplate({
+ target: 'link',
+ updates: { static: false, dynamic: true, form: true },
+ setup: page => page.evaluate('window.$module.enable()'),
+ }))
+
+ test('#link-self should refresh _self + dynamic', testTemplate({
+ target: 'link-self',
+ updates: { static: false, dynamic: true, form: true },
+ setup: page => page.evaluate('window.$module.enable()'),
+ }))
+ })
})
- describe('Module disabled', async () => {
- test('Form submission', testTemplate({
- target: 'form',
- updates: { static: false, dynamic: false, form: true },
- setup: async (page) => {
- await page.evaluate('window.$module.enable()')
- await page.evaluate('window.$module.disable()')
- },
- }))
-
- test('Link navigation', testTemplate({
- target: 'link',
- updates: { static: false, dynamic: false, form: true },
- setup: async (page) => {
- await page.evaluate('window.$module.enable()')
- await page.evaluate('window.$module.disable()')
- },
- }))
+ describe('With module enabled then disabled', async () => {
+ describe('Form submission', async () => {
+ test('#form should only refresh _self', testTemplate({
+ target: 'form',
+ updates: { static: false, dynamic: false, form: true },
+ setup: async (page) => {
+ await page.evaluate('window.$module.enable()')
+ await page.evaluate('window.$module.disable()')
+ },
+ }))
+
+ test('#form-self should only refresh _self', testTemplate({
+ target: 'form-self',
+ updates: { static: false, dynamic: false, form: true },
+ setup: async (page) => {
+ await page.evaluate('window.$module.enable()')
+ await page.evaluate('window.$module.disable()')
+ },
+ }))
+ })
+
+ describe('Link navigation', async () => {
+ test('#link should only refresh _self', testTemplate({
+ target: 'link',
+ updates: { static: false, dynamic: false, form: true },
+ setup: async (page) => {
+ await page.evaluate('window.$module.enable()')
+ await page.evaluate('window.$module.disable()')
+ },
+ }))
+
+ test('#link-self should only refresh _self', testTemplate({
+ target: 'link-self',
+ updates: { static: false, dynamic: false, form: true },
+ setup: async (page) => {
+ await page.evaluate('window.$module.enable()')
+ await page.evaluate('window.$module.disable()')
+ },
+ }))
+ })
})
})
@@ -62,9 +114,10 @@ const getSnapshot = (page: Page) => Promise.all([
page.getByTestId('form-value').textContent(),
])
+type Target = T | `${T}-${string}`
const testTemplate = (opt: {
setup?: (checkbox: Page) => unknown,
- target: 'link' | 'form',
+ target: Target<'form' | 'link'>,
updates: {
static: boolean
dynamic: boolean
@@ -78,10 +131,10 @@ const testTemplate = (opt: {
opt.setup?.(page)
await new Promise(resolve => setTimeout(resolve, 50))
- if (opt.target === 'link')
- await page.getByTestId('link').click()
+ if (opt.target.startsWith('link'))
+ await page.getByTestId(opt.target).click()
else
- await page.getByTestId('form').evaluate((form: HTMLFormElement) => form.requestSubmit())
+ await page.getByTestId(opt.target).evaluate((form: HTMLFormElement) => form.requestSubmit())
await turboFormRequest
await new Promise(resolve => setTimeout(resolve, 50))