Skip to content

Commit

Permalink
feat(collection): new component collection (#262)
Browse files Browse the repository at this point in the history
* feat: collection

* fix: lint

* refactor: collection

* fix: dom

* fix: package version

* fix: slot
  • Loading branch information
productdevbook authored Aug 3, 2023
1 parent 472a50c commit 90c6115
Show file tree
Hide file tree
Showing 18 changed files with 433 additions and 9 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"@oku-ui/avatar": "workspace:^",
"@oku-ui/checkbox": "workspace:^",
"@oku-ui/collapsible": "workspace:^",
"@oku-ui/collection": "workspace:^",
"@oku-ui/label": "workspace:^",
"@oku-ui/popper": "workspace:^",
"@oku-ui/presence": "workspace:^",
Expand Down
13 changes: 13 additions & 0 deletions packages/components/collection/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# `collection`

<span><a href="https://www.npmjs.com/package/@oku-ui/collection "><img src="https://img.shields.io/npm/v/@oku-ui/collection?style=flat&colorA=18181B&colorB=28CF8D" alt="Version"></a> </span> | <span> <a href="https://www.npmjs.com/package/@oku-ui/collection"> <img src="https://img.shields.io/npm/dm/@oku-ui/collection?style=flat&colorA=18181B&colorB=28CF8D" alt="Downloads"> </a> </span> | <span> <a href="https://oku-ui.com/primitives/components/collection"><img src="https://img.shields.io/badge/Open%20Documentation-18181B" alt="Website"></a> </span>

## Installation

```sh
$ pnpm add @oku-ui/collection
```

## Usage

soon docs
12 changes: 12 additions & 0 deletions packages/components/collection/build.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { defineBuildConfig } from 'unbuild'

export default defineBuildConfig({
entries: [
{
builder: 'mkdist',
input: './src/',
pattern: ['**/!(*.test|*.stories).ts'],
},
],
declaration: true,
})
51 changes: 51 additions & 0 deletions packages/components/collection/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"name": "@oku-ui/collection",
"type": "module",
"version": "0.2.3",
"license": "MIT",
"source": "src/index.ts",
"funding": "https://github.com/sponsors/productdevbook",
"homepage": "https://oku-ui.com/primitives",
"repository": {
"type": "git",
"url": "git+https://github.com/oku-ui/primitives.git",
"directory": "packages/components/collection"
},
"bugs": {
"url": "https://github.com/oku-ui/primitives/issues"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs"
}
},
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"files": [
"dist"
],
"engines": {
"node": ">=18"
},
"scripts": {
"build": "tsup",
"dev": "tsup --watch"
},
"peerDependencies": {
"vue": "^3.3.0"
},
"dependencies": {
"@oku-ui/primitive": "latest",
"@oku-ui/provide": "latest",
"@oku-ui/slot": "latest",
"@oku-ui/use-composable": "latest",
"@oku-ui/utils": "latest"
},
"devDependencies": {
"tsconfig": "workspace:^"
},
"publishConfig": {
"access": "public"
}
}
142 changes: 142 additions & 0 deletions packages/components/collection/src/collection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import type { FunctionalComponent, Ref, ReservedProps } from 'vue'
import { computed, defineComponent, h, ref, watchEffect } from 'vue'
import { useComposedRefs, useForwardRef } from '@oku-ui/use-composable'
import { createProvideScope } from '@oku-ui/provide'
import { OkuSlot } from '@oku-ui/slot'

const CollectionProps = {
scope: { type: null as any, required: true },
}

type CollectionElement = HTMLElement

// We have resorted to returning slots directly rather than exposing primitives that can then
// be slotted like `<CollectionItem as={Slot}>…</CollectionItem>`.
// This is because we encountered issues with generic types that cannot be statically analysed
// due to creating them dynamically via createCollection.

function createCollection<ItemElement extends HTMLElement, ItemData>(name: string) {
// const ItemData = {
// scope: { type: Object, required: true },
// }
/* -----------------------------------------------------------------------------------------------
* CollectionProvider
* --------------------------------------------------------------------------------------------- */

const PROVIDER_NAME = `${name}CollectionProvider`
const [createCollectionProvide, createCollectionScope] = createProvideScope(PROVIDER_NAME)

type ContextValue = {
collectionRef: Ref<CollectionElement | undefined>
itemMap: Map<Ref<ItemElement | null | undefined>, { ref: Ref<ItemElement> } & ItemData>
}

const [CollectionProviderImpl, useCollectionInject] = createCollectionProvide<ContextValue>(
PROVIDER_NAME,
{ collectionRef: ref(undefined), itemMap: new Map() },
)

const CollectionProvider = defineComponent({
name: PROVIDER_NAME,
inheritAttrs: false,
props: {
...CollectionProps,
},
setup(props, { slots }) {
const collectionRef = ref<CollectionElement>()
const itemMap = new Map<Ref<ItemElement>, { ref: Ref<ItemElement> } & ItemData>()
CollectionProviderImpl({
collectionRef,
itemMap,
scope: props.scope,
})

return () => slots.default?.()
},
})
/* -----------------------------------------------------------------------------------------------
* CollectionSlot
* --------------------------------------------------------------------------------------------- */

const COLLECTION_SLOT_NAME = `${name}CollectionSlot`

const CollectionSlot = defineComponent({
name: COLLECTION_SLOT_NAME,
inheritAttrs: false,
props: {
...CollectionProps,
},
setup(props, { slots }) {
const inject = useCollectionInject(COLLECTION_SLOT_NAME, props.scope)
const forwaredRef = useForwardRef()
const composedRefs = useComposedRefs(forwaredRef, inject.value.collectionRef)
return () => h(OkuSlot, { ref: composedRefs }, {
default: () => slots.default?.(),
})
},
})

/* -----------------------------------------------------------------------------------------------
* CollectionItem
* --------------------------------------------------------------------------------------------- */

const ITEM_SLOT_NAME = `${name}CollectionItemSlot`
const ITEM_DATA_ATTR = 'data-oku-collection-item'

type CollectionItemSlotProps = ItemData & {
scope: any | undefined
} & ReservedProps

const CollectionItemSlot: FunctionalComponent<CollectionItemSlotProps> = (props, context) => {
const { scope, ...itemData } = props
const refValue = ref<ItemElement | null>()
const forwaredRef = useForwardRef()
const composedRefs = useComposedRefs(refValue, forwaredRef)

const inject = useCollectionInject(ITEM_SLOT_NAME, scope)

watchEffect((clearMap) => {
inject.value.itemMap.set(refValue, { ref: refValue, ...(itemData as any) })
clearMap(() => inject.value.itemMap.delete(refValue))
})

return h(OkuSlot, { ref: composedRefs, ...{ [ITEM_DATA_ATTR]: '' } }, {
default: () => context.slots.default?.(),
})
}

/* -----------------------------------------------------------------------------------------------
* useCollection
* --------------------------------------------------------------------------------------------- */

function useCollection(scope: any) {
const inject = useCollectionInject(`${name}CollectionConsumer`, scope)

const getItems = computed(() => {
const collectionNode = inject.value.collectionRef.value
if (!collectionNode)
return []

const orderedNodes = Array.from(collectionNode.querySelectorAll(`[${ITEM_DATA_ATTR}]`))

const items = Array.from(inject.value.itemMap.values())

const orderedItems = items.sort(
(a, b) => orderedNodes.indexOf(a.ref.value!) - orderedNodes.indexOf(b.ref.value!),
)
return orderedItems
})

return getItems
}

return {
CollectionProvider,
CollectionSlot,
CollectionItemSlot,
useCollection,
createCollectionScope,
}
}

export { createCollection, CollectionProps }
4 changes: 4 additions & 0 deletions packages/components/collection/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export {
createCollection,
CollectionProps,
} from './collection'
59 changes: 59 additions & 0 deletions packages/components/collection/src/stories/CollectionDemo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<!-- eslint-disable no-console -->
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { createCollection } from '@oku-ui/collection'
export interface OkuCollectionProps {
template: '#1' | '#2'
allshow?: boolean
}
withDefaults(defineProps<OkuCollectionProps>(), {
template: '#1',
})
type ItemData = { disabled?: boolean }
const { CollectionSlot, CollectionItemSlot, CollectionProvider, useCollection } = createCollection<HTMLLIElement, ItemData >('List')
const labelRef = ref<any>()
onMounted(() => {
console.log(labelRef.value, 'ref')
})
const alert = () => window.alert('clicked')
function LogsItem() {
const getItems = useCollection(undefined)
console.log(getItems.value[0].ref.value)
}
</script>

<template>
<div class="cursor-default inline-block">
<div v-if="template === '#1' || allshow" class="flex flex-col">
<CollectionProvider :scope="undefined">
<CollectionSlot :scope="undefined">
<ul clas="w-52">
<CollectionItemSlot ref="labelRef" :scope="undefined">
<li>
Red
</li>
</CollectionItemSlot>
<CollectionItemSlot :scope="undefined" :disabled="true">
<li class="opacity-50">
Green
</li>
</CollectionItemSlot>

<CollectionItemSlot :scope="undefined">
<li>
Blue
</li>
</CollectionItemSlot>
</ul>
</CollectionSlot>
<LogsItem />
</CollectionProvider>
</div>
</div>
</template>
58 changes: 58 additions & 0 deletions packages/components/collection/src/stories/collection.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import type { Meta, StoryObj } from '@storybook/vue3'

import type { OkuCollectionProps } from './CollectionDemo.vue'
import OkuCollectionComponent from './CollectionDemo.vue'

interface StoryProps extends OkuCollectionProps {
}

const meta = {
title: 'Utilities/Collection',
component: OkuCollectionComponent,
args: {
template: '#1',
},
argTypes: {
template: {
control: 'text',
},
},
tags: ['autodocs'],
} satisfies Meta<typeof OkuCollectionComponent> & {
args: StoryProps
}

export default meta
type Story = StoryObj<typeof meta> & {
args: StoryProps
}

export const Styled: Story = {
args: {
template: '#1',
},
render: (args: any) => ({
components: { OkuCollectionComponent },
setup() {
return { args }
},
template: `
<OkuCollectionComponent v-bind="args" />
`,
}),
}

export const WithControl: Story = {
args: {
template: '#2',
},
render: (args: any) => ({
components: { OkuCollectionComponent },
setup() {
return { args }
},
template: `
<OkuCollectionComponent v-bind="args" />
`,
}),
}
10 changes: 10 additions & 0 deletions packages/components/collection/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extends": "tsconfig/node16.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist"
},
"include": [
"src"
]
}
22 changes: 22 additions & 0 deletions packages/components/collection/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { defineConfig } from 'tsup'
import pkg from './package.json'

const external = [
...Object.keys(pkg.dependencies || {}),
...Object.keys(pkg.peerDependencies || {}),
]

export default defineConfig((options) => {
return [
{
...options,
entryPoints: ['src/index.ts'],
external,
dts: true,
clean: true,
target: 'node16',
format: ['esm'],
outExtension: () => ({ js: '.mjs' }),
},
]
})
Loading

0 comments on commit 90c6115

Please sign in to comment.