diff --git a/docs/components/content/examples/MotionComponent.vue b/docs/components/content/examples/MotionComponent.vue new file mode 100644 index 00000000..4f3539b4 --- /dev/null +++ b/docs/components/content/examples/MotionComponent.vue @@ -0,0 +1,40 @@ + + + + + diff --git a/docs/components/content/examples/MotionGroupComponent.vue b/docs/components/content/examples/MotionGroupComponent.vue new file mode 100644 index 00000000..ed11f5fc --- /dev/null +++ b/docs/components/content/examples/MotionGroupComponent.vue @@ -0,0 +1,58 @@ + + + + + diff --git a/docs/content/2.features/7.components.md b/docs/content/2.features/7.components.md new file mode 100644 index 00000000..b3d5e6da --- /dev/null +++ b/docs/content/2.features/7.components.md @@ -0,0 +1,98 @@ +# Components + +vueuse/motion allows you to implement your animations directly within the template of your components without the need to wrap target elements in any additional components. + +These components work similar to the directive `v-motion` usage but work better in projects using server-side rendering. + +## `` + +Example of a `` component using a motion preset on a `

` element: + +```vue + +``` + + + +## `` + +The `` can be used to apply motion configuration to all of its child elements, this component is renderless by default and can be used as a wrapper by passing a value to the `:is` prop. + +```vue + +``` + + + + +## Props + +The `` and `` components allow you to define animation properties (variants) as props. + +- **`is`**: What element should rendered (`div` by default for ``). +- **`preset`**: Motion preset to use (expects camel-case string), see [Presets](/features/presets). + +### Variant props + +- **`initial`**: Properties the element will have before it is mounted. +- **`enter`**: Properties the element will have after it is mounted. +- **`visible`**: Properties the element will have whenever it is within view. Once out of view, the `initial` properties are reapplied. +- **`visible-once`**: Properties the element will have once it enters the view. +- **`hovered`**: Properties the element will have when hovered. +- **`focused`**: Properties the element will have when it receives focus. +- **`tapped`**: Properties the element will have upon being clicked or tapped. + +Variants can be passed as an object using the `:variants` prop. + +The `:variants` prop combines with other variant properties, allowing for the definition of custom variants from this object. + +Additional variant properties can be explored on the [Variants](/features/variants) page. + +### Shorthand Props + +We support shorthand props for quickly setting transition properties: + +- **`delay`** +- **`duration`** + +These properties apply to `visible`, `visible-once`, or `enter` variants if specified; otherwise, they default to the `initial` variant. + +```vue + +``` + +### Group props + +These props are specific to the `` component: + +- **`config-fn`** + - Type: `(index: number) => MotionComponentConfig` + A function that takes an `` child element index and returns a `MotionComponentConfig`, the returned value will be applied and merged to each element individually. diff --git a/docs/nuxt.config.ts b/docs/nuxt.config.ts index 420f12fe..6acd32d0 100644 --- a/docs/nuxt.config.ts +++ b/docs/nuxt.config.ts @@ -7,6 +7,9 @@ export default defineNuxtConfig({ '@vueuse/motion/nuxt': resolve(__dirname, '../src/nuxt/module.ts'), }, modules: ['@vueuse/motion/nuxt'], + features: { + devLogs: false, + }, typescript: { includeWorkspace: true, }, diff --git a/src/components/Motion.ts b/src/components/Motion.ts index 487e0e99..46bcd594 100644 --- a/src/components/Motion.ts +++ b/src/components/Motion.ts @@ -6,6 +6,7 @@ import { variantToStyle } from '../utils/transform' import { MotionComponentProps, setupMotionComponent } from '../utils/component' export default defineComponent({ + name: 'Motion', props: { ...MotionComponentProps, is: { diff --git a/src/components/MotionGroup.ts b/src/components/MotionGroup.ts index 97930391..bdc30de3 100644 --- a/src/components/MotionGroup.ts +++ b/src/components/MotionGroup.ts @@ -3,15 +3,17 @@ import type { Component } from '@nuxt/schema' import { defineComponent, h, useSlots } from 'vue' import { variantToStyle } from '../utils/transform' -import { MotionComponentProps, setupMotionComponent } from '../utils/component' +import { MotionComponentProps, MotionGroupComponentProps, setupMotionComponent } from '../utils/component' export default defineComponent({ + name: 'MotionGroup', props: { ...MotionComponentProps, is: { type: [String, Object] as PropType, required: false, }, + ...MotionGroupComponentProps, }, setup(props) { const slots = useSlots() diff --git a/src/nuxt/module.ts b/src/nuxt/module.ts index aff51804..ea86fa67 100644 --- a/src/nuxt/module.ts +++ b/src/nuxt/module.ts @@ -1,5 +1,5 @@ import { defu } from 'defu' -import { addImportsDir, addPlugin, createResolver, defineNuxtModule } from '@nuxt/kit' +import { addComponent, addImportsDir, addPlugin, createResolver, defineNuxtModule } from '@nuxt/kit' import type { NuxtModule } from '@nuxt/schema' import type { ModuleOptions as MotionModuleOpts } from '../types' @@ -20,6 +20,18 @@ export default defineNuxtModule({ // Add templates (options and directives) addPlugin(resolve('./runtime/templates/motion')) + addComponent({ + name: 'Motion', + export: 'MotionComponent', + filePath: resolve('./runtime/components'), + }) + + addComponent({ + name: 'MotionGroup', + export: 'MotionGroupComponent', + filePath: resolve('./runtime/components'), + }) + // Add auto imports addImportsDir(resolve('./runtime/composables')) diff --git a/src/nuxt/runtime/components/index.ts b/src/nuxt/runtime/components/index.ts new file mode 100644 index 00000000..31473667 --- /dev/null +++ b/src/nuxt/runtime/components/index.ts @@ -0,0 +1,2 @@ +export { default as MotionComponent } from '../../../components/Motion' +export { default as MotionGroupComponent } from '../../../components/MotionGroup' diff --git a/src/utils/component.ts b/src/utils/component.ts index aac0324d..0ada58a3 100644 --- a/src/utils/component.ts +++ b/src/utils/component.ts @@ -78,10 +78,28 @@ export const MotionComponentProps = { }, } +/** + * Partial `` config props + */ +export type MotionComponentConfig = Partial>> + +/** + * Component props specific to + */ +export const MotionGroupComponentProps = { + configFn: { + type: Function as PropType<(index: number) => MotionComponentConfig>, + required: false, + }, +} + /** * Shared logic for and */ -export function setupMotionComponent(props: LooseRequired>) { +export function setupMotionComponent( + // prettier-ignore + props: LooseRequired>, +) { // Motion instance map const instances = reactive<{ [key: number]: MotionInstance> }>({}) @@ -100,16 +118,12 @@ export function setupMotionComponent(props: LooseRequired { - const config = defu({}, propsConfig.value, preset.value, props.variants || {}) - + function applyTransitionHelpers(config: typeof propsConfig.value, values: Partial>) { for (const transitionKey of ['delay', 'duration'] as const) { - if (!props[transitionKey]) continue + if (!values[transitionKey]) continue - const transitionValueParsed = Number.parseInt(props[transitionKey] as string) + const transitionValueParsed = Number.parseInt(values[transitionKey] as string) - // TODO: extract to utility function // Apply transition property to existing variants where applicable for (const variantKey of ['enter', 'visible', 'visibleOnce'] as const) { const variantConfig = config[variantKey] @@ -123,6 +137,13 @@ export function setupMotionComponent(props: LooseRequired { + const config = defu({}, { ...propsConfig.value }, preset.value, props.variants || {}) + + return applyTransitionHelpers({ ...config }, props) }) // Replay animations on component update Vue @@ -156,7 +177,13 @@ export function setupMotionComponent(props: LooseRequired { - instances[index] = useMotion>(el as any, motionConfig.value) + if (props.configFn) { + const derivedConfig = defu({}, structuredClone({ ...motionConfig.value }), props.configFn?.(index) ?? {}) + applyTransitionHelpers(derivedConfig, props.configFn(index)) + instances[index] = useMotion>(el as any, derivedConfig) + } else { + instances[index] = useMotion>(el as any, motionConfig.value) + } } return node