Skip to content

Commit a306edb

Browse files
committed
Listen for events using visitModal()
See #17
1 parent b7d089c commit a306edb

File tree

11 files changed

+153
-41
lines changed

11 files changed

+153
-41
lines changed

demo-app/resources/js/Pages/Visit.jsx

+12-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,17 @@ export default function Visit() {
88
visitModal('#local');
99
};
1010

11+
const visitEdit = () => {
12+
visitModal('/users/1/edit', {
13+
navigate: true,
14+
listeners: {
15+
userGreets: function (greeting) {
16+
alert(greeting);
17+
}
18+
}
19+
})
20+
}
21+
1122
return (
1223
<>
1324
<Container>
@@ -19,7 +30,7 @@ export default function Visit() {
1930
<button onClick={() => visitModal('/data', { method: 'post', data: { message: 'Hi again!' } })} type="button">
2031
Open Route Modal
2132
</button>
22-
<button onClick={() => visitModal('/users/1/edit', { navigate: true })} type="button">
33+
<button onClick={visitEdit} type="button">
2334
Open Route Modal With Navigate
2435
</button>
2536
</div>

demo-app/resources/js/Pages/Visit.vue

+12-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
<script setup>
22
import Container from './Container.vue'
33
import { Modal, visitModal } from '@inertiaui/modal-vue'
4+
5+
function visitEdit() {
6+
visitModal('/users/1/edit', {
7+
navigate: true,
8+
listeners: {
9+
userGreets(greeting) {
10+
alert(greeting);
11+
}
12+
}
13+
})
14+
}
415
</script>
516

617
<template>
@@ -16,7 +27,7 @@ import { Modal, visitModal } from '@inertiaui/modal-vue'
1627
Open Route Modal
1728
</button>
1829

19-
<button @click="visitModal('/users/1/edit', { navigate: true })" type="button">
30+
<button @click="visitEdit" type="button">
2031
Open Route Modal With Navigate
2132
</button>
2233
</div>

demo-app/tests/Browser/EmitTest.php

+15
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,21 @@ public function it_can_dispatch_an_event_from_the_modal_to_the_modal_link(bool $
2626
});
2727
}
2828

29+
#[DataProvider('booleanProvider')]
30+
#[Test]
31+
public function it_can_dispatch_an_event_from_the_modal_to_the_visit_modal_method(bool $navigate)
32+
{
33+
$this->browse(function (Browser $browser) use ($navigate) {
34+
$browser->visit('/visit'.($navigate ? '?navigate=1' : ''))
35+
->waitForText('Visit programmatically')
36+
->press('Open Route Modal With Navigate')
37+
->waitFor('.im-modal-content')
38+
->clickLink('Send Message', 'button')
39+
->assertDialogOpened('Hello from EditUser')
40+
->dismissDialog();
41+
});
42+
}
43+
2944
#[DataProvider('booleanProvider')]
3045
#[Test]
3146
public function it_can_dispatch_events_back_and_forth_between_nested_modals(bool $navigate)

docs/basic-usage.md

+1
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,7 @@ visitModal('/users/create', {
329329
config: {
330330
slideover: true,
331331
}
332+
listeners: {},
332333
onClose: () => console.log('Modal closed'),
333334
onAfterLeave: () => console.log('Modal removed from DOM'),
334335
queryStringArrayFormat: 'brackets',

docs/event-bus.md

+12-2
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export default function MyPage() {
7272

7373
:::
7474

75-
On the parent page, you can listen to the event like this:
75+
On the parent page, you can listen to the event on the `ModalLink` component:
7676

7777
::: code-group
7878

@@ -98,7 +98,17 @@ export default function MyPage() {
9898
}
9999
```
100100

101-
:::
101+
If you're [programmatically opening the modal](/basic-usage.html#programmatic-usage), you add listeners using the `listeners` option:
102+
103+
```js
104+
visitModal('/users/create', {
105+
listeners: {
106+
increaseBy(amount) {
107+
console.log(`Increase by ${amount}`);
108+
}
109+
}
110+
})
111+
```
102112

103113
## Nested / Stacked Modals
104114

react/src/ModalRoot.jsx

+18-11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createElement, useEffect, useState, useRef } from 'react'
22
import { default as Axios } from 'axios'
3-
import { except, only } from './helpers'
3+
import { except, only, kebabCase } from './helpers'
44
import { router, usePage } from '@inertiajs/react'
55
import { mergeDataIntoQueryString } from '@inertiajs/core'
66
import { createContext, useContext } from 'react'
@@ -164,11 +164,13 @@ export const ModalStackProvider = ({ children }) => {
164164
}
165165

166166
on = (event, callback) => {
167+
event = kebabCase(event)
167168
this.listeners[event] = this.listeners[event] ?? []
168169
this.listeners[event].push(callback)
169170
}
170171

171172
off = (event, callback) => {
173+
event = kebabCase(event)
172174
if (callback) {
173175
this.listeners[event] = this.listeners[event]?.filter((cb) => cb !== callback) ?? []
174176
} else {
@@ -177,7 +179,7 @@ export const ModalStackProvider = ({ children }) => {
177179
}
178180

179181
emit = (event, ...args) => {
180-
this.listeners[event]?.forEach((callback) => callback(...args))
182+
this.listeners[kebabCase(event)]?.forEach((callback) => callback(...args))
181183
}
182184

183185
registerEventListenersFromProps = (props) => {
@@ -187,14 +189,9 @@ export const ModalStackProvider = ({ children }) => {
187189
.filter((key) => key.startsWith('on'))
188190
.forEach((key) => {
189191
// e.g. onRefreshKey -> refresh-key
190-
const snakeCaseKey = key
191-
.replace(/^on/, '')
192-
.replace(/^./, (firstLetter) => firstLetter.toLowerCase())
193-
.replace(/([A-Z])/g, '-$1')
194-
.toLowerCase()
195-
196-
this.on(snakeCaseKey, props[key])
197-
unsubscribers.push(() => this.off(snakeCaseKey, props[key]))
192+
const eventName = kebabCase(key).replace(/^on-/, '')
193+
this.on(eventName, props[key])
194+
unsubscribers.push(() => this.off(eventName, props[key]))
198195
})
199196

200197
return () => unsubscribers.forEach((unsub) => unsub())
@@ -274,7 +271,17 @@ export const ModalStackProvider = ({ children }) => {
274271
options.onAfterLeave,
275272
options.queryStringArrayFormat ?? 'brackets',
276273
options.navigate ?? getConfig('navigate'),
277-
)
274+
).then((modal) => {
275+
const listeners = options.listeners ?? {}
276+
277+
Object.keys(listeners).forEach((event) => {
278+
// e.g. refreshKey -> refresh-key
279+
const eventName = kebabCase(event)
280+
modal.on(eventName, listeners[event])
281+
})
282+
283+
return modal
284+
})
278285

279286
const visit = (
280287
href,

react/src/helpers.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
import { except, only, rejectNullValues, waitFor } from './../../vue/src/helpers.js'
2-
export { except, only, rejectNullValues, waitFor }
1+
import { except, only, rejectNullValues, waitFor, kebabCase } from './../../vue/src/helpers.js'
2+
export { except, only, rejectNullValues, waitFor, kebabCase }

vue/src/helpers.js

+27-1
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,30 @@ function waitFor(conditionFn, waitForSeconds = 3, checkIntervalMilliseconds = 10
6363
})
6464
}
6565

66-
export { except, only, rejectNullValues, waitFor }
66+
function kebabCase(string) {
67+
if (!string) return ''
68+
69+
// Replace all underscores with hyphens
70+
string = string.replace(/_/g, '-')
71+
72+
// Replace all multiple consecutive hyphens with a single hyphen
73+
string = string.replace(/-+/g, '-')
74+
75+
// Check if string is already all lowercase
76+
if (!/[A-Z]/.test(string)) {
77+
return string
78+
}
79+
80+
// Remove all spaces and convert to word case
81+
string = string
82+
.replace(/\s+/g, '')
83+
.replace(/_/g, '')
84+
.replace(/(?:^|\s|-)+([A-Za-z])/g, (m, p1) => p1.toUpperCase())
85+
86+
// Add delimiter before uppercase letters
87+
string = string.replace(/(.)(?=[A-Z])/g, '$1-')
88+
89+
// Convert to lowercase
90+
return string.toLowerCase()
91+
}
92+
export { except, only, rejectNullValues, waitFor, kebabCase }

vue/src/inertiauiModal.js

+23-11
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,29 @@ import ModalRoot from './ModalRoot.vue'
77
import useModal from './useModal.js'
88

99
function visitModal(url, options = {}) {
10-
return useModalStack().visit(
11-
url,
12-
options.method ?? 'get',
13-
options.data ?? {},
14-
options.headers ?? {},
15-
options.config ?? {},
16-
options.onClose,
17-
options.onAfterLeave,
18-
options.queryStringArrayFormat ?? 'brackets',
19-
options.navigate ?? getConfig('navigate'),
20-
)
10+
return useModalStack()
11+
.visit(
12+
url,
13+
options.method ?? 'get',
14+
options.data ?? {},
15+
options.headers ?? {},
16+
options.config ?? {},
17+
options.onClose,
18+
options.onAfterLeave,
19+
options.queryStringArrayFormat ?? 'brackets',
20+
options.navigate ?? getConfig('navigate'),
21+
)
22+
.then((modal) => {
23+
const listeners = options.listeners ?? {}
24+
25+
Object.keys(listeners).forEach((event) => {
26+
// e.g. refreshKey -> refresh-key
27+
const eventName = event.replace(/([A-Z])/g, '-$1').toLowerCase()
28+
modal.on(eventName, listeners[event])
29+
})
30+
31+
return modal
32+
})
2133
}
2234

2335
export { HeadlessModal, Modal, ModalLink, ModalRoot, getConfig, putConfig, resetConfig, visitModal, renderApp, useModal }

vue/src/modalStack.js

+7-11
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { computed, readonly, ref, markRaw, nextTick, h } from 'vue'
2-
import { except, only, waitFor } from './helpers'
2+
import { except, only, waitFor, kebabCase } from './helpers'
33
import { router } from '@inertiajs/vue3'
44
import { usePage } from '@inertiajs/vue3'
55
import { mergeDataIntoQueryString } from '@inertiajs/core'
@@ -143,11 +143,13 @@ class Modal {
143143
}
144144

145145
on = (event, callback) => {
146+
event = kebabCase(event)
146147
this.listeners[event] = this.listeners[event] ?? []
147148
this.listeners[event].push(callback)
148149
}
149150

150151
off = (event, callback) => {
152+
event = kebabCase(event)
151153
if (callback) {
152154
this.listeners[event] = this.listeners[event]?.filter((cb) => cb !== callback) ?? []
153155
} else {
@@ -156,7 +158,7 @@ class Modal {
156158
}
157159

158160
emit = (event, ...args) => {
159-
this.listeners[event]?.forEach((callback) => callback(...args))
161+
this.listeners[kebabCase(event)]?.forEach((callback) => callback(...args))
160162
}
161163

162164
registerEventListenersFromAttrs = ($attrs) => {
@@ -165,15 +167,9 @@ class Modal {
165167
Object.keys($attrs)
166168
.filter((key) => key.startsWith('on'))
167169
.forEach((key) => {
168-
// e.g. onRefreshKey -> refresh-key
169-
const snakeCaseKey = key
170-
.replace(/^on/, '')
171-
.replace(/^./, (firstLetter) => firstLetter.toLowerCase())
172-
.replace(/([A-Z])/g, '-$1')
173-
.toLowerCase()
174-
175-
this.on(snakeCaseKey, $attrs[key])
176-
unsubscribers.push(() => this.off(snakeCaseKey, $attrs[key]))
170+
const eventName = kebabCase(key).replace(/^on-/, '')
171+
this.on(eventName, $attrs[key])
172+
unsubscribers.push(() => this.off(eventName, $attrs[key]))
177173
})
178174

179175
return () => unsubscribers.forEach((unsub) => unsub())

vue/tests/helpers.test.js

+24-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, it, expect } from 'vitest'
2-
import { except, only, rejectNullValues } from '../src/helpers'
2+
import { except, only, rejectNullValues, kebabCase } from '../src/helpers'
33

44
describe('helpers', () => {
55
describe('except', () => {
@@ -97,4 +97,27 @@ describe('helpers', () => {
9797
expect(rejectNullValues(arr)).toEqual([])
9898
})
9999
})
100+
101+
describe('kebabCase', () => {
102+
it.each([
103+
// Basic camelCase/PascalCase
104+
['camelCase', 'camel-case'],
105+
['ThisIsPascalCase', 'this-is-pascal-case'],
106+
107+
// With numbers
108+
['user123Name', 'user123-name'],
109+
['FirstName1', 'first-name1'],
110+
111+
// With acronyms
112+
['parseXMLDocument', 'parse-x-m-l-document'],
113+
114+
// Mixed cases and special chars
115+
['snake_case_value', 'snake-case-value'],
116+
['already-kebab-case', 'already-kebab-case'],
117+
['UPPERCASE', 'u-p-p-e-r-c-a-s-e'],
118+
['multiple__underscores', 'multiple-underscores'],
119+
])('should convert %s to %s', (input, expected) => {
120+
expect(kebabCase(input)).toBe(expected)
121+
})
122+
})
100123
})

0 commit comments

Comments
 (0)