Skip to content

Commit

Permalink
feat: add x-jsx x-tpl components
Browse files Browse the repository at this point in the history
  • Loading branch information
2214962083 committed Apr 21, 2022
1 parent 6394ee2 commit ce906b5
Show file tree
Hide file tree
Showing 10 changed files with 217 additions and 16 deletions.
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -211,5 +211,6 @@
"activityBar.background": "#580744",
"titleBar.activeBackground": "#7B0A5F",
"titleBar.activeForeground": "#FFFBFE"
}
},
"commentTranslate.source": "Google"
}
2 changes: 2 additions & 0 deletions packages/vue-xrender/src/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export {default as XJsx} from './x-jsx'
export {default as XTpl} from './x-tpl'
50 changes: 50 additions & 0 deletions packages/vue-xrender/src/components/x-jsx.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import {FunctionComponent, JsxFn, JsxNode} from '@/utils/types-helper'
import {defineComponent, VNode, h, PropType} from 'vue-demi'

const valueIsFunctionComponent = (value: any): value is FunctionComponent => {
return typeof value === 'object' && value.functional && typeof value.render === 'function'
}

/**
* jsx render component
*
* @example
* ```jsx
* <x-jsx :jsx="yourJsx">
* i am child text
* </x-jsx>
*
* const yourJsx = <div>hello</div>
* const yourJsx = (props) => <div>hello, {props.children}</div>
* ```
*/
const vm = defineComponent({
name: 'XJsx',
props: {
jsx: {
type: [Function, Object, Array, String, Number, Boolean] as PropType<JsxNode | JsxFn | FunctionComponent>
}
},
render(): VNode {
const {$slots, $attrs, $props} = this as InstanceType<typeof vm>
const {jsx} = $props

const children = typeof $slots.default === 'function' ? $slots.default() : $slots.default
const props = {...$attrs, children}

let result: VNode

if (valueIsFunctionComponent(jsx)) {
result = jsx.render(h, props)
} else if (typeof jsx === 'function') {
result = jsx(props, h) as VNode
} else {
result = jsx as VNode
}

return result
}
})

export default vm
77 changes: 77 additions & 0 deletions packages/vue-xrender/src/components/x-tpl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import {Vue2CompileResult} from '@/utils/types-helper'
import {Vue, defineComponent, VNode, h, PropType, isVue2} from 'vue-demi'

// Use weak map to store the mapping relationship between ctx and {[template]: renderFunction}
// This memory will be automatically reclaimed when other references to ctx are destroyed (related to the browser memory reclamation mechanism)
const compileMap = new WeakMap<Object, Record<string, Function>>()

/**
* compile a template to render function
* @param template template string
* @param ctx context
* @returns render function
*/
const compile = (template: string, ctx: Object): Function => {
const historyResults = compileMap.get(ctx)
if (historyResults && historyResults[template]) return historyResults[template].bind(ctx)

let renderFunc: Function
if (isVue2) {
const compileResult = Vue.compile(template) as unknown as Vue2CompileResult
renderFunc = compileResult.staticRenderFns?.[0] ?? compileResult.render
} else {
renderFunc = Vue.compile(template)
}

const results = {
...historyResults,
[template]: renderFunc
}
compileMap.set(ctx, results)
return renderFunc.bind(ctx)
}

/**
* template render component
*
* @example
* ```jsx
* <x-tpl :tpl="yourTemplate" :ctx="this" />
*
* export default {
* data() {
* return {
* name: 'xiao ming',
* yourTemplate: '<div>hello, {{name}}</div>'
* }
* }
* ```
*/
const vm = defineComponent({
name: 'XTpl',
props: {
tpl: {
type: String
},
ctx: {
type: Object as PropType<Record<string, any>>
}
},
render() {
const {$props, $parent} = this as InstanceType<typeof vm>
const {tpl, ctx} = $props
const _ctx = ctx || $parent || {}

// console.log('render', isVue2, tpl, _ctx)

if (!tpl) return null

const renderFunc = compile(tpl, _ctx)
const result = isVue2 ? renderFunc(h) : renderFunc(ctx)

return result as VNode
}
})

export default vm
19 changes: 9 additions & 10 deletions packages/vue-xrender/src/hooks/useJsx.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,31 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import {CreateElement, JsxNode, VueComponentConstructor} from '@/utils/types-helper'
import {JsxDefaultProps, JsxFn, JsxNode, VueComponentConstructor} from '@/utils/types-helper'
import {defineComponent, getCurrentInstance, VNode, h} from 'vue-demi'

export type JsxProps<T extends JsxNode = JsxNode> = Record<string, any> & {
children: T
}

export type JsxFn = (props: JsxProps, h?: CreateElement) => JsxNode

/**
* create a vue component from jsx
* @param name component name in template
* @param jsx jsx node, if you want to reactive the component, you can use `() => jsx`
*/
export function useJsx(name: string, jsx: JsxNode | JsxFn): VueComponentConstructor
export function useJsx<P extends JsxDefaultProps = JsxDefaultProps, T extends JsxNode = JsxNode>(
name: string,
jsx: JsxNode | JsxFn<P, T>
): VueComponentConstructor

/**
* create a vue component from jsx
* @param jsx jsx node, if you want to reactive the component, you can use `() => jsx`
*/
export function useJsx(jsx: JsxNode | JsxFn): VueComponentConstructor
export function useJsx<P extends JsxDefaultProps = JsxDefaultProps, T extends JsxNode = JsxNode>(
jsx: JsxNode | JsxFn<P, T>
): VueComponentConstructor

export function useJsx(...args: any[]) {
const name: string | undefined = args[1] ? args[0] : undefined
const jsx: JsxNode | JsxFn = args[1] ? args[1] : args[0]

const instance = getCurrentInstance()?.proxy
if (!instance) throw new Error(`useJsx error: instance not foundname: ${name}`)
if (!instance) throw new Error(`useJsx error: instance not found, name: ${name}`)

const jsxCom = defineComponent({
name,
Expand Down
2 changes: 2 additions & 0 deletions packages/vue-xrender/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export * from './hooks'
export * from './components'
export * from './utils/types-helper'
22 changes: 22 additions & 0 deletions packages/vue-xrender/src/utils/types-helper.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import {defineComponent, h, VNode} from 'vue-demi'

export type DefineComponent = ReturnType<typeof defineComponent>
Expand All @@ -10,3 +11,24 @@ export type CreateElement = typeof h
export type JsxNode = JSX.Element | VNodeChild

export type VueComponentConstructor = DefineComponent

export type JsxDefaultProps = Record<string, any>

export type JsxProps<P extends JsxDefaultProps = JsxDefaultProps, T extends JsxNode = JsxNode> = P & {
children: T
}

export type JsxFn<P extends JsxDefaultProps = JsxDefaultProps, T extends JsxNode = JsxNode> = (
props: JsxProps<P, T>,
h?: CreateElement
) => JsxNode

export type FunctionComponent = {
functional: true
render: (h: CreateElement, context: any) => VNode
}

export type Vue2CompileResult = {
render(h: CreateElement): VNode
staticRenderFns: (() => VNode)[]
}
28 changes: 26 additions & 2 deletions playgrounds/vue2/src/App.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="tsx" setup>
import {useJsx} from 'vue-xrender'
import {JsxFn, useJsx, XJsx, XTpl} from 'vue-xrender'
import {ref} from '@vue/composition-api'
const time = ref(0)
Expand All @@ -23,13 +23,34 @@ const Title = useJsx(props => (
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const Card2 = useJsx('Card', props => (
<div>
<h1>Card component, create in setup jsx, use in template</h1>
<h1>Card component, create in setup useJsx, use in template</h1>
<Title>
<SubTitle></SubTitle>
</Title>
{props.children}
</div>
))
const CardJsx: JsxFn = props => (
<div>
<h1>CardJsx component, create in setup jsx function, use in template</h1>
<Title>
<SubTitle></SubTitle>
</Title>
{props.children}
</div>
)
const CardTpl = `
<div>
<h1>CardTpl component, create in setup template string, use in template</h1>
<h2>CardTpl real Time: {{time}}s</h2>
</div>
`
defineExpose({
time
})
</script>

<template>
Expand All @@ -40,5 +61,8 @@ const Card2 = useJsx('Card', props => (
<card>
<div>Card component's children from template</div>
</card>

<x-jsx :jsx="CardJsx"> CardJsx component's children </x-jsx>
<x-tpl :tpl="CardTpl" />
</div>
</template>
28 changes: 26 additions & 2 deletions playgrounds/vue3/src/App.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="tsx" setup>
import {useJsx} from 'vue-xrender'
import {JsxFn, useJsx, XJsx, XTpl} from 'vue-xrender'
import {ref} from 'vue'
const time = ref(0)
Expand All @@ -23,13 +23,34 @@ const Title = useJsx(props => (
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const Card2 = useJsx('Card', props => (
<div>
<h1>Card component, create in setup jsx, use in template</h1>
<h1>Card component, create in setup useJsx, use in template</h1>
<Title>
<SubTitle></SubTitle>
</Title>
{props.children}
</div>
))
const CardJsx: JsxFn = props => (
<div>
<h1>CardJsx component, create in setup jsx function, use in template</h1>
<Title>
<SubTitle></SubTitle>
</Title>
{props.children}
</div>
)
const CardTpl = `
<div>
<h1>CardTpl component, create in setup template string, use in template</h1>
<h2>CardTpl real Time: {{time}}s</h2>
</div>
`
defineExpose({
time
})
</script>

<template>
Expand All @@ -40,5 +61,8 @@ const Card2 = useJsx('Card', props => (
<card>
<div>Card component's children from template</div>
</card>

<x-jsx :jsx="CardJsx"> CardJsx component's children </x-jsx>
<x-tpl :tpl="CardTpl" />
</div>
</template>
2 changes: 1 addition & 1 deletion playgrounds/vue3/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export default defineConfig({
resolve: {
dedupe: ['vue', 'vue-demi', '@vue/runtime-core', '@vue/runtime-dom'], // use the same version
alias: {
vue: pathResolve('./node_modules/vue/dist/vue.runtime.esm-browser.js') // use the same version, also use runtime template compiler
vue: pathResolve('./node_modules/vue/dist/vue.esm-browser.js') // use the same version, also use runtime template compiler
}
}
})

0 comments on commit ce906b5

Please sign in to comment.