-
-
Notifications
You must be signed in to change notification settings - Fork 338
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
Form component #1598
Form component #1598
Conversation
From a high level review, (I didn't go in depth yet), I see potentially two problems coming up:
|
What you are describe sounds more like a general problem that Toolpad has and not something that's related to form or this PR. So while I agree in theory it can bring problems, I wouldn't consider this topic to be part of this task/PR
It breaks now, but I will make sure that it does not when there is not context (or just add a global context). However as per your alternative suggestion - I did try to go that way where form only wraps regular components and detects it's changes, but it creates an issue of binding back data or controlling data from the form itself UNLESS we want to control data via hooking into our bindings/dom data model? Though still we would need way to "register" form fields somehow and current implementation basically solves all that |
What I'm describing is a limitation of the current code base that needs to be refactored if we want to implement features like List/Form in a way that is not "hacking it in to make it look like it mostly works, covering up all the nasty side-effects". Which is certainly possible, but also it's just passing on the hot potato to the person that will handle the tickets that inevitably come in for this feature. |
No it doesn't require refactoring if we want to implement the base requirements that we have today
Working around certain limitations that are imposed on architectural decisions made in the past is not necessarily a hack. Improving code by knowing more requirements is a natural way of evolving codebase and it doesn't really matter who's the person going to be who "inevitably come in for this feature". The more important thing is to understand whether we (as a team) even want or need to solve what you call "nasty side-effects" at all, as it's also possible that nobody will even care about it until certain point of time, where it's more likely that someone (at least those who upvoted original feature) will care for not having a feature they requested. |
@@ -268,6 +270,30 @@ function RenderedNodeContent({ node, childNodeGroups, Component }: RenderedNodeC | |||
return hookResult; | |||
}, [isLayoutNode, liveBindings, nodeId]); | |||
|
|||
const changeClosestFormValue = React.useCallback( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After trying external form libs work to support form component I decided to throw all that away and start from scratch. IMO this should simplify how we deal with fields that have changing values, without having to decorate them with extra logic.
@Janpot this is not ready for review, but I want to double check before proceeding whether this could be a good approach or if you have an alternative suggestion how to approach it?
- Form becomes just a wrapper without any extra children or any logic (so far)
- When
onChangeHandler
is triggered (assuming this is any component that sets value and that we would like to access via form) we check if component is placed inside form (getClosestForm
). If we find a closest form then together with field change handler we set controlled binding for the form value grabbing current value and altering the value that was just changed
The issue that I'm having right now is - even if I configure form like this:
value: {
visible: false,
typeDef: { type: 'object', default: {} },
onChangeProp: 'onChange',
},
for some reason closestForm.props.value
is not defined initially (I'm expecting it would default to {}
?).
I'd appreciate help to better understand how the data flows and what am I missing to make this work correctly (if this approach makes sense)
Apart from that I'm able to capture value changes and use it as form value.
CleanShot.2023-02-08.at.15.13.27.mp4
Any feedback on this approach?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure what the specific issue is for the missing default value - you'd probably have to look into the bindings logic in the runtime to figure it out - there's a useEffect
there that handles default values (src/runtime/ToolpadApp
, around line 349?). At least this is where i would look first...
There probably are some similarities between what you need to do here and what I did for the List component in #1527 (looking for a review there btw to see if my latest method sounds good? Also maybe it would be helpful if we merged the List first.).
What I did there was to create a local scope inside the list component by making it set a context, on the runtime (src/runtime/ToolpadApp
) that wraps all of it's children. In the case of a form, this context could probably hold the form state.
The context could also have an onChange
handler that every child of it could use to change values.
Anyway that's just a generic idea of it could work, without having gotten into the details, so it could be wrong about some things or not work well for this particular case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also maybe it would be helpful if we merged the List first.
For sure, this is no where near being ready to be merged anyways so I'd need to adjust accordingly after List is merged.
The context could also have an onChange handler that every child of it could use to change values.
Could you please clarify what you mean by that? Currently it's implemented in a way that child components wouldn't have to know anything about form component
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How I would go about it without doing low-level work to toolpad to enable defining arbitrary scopes.
-
create shared
FormValue
andSetFormValue
contexts to be used by both the Form and individual components. These contexts are provided by theForm
component to provide itsvalue
andonChange
prop downstream:<Paper> <FormValue.Provider value={value}> <SetFormValue.Provider value={(field, newValue) => onChange({ ...value, [field]: newValue })} > {children} </SetFormValue.Provider> </FormValue.Provider> </Paper>
In the form components we use these contexts, and if they exist we override the
value
andonChange
from the props:const formValue = React.useContext(FormValue); const setFormValue = React.useContext(SetFormValue); inveriant(!!formValue === !!setFormValue, '...'); const inputValue = formValue ? formValue[name] : valueProp; const setInput = setFormValue ? (newValue) => setFormValue(name, newValue) : onChangeProp; return <TextField value={input} onChange={setInput} />;
-
If the above works, we have the basis, but we still need to filter out the components that are bound to a form in the bindings. The second part of 1. will be shared by every form-capable component, so we can extract it and put it in the runtime. e.g. we can add some definition to
ToolpadComponentConfig
like:argTypes: { /* ... */ }, formValue: ['value', 'onChange']
In the runtime we can check for existence of this config when rendering the component and do the logic of 1. automatically, when preparing its properties.
Then we can also read it while setting up the bindings and omit these properties from creating scope variables (so just removescopePath
for those)This is a two part explanation, the end result is 2. witha clean global scope, but try 1. first to verify that the reasoning works.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Janpot 's idea sounds good to me!
Could you please clarify what you mean by that? Currently it's implemented in a way that child components wouldn't have to know anything about form component
In this case this onChange
handler would probably need to be passed as a prop to each component, or some other method with the same result.
I guess one of the main advantages of using context would be that you wouldn't have to "search" for the closest form manually.
As for including form logic inside components themselves, I'm fine with that if we decide it's the easiest/best way to go about it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for through explanation, it seems that this would be similar to the first approach that I tried (except that it used external form lib, which is not necessary).
-
However I'm wondering - is it necessary to decorate each "form-capable" component with extra information such as
formValue: ['value', 'onChange']
if I understand correctly this should be pretty much the same for each component? -
Am I missing something important or could we actually do the same without decorating components with special configuration and using approach similar to what this "new implementation" is doing - using
getClosestForm
determine if we should create global scope or not and how the value should be set. In such case if component is part of form, then it would always set value on the form values, and also read it from there, but all of that could happen without component knowing about it (unless that's the part that is not possible)?
So I'm just trying to understand if I'm missing something which would make current approach incompatible with what you defined
Also, can you confirm if I understand correctly what is meant by "omit properties from creating scope":
- If form-capabale component is part of form it would not be accessible from a global scope, like below? 👇
Thank you
@Janpot let me know if you have any more feedback since your previous comments (see above), otherwise we can probably merge this PR. |
I feel like this feature could use an integration test |
I was gonna do it separately but I'll add it before we merge this then, even if in a separate PR. |
Either way is fine for me, I just don't feel like this feature shouldn't be covered at all |
I'll merge this soon if there's no more feedback, will add the integration test separately right after. |
This reverts commit ad61b30.
The text field stopped being interactive in forms in my last change... |
This reverts commit de1afd0.
Closes #749
Drag-and-drop form component.
button
,reset
andsubmit
as options for button typeFor automatically generating a form from external data I've created this separate issue: #1770
Screen.Recording.2023-03-21.at.17.42.39.mov
Screen.Recording.2023-03-21.at.17.43.35.mov