Skip to content

Commit

Permalink
Listen for events using visitModal() (#44)
Browse files Browse the repository at this point in the history
See #17
  • Loading branch information
pascalbaljet authored Oct 31, 2024
1 parent b7d089c commit ca5ba6b
Show file tree
Hide file tree
Showing 11 changed files with 153 additions and 41 deletions.
13 changes: 12 additions & 1 deletion demo-app/resources/js/Pages/Visit.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ export default function Visit() {
visitModal('#local');
};

const visitEdit = () => {
visitModal('/users/1/edit', {
navigate: true,
listeners: {
userGreets: function (greeting) {
alert(greeting);
}
}
})
}

return (
<>
<Container>
Expand All @@ -19,7 +30,7 @@ export default function Visit() {
<button onClick={() => visitModal('/data', { method: 'post', data: { message: 'Hi again!' } })} type="button">
Open Route Modal
</button>
<button onClick={() => visitModal('/users/1/edit', { navigate: true })} type="button">
<button onClick={visitEdit} type="button">
Open Route Modal With Navigate
</button>
</div>
Expand Down
13 changes: 12 additions & 1 deletion demo-app/resources/js/Pages/Visit.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
<script setup>
import Container from './Container.vue'
import { Modal, visitModal } from '@inertiaui/modal-vue'
function visitEdit() {
visitModal('/users/1/edit', {
navigate: true,
listeners: {
userGreets(greeting) {
alert(greeting);
}
}
})
}
</script>

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

<button @click="visitModal('/users/1/edit', { navigate: true })" type="button">
<button @click="visitEdit" type="button">
Open Route Modal With Navigate
</button>
</div>
Expand Down
15 changes: 15 additions & 0 deletions demo-app/tests/Browser/EmitTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,21 @@ public function it_can_dispatch_an_event_from_the_modal_to_the_modal_link(bool $
});
}

#[DataProvider('booleanProvider')]
#[Test]
public function it_can_dispatch_an_event_from_the_modal_to_the_visit_modal_method(bool $navigate)
{
$this->browse(function (Browser $browser) use ($navigate) {
$browser->visit('/visit'.($navigate ? '?navigate=1' : ''))
->waitForText('Visit programmatically')
->press('Open Route Modal With Navigate')
->waitFor('.im-modal-content')
->clickLink('Send Message', 'button')
->assertDialogOpened('Hello from EditUser')
->dismissDialog();
});
}

#[DataProvider('booleanProvider')]
#[Test]
public function it_can_dispatch_events_back_and_forth_between_nested_modals(bool $navigate)
Expand Down
1 change: 1 addition & 0 deletions docs/basic-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@ visitModal('/users/create', {
config: {
slideover: true,
}
listeners: {},
onClose: () => console.log('Modal closed'),
onAfterLeave: () => console.log('Modal removed from DOM'),
queryStringArrayFormat: 'brackets',
Expand Down
14 changes: 12 additions & 2 deletions docs/event-bus.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export default function MyPage() {

:::

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

::: code-group

Expand All @@ -98,7 +98,17 @@ export default function MyPage() {
}
```

:::
If you're [programmatically opening the modal](/basic-usage.html#programmatic-usage), you add listeners using the `listeners` option:

```js
visitModal('/users/create', {
listeners: {
increaseBy(amount) {
console.log(`Increase by ${amount}`);
}
}
})
```

## Nested / Stacked Modals

Expand Down
29 changes: 18 additions & 11 deletions react/src/ModalRoot.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createElement, useEffect, useState, useRef } from 'react'
import { default as Axios } from 'axios'
import { except, only } from './helpers'
import { except, only, kebabCase } from './helpers'
import { router, usePage } from '@inertiajs/react'
import { mergeDataIntoQueryString } from '@inertiajs/core'
import { createContext, useContext } from 'react'
Expand Down Expand Up @@ -164,11 +164,13 @@ export const ModalStackProvider = ({ children }) => {
}

on = (event, callback) => {
event = kebabCase(event)
this.listeners[event] = this.listeners[event] ?? []
this.listeners[event].push(callback)
}

off = (event, callback) => {
event = kebabCase(event)
if (callback) {
this.listeners[event] = this.listeners[event]?.filter((cb) => cb !== callback) ?? []
} else {
Expand All @@ -177,7 +179,7 @@ export const ModalStackProvider = ({ children }) => {
}

emit = (event, ...args) => {
this.listeners[event]?.forEach((callback) => callback(...args))
this.listeners[kebabCase(event)]?.forEach((callback) => callback(...args))
}

registerEventListenersFromProps = (props) => {
Expand All @@ -187,14 +189,9 @@ export const ModalStackProvider = ({ children }) => {
.filter((key) => key.startsWith('on'))
.forEach((key) => {
// e.g. onRefreshKey -> refresh-key
const snakeCaseKey = key
.replace(/^on/, '')
.replace(/^./, (firstLetter) => firstLetter.toLowerCase())
.replace(/([A-Z])/g, '-$1')
.toLowerCase()

this.on(snakeCaseKey, props[key])
unsubscribers.push(() => this.off(snakeCaseKey, props[key]))
const eventName = kebabCase(key).replace(/^on-/, '')
this.on(eventName, props[key])
unsubscribers.push(() => this.off(eventName, props[key]))
})

return () => unsubscribers.forEach((unsub) => unsub())
Expand Down Expand Up @@ -274,7 +271,17 @@ export const ModalStackProvider = ({ children }) => {
options.onAfterLeave,
options.queryStringArrayFormat ?? 'brackets',
options.navigate ?? getConfig('navigate'),
)
).then((modal) => {
const listeners = options.listeners ?? {}

Object.keys(listeners).forEach((event) => {
// e.g. refreshKey -> refresh-key
const eventName = kebabCase(event)
modal.on(eventName, listeners[event])
})

return modal
})

const visit = (
href,
Expand Down
4 changes: 2 additions & 2 deletions react/src/helpers.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
import { except, only, rejectNullValues, waitFor } from './../../vue/src/helpers.js'
export { except, only, rejectNullValues, waitFor }
import { except, only, rejectNullValues, waitFor, kebabCase } from './../../vue/src/helpers.js'
export { except, only, rejectNullValues, waitFor, kebabCase }
28 changes: 27 additions & 1 deletion vue/src/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,30 @@ function waitFor(conditionFn, waitForSeconds = 3, checkIntervalMilliseconds = 10
})
}

export { except, only, rejectNullValues, waitFor }
function kebabCase(string) {
if (!string) return ''

// Replace all underscores with hyphens
string = string.replace(/_/g, '-')

// Replace all multiple consecutive hyphens with a single hyphen
string = string.replace(/-+/g, '-')

// Check if string is already all lowercase
if (!/[A-Z]/.test(string)) {
return string
}

// Remove all spaces and convert to word case
string = string
.replace(/\s+/g, '')
.replace(/_/g, '')
.replace(/(?:^|\s|-)+([A-Za-z])/g, (m, p1) => p1.toUpperCase())

// Add delimiter before uppercase letters
string = string.replace(/(.)(?=[A-Z])/g, '$1-')

// Convert to lowercase
return string.toLowerCase()
}
export { except, only, rejectNullValues, waitFor, kebabCase }
34 changes: 23 additions & 11 deletions vue/src/inertiauiModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,29 @@ import ModalRoot from './ModalRoot.vue'
import useModal from './useModal.js'

function visitModal(url, options = {}) {
return useModalStack().visit(
url,
options.method ?? 'get',
options.data ?? {},
options.headers ?? {},
options.config ?? {},
options.onClose,
options.onAfterLeave,
options.queryStringArrayFormat ?? 'brackets',
options.navigate ?? getConfig('navigate'),
)
return useModalStack()
.visit(
url,
options.method ?? 'get',
options.data ?? {},
options.headers ?? {},
options.config ?? {},
options.onClose,
options.onAfterLeave,
options.queryStringArrayFormat ?? 'brackets',
options.navigate ?? getConfig('navigate'),
)
.then((modal) => {
const listeners = options.listeners ?? {}

Object.keys(listeners).forEach((event) => {
// e.g. refreshKey -> refresh-key
const eventName = event.replace(/([A-Z])/g, '-$1').toLowerCase()
modal.on(eventName, listeners[event])
})

return modal
})
}

export { HeadlessModal, Modal, ModalLink, ModalRoot, getConfig, putConfig, resetConfig, visitModal, renderApp, useModal }
18 changes: 7 additions & 11 deletions vue/src/modalStack.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { computed, readonly, ref, markRaw, nextTick, h } from 'vue'
import { except, only, waitFor } from './helpers'
import { except, only, waitFor, kebabCase } from './helpers'
import { router } from '@inertiajs/vue3'
import { usePage } from '@inertiajs/vue3'
import { mergeDataIntoQueryString } from '@inertiajs/core'
Expand Down Expand Up @@ -143,11 +143,13 @@ class Modal {
}

on = (event, callback) => {
event = kebabCase(event)
this.listeners[event] = this.listeners[event] ?? []
this.listeners[event].push(callback)
}

off = (event, callback) => {
event = kebabCase(event)
if (callback) {
this.listeners[event] = this.listeners[event]?.filter((cb) => cb !== callback) ?? []
} else {
Expand All @@ -156,7 +158,7 @@ class Modal {
}

emit = (event, ...args) => {
this.listeners[event]?.forEach((callback) => callback(...args))
this.listeners[kebabCase(event)]?.forEach((callback) => callback(...args))
}

registerEventListenersFromAttrs = ($attrs) => {
Expand All @@ -165,15 +167,9 @@ class Modal {
Object.keys($attrs)
.filter((key) => key.startsWith('on'))
.forEach((key) => {
// e.g. onRefreshKey -> refresh-key
const snakeCaseKey = key
.replace(/^on/, '')
.replace(/^./, (firstLetter) => firstLetter.toLowerCase())
.replace(/([A-Z])/g, '-$1')
.toLowerCase()

this.on(snakeCaseKey, $attrs[key])
unsubscribers.push(() => this.off(snakeCaseKey, $attrs[key]))
const eventName = kebabCase(key).replace(/^on-/, '')
this.on(eventName, $attrs[key])
unsubscribers.push(() => this.off(eventName, $attrs[key]))
})

return () => unsubscribers.forEach((unsub) => unsub())
Expand Down
25 changes: 24 additions & 1 deletion vue/tests/helpers.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest'
import { except, only, rejectNullValues } from '../src/helpers'
import { except, only, rejectNullValues, kebabCase } from '../src/helpers'

describe('helpers', () => {
describe('except', () => {
Expand Down Expand Up @@ -97,4 +97,27 @@ describe('helpers', () => {
expect(rejectNullValues(arr)).toEqual([])
})
})

describe('kebabCase', () => {
it.each([
// Basic camelCase/PascalCase
['camelCase', 'camel-case'],
['ThisIsPascalCase', 'this-is-pascal-case'],

// With numbers
['user123Name', 'user123-name'],
['FirstName1', 'first-name1'],

// With acronyms
['parseXMLDocument', 'parse-x-m-l-document'],

// Mixed cases and special chars
['snake_case_value', 'snake-case-value'],
['already-kebab-case', 'already-kebab-case'],
['UPPERCASE', 'u-p-p-e-r-c-a-s-e'],
['multiple__underscores', 'multiple-underscores'],
])('should convert %s to %s', (input, expected) => {
expect(kebabCase(input)).toBe(expected)
})
})
})

0 comments on commit ca5ba6b

Please sign in to comment.