Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic component #2 #767

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions admin-js/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,18 @@ import {
// Filters
email, maxLength, maxValue, minLength, minValue, regex, required,
// Misc
AutocompleteInput, EditButton, HttpError, WithRecord
AutocompleteInput, EditButton, HttpError, WithRecord,
// For custom components...
useCreatePath, useRecordContext, useResourceContext, Button,
} from "react-admin";
import { createElement } from "react";
import VisibilityOffIcon from "@mui/icons-material/VisibilityOff";


import Queue from '@mui/icons-material/Queue';
import { Link } from 'react-router-dom';
import { stringify } from 'query-string';

// Hacked TimeField/TimeInput to actually work with times.
// TODO: Replace once new components are introduced using Temporal API.

Expand Down Expand Up @@ -66,7 +74,9 @@ const COMPONENTS = {
ReferenceOneField, SelectField, TextField, TimeField,

BooleanInput, DateInput, DateTimeInput, NumberInput, ReferenceInput, SelectInput,
TextInput, TimeInput
TextInput, TimeInput,

useRecordContext, useResourceContext, useCreatePath, Button, Queue, Link, stringify, createElement
};
const FUNCTIONS = {email, maxLength, maxValue, minLength, minValue, regex, required};
const _body = document.querySelector("body");
Expand All @@ -77,6 +87,8 @@ if (STATE["js_module"]) {
// The inline comment skips the webpack import() and allows us to use the native
// browser's import() function. Needed to dynamically import a module.
MODULE_LOADER = import(/* webpackIgnore: true */ STATE["js_module"]).then((mod) => {
for (const k of Object.keys(mod.g))
mod.g[k] = COMPONENTS[k]
Comment on lines +90 to +91
Copy link
Member Author

@Dreamsorcerer Dreamsorcerer Aug 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main application, after dynamically importing the module, simply loops through the module's global object and inserts all the functions it asks for.

This means the module's components will be using the actual same react functions as the main application, and therefore have the same context etc. required to make the component work correctly.

Copy link

@Anthony-E-B Anthony-E-B Aug 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Dreamsorcerer coming from SO as discussed. Would like to have docs or steps to maybe set up an environment where I can reproduce your issue and try out things. If that's too much work you should not bother because there's a tiny chance I can really take a deep look at it. Will try tho. Thanks

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, the base project is in Python, if you're on Mac/Linux then you'll already have it installed, so should only take a couple of minutes to run.

  • Clone this branch and cd aiohttp-admin.
  • Install python dependencies: pip install -r requirements.txt
  • Build the JS module: cd admin-js; yarn install; yarn build
  • Try the example:
    • cd ../examples
    • PYTHONPATH='..' python simple.py
    • Go to http://localhost:8080/admin
    • You can login with admin/admin and see the basic project working.

If you click on one of the rows to get to the show view, you'll see the custom button created in the examples:
image

The custom button is loaded from examples/custom-button.js. If you want to try building something with yarn or similar, you can just create a new project and copy the output file over custom-button.js.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Anthony-E-B I had an idea, maybe you might know if it's workable and how to go about it?

I was thinking that maybe I could find a way to replace react in a project's dependencies with some kind of shim.

e.g. I want to use a 3rd-party component, and they have react listed as a dependency. In my build configuration, I replace react with my custom library (no idea how this is done, but presumably it must be possible so people can use patched versions of a library etc.). That custom library has all the same functions as react, except that it will actually proxy the calls to the main application.

So, essentially we replace react in the build script in order to build the component without react being in the compiled module. Then when we import that module from the main application, the module will send all those react calls to the main application, which will use the version of react already compiled in there.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this can be used for replacing react:
https://github.com/nerdchacha/module-replace-webpack-plugin

Although doesn't appear to be maintained. Then I just need to figure out a way to build the shim and proxy the calls through.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Anthony-E-B If you're interested, I've come up with a solution in #804.

I think the only thing that would be nice to improve, is having some way to import react-admin/react-router-dom components when they are not included in the shim (because adding all of react-admin to the shim increases the application size by 400KB) with some kind of fallback mechanism in the build process.

Object.assign(COMPONENTS, mod.components);
Object.assign(FUNCTIONS, mod.functions);
});
Expand Down
39 changes: 39 additions & 0 deletions examples/custom-clone.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
export const g = {"Queue": null, "Link": null, "stringify": null, "Button": null,
"createElement": null, "useRecordContext": null, "useCreatePath": null,
"useResourceContext": null}

const CustomCloneButton = (props) => {
const {
label = "My custom clone",
scrollToTop = true,
icon = g.createElement(g.Queue),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then we use the functions from the global object the same as we would if we had imported them normally.

We can't use JSX here, because it will evaluate to null at compile time and not work at all.

...rest
} = props;
const resource = g.useResourceContext(props);
const record = g.useRecordContext(props);
const createPath = g.useCreatePath();
const pathname = createPath({resource, type: "create"});
props = {
component: g.Link,
to: (
record
? {
pathname,
search: g.stringify({source: JSON.stringify(record)}),
state: {_scrollToTop: scrollToTop},
}
: pathname
),
label: label,
onClick: stopPropagation,
...sanitizeRestProps(rest)
};
return g.createElement(g.Button, props, icon);
};

// useful to prevent click bubbling in a datagrid with rowClick
const stopPropagation = e => e.stopPropagation();

const sanitizeRestProps = ({resource, record, ...rest}) => rest;

export const components = {CustomCloneButton: CustomCloneButton};
13 changes: 11 additions & 2 deletions examples/simple.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from datetime import datetime
from enum import Enum
from pathlib import Path

import sqlalchemy as sa
from aiohttp import web
Expand All @@ -13,6 +14,7 @@

import aiohttp_admin
from aiohttp_admin.backends.sqlalchemy import SAResource
from aiohttp_admin.types import comp

# Example DB models

Expand Down Expand Up @@ -49,6 +51,11 @@ async def check_credentials(username: str, password: str) -> bool:
return username == "admin" and password == "admin"


async def serve_js(request):
js = Path(__file__).with_name("custom-clone.js").read_text()
return web.Response(text=js, content_type="text/javascript")


async def create_app() -> web.Application:
engine = create_async_engine("sqlite+aiosqlite:///:memory:")
session = async_sessionmaker(engine, expire_on_commit=False)
Expand All @@ -64,6 +71,7 @@ async def create_app() -> web.Application:
sess.add(SimpleParent(id=p.id, date=datetime(2023, 2, 13, 19, 4)))

app = web.Application()
app.router.add_get("/js", serve_js, name="js")

# This is the setup required for aiohttp-admin.
schema: aiohttp_admin.Schema = {
Expand All @@ -72,9 +80,10 @@ async def create_app() -> web.Application:
"secure": False
},
"resources": (
{"model": SAResource(engine, Simple)},
{"model": SAResource(engine, Simple), "show_actions": (comp("CustomCloneButton"),)},
{"model": SAResource(engine, SimpleParent)}
)
),
"js_module": str(app.router["js"].url_for())
}
aiohttp_admin.setup(app, schema)

Expand Down
Loading