Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add events (fix #32) #72

Merged
merged 5 commits into from
May 1, 2022
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
2 changes: 1 addition & 1 deletion examples/vue3/cypress/integration/stories-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ describe('Stories list', () => {
cy.get('[data-test-id="story-list-item"]').contains('BaseButton')
.contains('3') // Variants count
cy.get('[data-test-id="story-list-item"]').contains('Demo')
cy.get('[data-test-id="story-list-folder"]').should('have.length', 1)
cy.get('[data-test-id="story-list-folder"]').should('have.length', 2)
})

it('should toggle folder', () => {
Expand Down
15 changes: 15 additions & 0 deletions examples/vue3/src/components/EventButton.story.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<script lang="ts" setup>
import EventButton from './EventButton.vue'
import { hstEvent } from 'histoire/client'
</script>

<template>
<Story
title="events/EventButton"
>
<EventButton @myEvent="hstEvent('My event', $event)" /><br>
<button @click="hstEvent('Click', $event)">
Click
</button>
</Story>
</template>
28 changes: 28 additions & 0 deletions examples/vue3/src/components/EventButton.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<script lang="ts" setup>
import { ref } from 'vue'

const emit = defineEmits<{
(e: 'myEvent', value: { a: string, b: string }): void
}>()

const value1 = ref('Hello')
const value2 = ref('World')

function sendEvent () {
emit('myEvent', { a: value1.value, b: value2.value })
}
</script>

<template>
<input
v-model="value1"
type="text"
>
<input
v-model="value2"
type="text"
>
<button @click="sendEvent">
Send
</button>
</template>
16 changes: 16 additions & 0 deletions examples/vue3/src/components/EventButtonGrid.story.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<script lang="ts" setup>
import EventButton from './EventButton.vue'
import { hstEvent } from 'histoire/client'
</script>

<template>
<Story
title="events/EventButtonGrid"
:layout="{ type: 'grid', width: 500 }"
>
<EventButton @myEvent="hstEvent('My event', $event)" /><br>
<button @click="hstEvent('Click', $event)">
Click
</button>
</Story>
</template>
53 changes: 53 additions & 0 deletions packages/histoire/src/client/app/components/story/StoryEvent.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<script lang="ts" setup>
import { computed } from 'vue'
import { HstEvent } from '../../stores/events'

const props = defineProps<{
event: HstEvent
}>()

const formattedArgument = computed(() => {
switch (typeof props.event.argument) {
case 'string':
return `"${props.event.argument}"`
case 'object':
return `{ ${Object.keys(props.event.argument).map((key) => `${key}: ${props.event.argument[key]}`).join(', ')} }`
default:
return props.event.argument
}
})
</script>

<template>
<VDropdown
class="htw-group"
placement="right"
>
<template #default="{ shown }">
<div
class="group-hover:htw-bg-primary-100 dark:group-hover:htw-bg-primary-700 htw-cursor-pointer htw-py-2 htw-px-4 htw-flex htw-items-baseline htw-gap-1 htw-leading-normal"
:class="[
shown ? 'htw-bg-primary-50 dark:htw-bg-primary-600' : 'group-odd:htw-bg-gray-100/50 dark:group-odd:htw-bg-gray-750/40',
]"
>
<span
:class="{
'htw-text-primary-500': shown,
}"
>
{{ event.name }}
</span>
<span
v-if="event.argument"
class="htw-text-xs htw-opacity-50 htw-truncate"
>{{ formattedArgument }}</span>
</div>
</template>

<template #popper>
<div class="htw-overflow-auto htw-max-w-[400px] htw-max-h-[400px]">
<pre class="htw-p-4">{{ event.argument }}</pre>
</div>
</template>
</VDropdown>
</template>
45 changes: 45 additions & 0 deletions packages/histoire/src/client/app/components/story/StoryEvents.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<script lang="ts" setup>
import { ref, computed, watch, onMounted, nextTick } from 'vue'
import { Icon } from '@iconify/vue'
import BaseEmpty from '../base/BaseEmpty.vue'
import { useEventsStore } from '../../stores/events'
import StoryEvent from './StoryEvent.vue'

const eventsStore = useEventsStore()

const hasEvents = computed(() => eventsStore.events.length)

onMounted(resetUnseen)
watch(() => eventsStore.unseen, resetUnseen)

async function resetUnseen () {
if (eventsStore.unseen > 0) {
eventsStore.unseen = 0
}
await nextTick()
eventsElement.value.scrollTo({ top: eventsElement.value.scrollHeight })
}

const eventsElement = ref<HTMLDivElement>()
</script>

<template>
<div ref="eventsElement">
<BaseEmpty
v-if="!hasEvents"
>
<Icon
icon="carbon:event-schedule"
class="htw-w-8 htw-h-8 htw-opacity-50 htw-mb-6"
/>
No event fired
</BaseEmpty>
<div v-else>
<StoryEvent
v-for="(event, key) of eventsStore.events"
:key="key"
:event="event"
/>
</div>
</div>
</template>
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
<script lang="ts" setup>
import { computed, isRef, onUnmounted, Ref, ref, toRaw, watch } from 'vue'
import { computed, onUnmounted, Ref, ref, toRaw, watch } from 'vue'
import { useEventListener } from '@vueuse/core'
import { Icon } from '@iconify/vue'
import { STATE_SYNC, PREVIEW_SETTINGS_SYNC, SANDBOX_READY } from '../../util/const'
import { STATE_SYNC, PREVIEW_SETTINGS_SYNC, SANDBOX_READY, EVENT_SEND } from '../../util/const'
import type { Story, Variant } from '../../types'
import HatchedPattern from '../misc/HatchedPattern.vue'
import CheckerboardPattern from '../misc/CheckerboardPattern.vue'
import { toRawDeep } from '../../util/reactivity'
import { getSandboxUrl } from '../sandbox/lib'
import { usePreviewSettingsStore } from '../../stores/preview-settings'
import { HstEvent, useEventsStore } from '../../stores/events'

const props = defineProps<{
story: Story
Expand Down Expand Up @@ -46,28 +47,48 @@ watch(() => props.variant.state, () => {
Object.assign(props.variant, {
previewReady: false,
})

useEventListener(window, 'message', (event) => {
if (event.data.type === STATE_SYNC) {
synced = true
if (props.variant.state) {
for (const key in event.data.state) {
if (typeof props.variant.state[key] === 'object') {
Object.assign(props.variant.state[key], event.data.state[key])
} else {
// eslint-disable-next-line vue/no-mutating-props
props.variant.state[key] = event.data.state[key]
}
switch (event.data.type) {
case STATE_SYNC:
updateVariantState(event.data.state)
break
case EVENT_SEND:
logEvent(event.data.event)
break
case SANDBOX_READY:
setPreviewReady()
break
}
})

function updateVariantState (state: any) {
synced = true
if (props.variant.state) {
for (const key in state) {
if (typeof props.variant.state[key] === 'object') {
Object.assign(props.variant.state[key], state[key])
} else {
// eslint-disable-next-line vue/no-mutating-props
props.variant.state[key] = state[key]
}
} else {
// eslint-disable-next-line vue/no-mutating-props
props.variant.state = event.data.state
}
} else if (event.data.type === SANDBOX_READY) {
Object.assign(props.variant, {
previewReady: true,
})
} else {
// eslint-disable-next-line vue/no-mutating-props
props.variant.state = state
}
})
}

function logEvent (event: HstEvent) {
const eventsStore = useEventsStore()
eventsStore.addEvent(event)
}

function setPreviewReady () {
Object.assign(props.variant, {
previewReady: true,
})
}

const sandboxUrl = computed(() => {
return getSandboxUrl(props.story, props.variant)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,33 @@
<script lang="ts" setup>
import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useStoryStore } from '../../stores/story'
import { useEventsStore } from '../../stores/events'

import BaseSplitPane from '../base/BaseSplitPane.vue'
import BaseEmpty from '../base/BaseEmpty.vue'
import BaseTab from '../base/BaseTab.vue'
import StoryControls from './StoryControls.vue'
import StoryDocs from './StoryDocs.vue'
import StoryEvents from './StoryEvents.vue'
import StorySourceCode from './StorySourceCode.vue'

const storyStore = useStoryStore()
const eventsStore = useEventsStore()

const router = useRouter()
const route = useRoute()

const panelContentComponent = computed(() => {
switch (route.query.tab) {
case 'docs':
return StoryDocs
case 'events':
return StoryEvents
default:
return StoryControls
}
})
</script>

<template>
Expand Down Expand Up @@ -46,12 +61,22 @@ const route = useRoute()
>
Docs
</BaseTab>
<BaseTab
:to="{ ...$route, query: { ...$route.query, tab: 'events' } }"
:matched="$route.query.tab === 'events'"
>
Events
<span
v-if="eventsStore.unseen"
class="htw-text-center htw-text-gray-900 dark:htw-text-gray-100 htw-text-xs htw-mx-1 htw-px-0.5 htw-h-4 htw-min-w-4 htw-rounded-full active htw-bg-primary-500 htw-text-white dark:htw-text-black"
>
{{ eventsStore.unseen <=99 ? eventsStore.unseen : "99+" }}
</span>
</BaseTab>
</nav>

<component
:is="$route.query.tab === 'docs'
? StoryDocs
: StoryControls"
:is="panelContentComponent"
:story="storyStore.currentStory"
:variant="storyStore.currentVariant"
class="htw-h-full htw-overflow-auto"
Expand Down
36 changes: 36 additions & 0 deletions packages/histoire/src/client/app/stores/events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { defineStore } from 'pinia'
import { ref, reactive, watch } from 'vue'
import { useStoryStore } from './story.js'

export type HstEvent = {
name: string
argument: unknown
}

export const useEventsStore = defineStore('events', () => {
const storyStore = useStoryStore()

const events = reactive<Array<HstEvent>>([])
const unseen = ref(0)

function addEvent (event: HstEvent) {
events.push(event)
unseen.value++
}

function reset () {
events.length = 0
unseen.value = 0
}

watch(() => storyStore.currentVariant, () => {
reset()
})

return {
addEvent,
reset,
events,
unseen,
}
})
1 change: 1 addition & 0 deletions packages/histoire/src/client/app/util/const.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export const STATE_SYNC = '__histoire:state-sync'
export const SANDBOX_READY = '__histoire:sandbox-ready'
export const EVENT_SEND = '__histoire:event'
export const PREVIEW_SETTINGS_SYNC = '__histoire:preview-settings-sync'
30 changes: 30 additions & 0 deletions packages/histoire/src/client/app/util/events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { useEventsStore } from '../stores/events.js'
import { EVENT_SEND } from './const'

export function hstEvent (name: string, argument) {
console.log('[histoire] Event fired', { name, argument })
const event = {
name,
argument: JSON.parse(stringifyEvent(argument)), // Needed for HTMLEvent that can't be cloned
}
if (location.href.includes('__sandbox')) {
window.parent?.postMessage({
type: EVENT_SEND,
event,
})
} else {
useEventsStore().addEvent(event)
}
}

function stringifyEvent (e) {
const obj = {}
for (const k in e) {
obj[k] = e[k]
}
return JSON.stringify(obj, (k, v) => {
if (v instanceof Node) return 'Node'
if (v instanceof Window) return 'Window'
return v
}, ' ')
}
1 change: 1 addition & 0 deletions packages/histoire/src/client/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { App } from 'vue'
import type { Story, Variant } from './app/types.js'
export { hstEvent } from './app/util/events'

export type Vue3StorySetupHandler = (payload: {
app: App
Expand Down