pydantic / FastUI

Build better UIs faster.
https://fastui-demo.onrender.com
MIT License
8.09k stars 310 forks source link

Custom Components #19

Closed samuelcolvin closed 9 months ago

samuelcolvin commented 9 months ago

Currently only the white listed set of components can be rendered, while we can and should extend that list, we should also allow custom components.

The idea would be to define a component like

class Custom(BaseModel):
    data: Any
    class_name: _class_name.ClassName = None
    sub_type: str = Field(serializeation_alias='subType')
    type: typing.Literal['Custom'] = 'Custom'

subType let's implementing code do a nice switch thing when deciding how to render custom components.

The default implementation should just show data as JSON with a message saying "not implemented".

paddymul commented 9 months ago

I just put in a PR into FastUI for what should definitely be a custom component packaged in a separate pypi package. Here is the essence of the developer experience I would love to see in FastUI. I'll put my own component in (DFViewer)

class DFViewer(BaseModel):
    data: pd.Dataframe
    type:"DFViewer"

    class Meta: #borrowed from django models
        js_cdn_package: "buckaroo-dfviewer"
        js_local_dev_path: "../local_package/dist/index.js"
        js_component: "DFViewer"
        js_type: "DFViwerProps"

Somewhere else an app would have a setup step that looks something like:

from fastui import AnyComponent,FastUI
FastUI.configure(AnyComponent + [DFViewer, MyOtherCustomComponent])

I'm not attached to class Meta in the CustomComponent implementation... But I do think that the JS Configuration options should be clearly separated from the rest of the Pydantic typing for the Model. A user of the CustomComponent should see something that mostly looks like a regular Pydantic model, the accidental complexity of the JS loading should be clearly delineated.

In order of importance for JS Loading I would say

I'm very excited to see where this project goes. Just giving my feedback.

samuelcolvin commented 9 months ago

Thanks so much. I think what I think you're asking for is very possible.

I would suggest that if you want to implement a custom component, you just wrap the implementation I mentioned above, perhaps we should add a lib key to avoid conflicts between different libs implementing their own components.

In concrete terms, you would do something like this:

def df_viewer(df: pd.Dataframe) -> c.Custom:
    return c.Custom(df.to_json(), sub_type='DFViewer', lib='Buckaroo')

The point is we're reusing CustomComponent, not defining a new class.


On the frontend, I don't think we need most of the stuff you're discussing.

We just let users (or libraries), create their own react apps, install fastui and use the FastUI component, and pass a customRender method that returns a specific compent when the props match type == 'Custom' && lib == 'Buckaroo' && sub_type == 'DFViewer', like this in npm-fastui-prebuilt:

export default function App() {
  return (
    <div className="top-offset">
      <FastUI
        rootUrl="/api"
        classNameGenerator={bootstrap.classNameGenerator}
        customRender={bootstrap.customRender}
        NotFound={NotFound}
        Spinner={Spinner}
        Transition={Transition}
      />
    </div>
  )
}
paddymul commented 9 months ago

If you expect devs using FastUI to create a regular React frontend app, and insert the <FastUI...> </FastUI> component into their regular frontend app, I understand your approach. Call this the enhance-frontend-approach

If however users of FastUI expect to only write python code and have FastUI spit out a UI, I think my js loading bits make more sense. Call this the no-user-js approach.

For the no-user-js approach I would want the experience for a user of Buckaroo-FastUI to be as follows (psuedo-code for package and argument names).

pip install Buckaroo-FastUI

then

from buckaroo.fastui import DFViewer

@router.get('', response_model=FastUI, response_model_exclude_none=True)
def components_view() -> list[AnyComponent]:
    return demo_page(
        c.Div(components=[c.Heading(text='DFViewer showing citibike data', level=2)]),
        DFViewer(df=pd.read_csv("citibike_data.csv")))

I think that for the most part developing JS/TS/TSX is fairly straightforward for many users... Setting up a TSX buildchain is very tricky with many variations. Providing a well thought out way to do the most common things will hopefully avoid many possible problems.

I don't think that the no-user-js approach has to be an early goal of FastUI, and there are reasons to wait on implementing that. But I think that the core of FastUI is pretty close to enabling that experience and its helpful to keep it in mind.

samuelcolvin commented 9 months ago

@paddymul ye, I think that's fair.

Providing a way to use extra js while still using the main prebuilt react app has lots of advantages.

I guess the idea would be that we add:

  1. A way to inject extra JS scripts into the page via a URL, or JS via a string
  2. A way to render custom elements by calling a JS function.

My main question is about (2.) - the most obvious solution is that the JS function returns a string of HTML, that could could also return <div id="whatever">loading</div> then render content into #whatever asynchronously. I don't know enough about the bowels of React to know if there's a better solution.


As you say, I'd rather not add this yet.