Skip to content

Commit d0323c5

Browse files
authored
support Custom components (#88)
1 parent acc26f0 commit d0323c5

File tree

14 files changed

+192
-69
lines changed

14 files changed

+192
-69
lines changed

.eslintrc.cjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ module.exports = {
88
'plugin:react-hooks/recommended',
99
'prettier',
1010
],
11-
ignorePatterns: ['node_modules', 'dist'],
11+
ignorePatterns: ['node_modules', 'dist', 'htmlcov'],
1212
parser: '@typescript-eslint/parser',
1313
plugins: ['react', '@typescript-eslint', 'react-refresh', 'simple-import-sort'],
1414
rules: {

.github/workflows/ci.yml

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -49,29 +49,29 @@ jobs:
4949
OS: ${{ matrix.os }}
5050

5151
steps:
52-
- uses: actions/checkout@v3
52+
- uses: actions/checkout@v3
5353

54-
- name: set up python
55-
uses: actions/setup-python@v4
56-
with:
57-
python-version: ${{ matrix.python-version }}
54+
- name: set up python
55+
uses: actions/setup-python@v4
56+
with:
57+
python-version: ${{ matrix.python-version }}
5858

59-
- run: pip install -r src/python-fastui/requirements/test.txt
60-
- run: pip install -r src/python-fastui/requirements/pyproject.txt
61-
- run: pip install src/python-fastui
59+
- run: pip install -r src/python-fastui/requirements/test.txt
60+
- run: pip install -r src/python-fastui/requirements/pyproject.txt
61+
- run: pip install src/python-fastui
6262

63-
- run: coverage run -m pytest src
63+
- run: coverage run -m pytest src
6464

65-
# test demo on 3.11 and 3.12, these tests are intentionally omitted from coverage
66-
- if: matrix.python-version == '3.11' || matrix.python-version == '3.12'
67-
run: pytest demo/tests.py
65+
# test demo on 3.11 and 3.12, these tests are intentionally omitted from coverage
66+
- if: matrix.python-version == '3.11' || matrix.python-version == '3.12'
67+
run: pytest demo/tests.py
6868

69-
- run: coverage xml
69+
- run: coverage xml
7070

71-
- uses: codecov/codecov-action@v3
72-
with:
73-
file: ./coverage.xml
74-
env_vars: PYTHON,OS
71+
- uses: codecov/codecov-action@v3
72+
with:
73+
file: ./coverage.xml
74+
env_vars: PYTHON,OS
7575

7676
npm-build:
7777
runs-on: ubuntu-latest

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
htmlcov/

demo/components_list.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,18 @@ class Delivery(BaseModel):
184184
],
185185
class_name='border-top mt-3 pt-1',
186186
),
187+
c.Div(
188+
components=[
189+
c.Heading(text='Custom', level=2),
190+
c.Markdown(text="""\
191+
Below is a custom component, in this case it implements [cowsay](https://en.wikipedia.org/wiki/Cowsay),
192+
but you might be able to do something even more useful with it.
193+
194+
The statement spoken by the famous cow is provided by the backend."""),
195+
c.Custom(data='This is a custom component', sub_type='cowsay'),
196+
],
197+
class_name='border-top mt-3 pt-1',
198+
),
187199
title='Components',
188200
)
189201

src/npm-fastui-bootstrap/src/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,5 +118,7 @@ export const classNameGenerator: ClassNameGenerator = ({ props, fullPath, subEle
118118
return ''
119119
}
120120
}
121+
case 'Code':
122+
return 'rounded'
121123
}
122124
}

src/npm-fastui-prebuilt/src/App.tsx

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { FastUI, renderClassName } from 'fastui'
1+
import { CustomRender, FastUI, renderClassName } from 'fastui'
22
import * as bootstrap from 'fastui-bootstrap'
33
import { FC, ReactNode } from 'react'
44

@@ -8,7 +8,7 @@ export default function App() {
88
<FastUI
99
rootUrl="/api"
1010
classNameGenerator={bootstrap.classNameGenerator}
11-
customRender={bootstrap.customRender}
11+
customRender={customRender}
1212
NotFound={NotFound}
1313
Spinner={Spinner}
1414
Transition={Transition}
@@ -38,3 +38,31 @@ const Transition: FC<{ children: ReactNode; transitioning: boolean }> = ({ child
3838
{children}
3939
</div>
4040
)
41+
42+
const customRender: CustomRender = (props) => {
43+
const { type } = props
44+
if (type === 'Custom' && props.library === undefined && props.subType === 'cowsay') {
45+
console.assert(typeof props.data === 'string', 'cowsay data must be a string')
46+
const text = props.data as string
47+
return () => <Cowsay text={text} />
48+
} else {
49+
return bootstrap.customRender(props)
50+
}
51+
}
52+
53+
const COWSAY = ` {above}
54+
< {text} >
55+
{below}
56+
\\ ^__^
57+
\\ (oo)\\_______
58+
(__)\\ )\\/\\
59+
||----w |
60+
|| ||`
61+
62+
const Cowsay: FC<{ text: string }> = ({ text }) => {
63+
const len = text.length
64+
const cowsay = COWSAY.replace('{text}', text)
65+
.replace('{above}', '_'.repeat(len + 2))
66+
.replace('{below}', '-'.repeat(len + 2))
67+
return <pre>{cowsay}</pre>
68+
}

src/npm-fastui/src/components/Code.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ export interface CodeProps {
99
className?: ClassName
1010
codeStyle?: string
1111
}
12+
1213
export const CodeComp = lazy(() => import('./CodeLazy'))
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { FC, useContext } from 'react'
2+
3+
import { ClassName } from '../hooks/className'
4+
import { ErrorContext } from '../hooks/error'
5+
6+
import { JsonData, JsonComp } from './Json'
7+
8+
export interface CustomProps {
9+
type: 'Custom'
10+
data: JsonData
11+
subType: string
12+
library?: string
13+
className?: ClassName
14+
}
15+
16+
export const CustomComp: FC<CustomProps> = (props) => {
17+
const { data, subType, library } = props
18+
const { DisplayError } = useContext(ErrorContext)
19+
20+
const description = [`The custom component "${subType}"`]
21+
if (library) {
22+
description.push(`from library "${library}"`)
23+
}
24+
description.push(`has no implementation with this frontend app.`)
25+
26+
return (
27+
<DisplayError title="Custom component without implementation" description={description.join(' ')}>
28+
Custom component data:
29+
<JsonComp type="JSON" value={data} />
30+
</DisplayError>
31+
)
32+
}

src/npm-fastui/src/components/Json.tsx

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { FC } from 'react'
22

33
import { ClassName } from '../hooks/className'
44

5+
import { CodeComp } from './Code'
6+
57
export type JsonData = string | number | boolean | null | JsonData[] | { [key: string]: JsonData }
68

79
export interface JsonProps {
@@ -10,16 +12,15 @@ export interface JsonProps {
1012
className?: ClassName
1113
}
1214

13-
export const JsonComp: FC<JsonProps> = ({ value }) => {
15+
export const JsonComp: FC<JsonProps> = (props) => {
16+
let { value, className } = props
1417
// if the value is a string, we assume it's already JSON, and parse it
1518
if (typeof value === 'string') {
16-
value = JSON.parse(value)
19+
try {
20+
value = JSON.parse(value)
21+
} catch (e) {
22+
// if it's not valid JSON, we just display it as a string
23+
}
1724
}
18-
return (
19-
<div className="code-block">
20-
<pre>
21-
<code>{JSON.stringify(value, null, 2)}</code>
22-
</pre>
23-
</div>
24-
)
25+
return <CodeComp type="Code" text={JSON.stringify(value, null, 2)} language="json" className={className} />
2526
}

src/npm-fastui/src/components/index.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import { JsonComp, JsonProps } from './Json'
4040
import { ServerLoadComp, ServerLoadProps } from './ServerLoad'
4141
import { ImageComp, ImageProps } from './image'
4242
import { IframeComp, IframeProps } from './Iframe'
43+
import { CustomComp, CustomProps } from './Custom'
4344

4445
export type {
4546
TextProps,
@@ -67,6 +68,7 @@ export type {
6768
ServerLoadProps,
6869
ImageProps,
6970
IframeProps,
71+
CustomProps,
7072
}
7173

7274
// TODO some better way to export components
@@ -97,6 +99,7 @@ export type FastProps =
9799
| ServerLoadProps
98100
| ImageProps
99101
| IframeProps
102+
| CustomProps
100103

101104
export type FastClassNameProps = Exclude<FastProps, TextProps | AllDisplayProps | ServerLoadProps | PageTitleProps>
102105

@@ -179,6 +182,8 @@ export const AnyComp: FC<FastProps> = (props) => {
179182
return <ImageComp {...props} />
180183
case 'Iframe':
181184
return <IframeComp {...props} />
185+
case 'Custom':
186+
return <CustomComp {...props} />
182187
default:
183188
unreachable('Unexpected component type', type, props)
184189
return <DisplayError title="Invalid Server Response" description={`Unknown component type: "${type}"`} />

0 commit comments

Comments
 (0)