-
Notifications
You must be signed in to change notification settings - Fork 348
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
Support valid min, max, step and value types for various input types #442
Conversation
I don't think supporting these unsafe uses is a good idea. If BuckleScript supports |
I agree it's not ideal that people can create these objects with |
Next release will be breaking at |
If we're open to breaking changes, then we can use an abstract type today with zero runtime (the 'union type trick'). E.g.: module Input = {
type t;
external string: string => t = "%identity";
external date: Js.Date.t => t = "%identity";
external float: float => t = "%identity";
external int: int => t = "%identity";
};
[@bs.deriving abstract]
type domProps = {
...,
[@bs.optional] max: Input.t,
[@bs.optional] min: Input.t,
[@bs.optional] value: Input.t,
...
}; EDIT: usage would look like, module Input = ReactDOMRe.Input;
<input value=5->Input.int min=0->Input.int max=10->Input.int> |
I had chosen this setup to prevent breaking and I find it more straightforward to avoid multiple functions (at the risk of the user passing multiple props rendering to the same name), but in the absence of @yawaramin Should I revise the PR or would you like to handle that? |
I think I just found a slightly safer approach: module Input = {
type t('a);
external string: string => t(string) = "%identity";
external date: Js.Date.t => t(Js.Date.t) = "%identity";
external float: float => t(float) = "%identity";
external int: int => t(int) = "%identity";
};
[@bs.deriving abstract]
type domProps('a) = {
[@bs.optional] min: Input.t('a),
[@bs.optional] max: Input.t('a),
[@bs.optional] step: Input.t('a),
}; This should force the related attributes: min, max, etc., to be of the same type. So it should be impossible to do I'm not sure if Reason JSX will work with this though. Let me try both approaches tonight and update this PR with the one that works. |
To model `<input>` element value-related attributes.
I've proposed sgny#1 |
Use type-safe 'union type' trick
src/ReactDOMRe.re
Outdated
|
||
external string: string => t(string) = "%identity"; | ||
external date: Js.Date.t => t(Js.Date.t) = "%identity"; | ||
external float: float => t(float) = "%identity"; | ||
external int: int => t(int) = "%identity"; | ||
|
||
external dateStep: float => step(Js.Date.t) = "%identity"; |
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.
Good catch! Btw I took a closer look, and according to https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-step , the step
attribute is always a number. So it would never be a date. I guess we can get rid of dateStep
!
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.
step
is allowed for dates, but I'm not sure if it can be a float. See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date.
There is apparently also any
as a valid value for step, I've been thinking of ways to handle that. I'd think of @bs.inline
but that probably requires t('a) = string
to work properly. I'll give that a try. I might make Step
a submodule of Input
. Any opinions intStep
ot Step.int
?
I'd originally included step: string
to allow an escape hatch. I think we can omit that when the input type is string, but perhaps it's useful to allow people the possibility to pass all these props as a string.
I don't believe this level of safety can be achieved with @bs.unwrap!
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.
If we want to support any
we would need to do a little more trickery, e.g.
module Input: {
type step(_);
...
[@bs.inline "any"] let anyStep: step(_);
external dateStep: float => step(Js.Date.t) = "%identity";
} = {
type step(_) = string;
...
[@bs.inline] let anyStep = "any";
external dateStep: float => step(Js.Date.t) = "%identity";
};
So we take advantage of JavaScript's dynamism here to allow the any
string as well as specific ints and floats. any
seems like it should be the default though, if no step
is specified. Maybe not worth worrying about.
Re: submodules, I'd say let's keep everything in Input
for now, we're not at the point where it's overwhelming.
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.
Just saw your comment now, so we've converged to the same trick, I'll update the PR and revise Input
to avoid the submodule.
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.
Default value for step
is 1 for all types of input, according to Mozilla documentation. Since type step('a) = string
only actually matters for inlined values, keeping anyStep
shouldn't hurt, but I'm not particularly attached to it either.
@sgny oh now I understand what you did: |
We could probably only allow module Input = {
type t('a);
external string: string => t(string) = "%identity";
external date: Js.Date.t => t(Js.Date.t) = "%identity";
external float: float => t(float) = "%identity";
external int: int => t(int) = "%identity";
module Step: {
type t('a);
[@bs.inline "any"] let any: t(_);
external date: int => t(Js.Date.t) = "%identity";
external float: float => t(float) = "%identity";
external int: int => t(int) = "%identity";
} = {
type t('a) = string;
[@bs.inline] let any = "any";
external date: int => t(Js.Date.t) = "%identity";
external float: float => t(float) = "%identity";
external int: int => t(int) = "%identity";
};
}; |
@sgny yup, perfectly valid. I love how the type of the |
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.
One comment, but looks great!
[@bs.deriving abstract] | ||
type props = { | ||
type props('input) = { |
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.
@rickyvetter mentioned yesterday that props
is for JSX2, maybe we should consider not adding the 'type-safe' input support to JSX2?
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.
So props
can be left alone as it is likely to be removed soon, right?
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.
Yes. props
will be deprecated in 0.9 and removed in 0.10/1.0.
Thinking a bit further to make sure nothing is missed, I realized We could use module Input: {
type type_('a);
type t('a);
type step('a);
external string: string => t(string) = "%identity";
external date: Js.Date.t => t(Js.Date.t) = "%identity";
external float: float => t(float) = "%identity";
external int: int => t(int) = "%identity";
external bool: bool => t(bool) = "%identity";
[@bs.inline "number"]
let intType: type_(int);
[@bs.inline "number"]
let floatType: type_(float);
[@bs.inline "date"]
let dateType: type_(Js.Date.t);
[@bs.inline "checked"]
let checkedType: type_(bool);
external otherType: string => type_(_) = "%identity";
[@bs.inline "any"]
let anyStep: step(_);
external dateStep: int => step(Js.Date.t) = "%identity";
external floatStep: float => step(float) = "%identity";
external intStep: int => step(int) = "%identity";
} = {
type type_('a) = string;
type t('a);
type step('a) = string;
external string: string => t(string) = "%identity";
external date: Js.Date.t => t(Js.Date.t) = "%identity";
external float: float => t(float) = "%identity";
external int: int => t(int) = "%identity";
external bool: bool => t(bool) = "%identity";
[@bs.inline]
let intType = "number";
[@bs.inline]
let floatType = "number";
[@bs.inline]
let dateType = "date";
[@bs.inline]
let checkedType = "checked";
external otherType: string => type_(_) = "%identity";
[@bs.inline]
let anyStep = "any";
external dateStep: int => step(Js.Date.t) = "%identity";
external floatStep: float => step(float) = "%identity";
external intStep: int => step(int) = "%identity";
}; Since |
How does this work if you don't define any of the parameterized values? For example Thoughts on this in relation to #571? I believe it's somewhat controversial but I would like to further specialize the props types per-element. Since many elements don't even take these attributes it would minimize the places we would need to add type parameters for this kind of tying. In general I am into this change and the implementation looks really sweet. I think we should be careful with it though as it adds a lot of complexity. For example you could imagine doing the same with color where folks can't pass strings as colors and instead are force to do something like |
Hi @rickyvetter I tried my local version and didn't get any errors, e.g. You're right there is complexity here, but I was thinking the tradeoff is worth it because as far as I know the interplay between the The other issue is that, if we lock down DOM element props using a codegen technique (say, from the W3C specs), then the codegen would need special handling logic for these special input validations that we're putting in place here. If you indeed have that in your sights, then we'd probably want to fall back to the less type-safe option I proposed before, #442 (comment) |
Sorry, for the late response, I've been tied down for the past few days with work. I'd already tried only passing props without type parameters and as with @yawaramin had no issues. This PR already has limited impact when the component is not I'll prepare a commit specifying |
I have implemented @rickyvetter |
I consider this closely related to #567. I'm going to do some more work there and don't want to land this preemptively if it requires more breaking changes for elements that won't be parameterized. |
I'm closing this, but I might ping @sgny or @yawaramin in further development since the idea to keep a higher bar for type-safety for lower-case components has been under my interest for a while. |
For various types of
<input />
, there are different allowed types for various props (exceptstring
, which is the fallback). To allow some degree of type safety, while not excluding any valid use, this PR defines multiple props, all rendered as the same prop (e.g.min
) by the[@bs.as]
annotation, but accepting different types such asint
,float
, etc.This is not exhaustive as more props could be similarly defined. I have chosen the
string
fallback type as a sort of default, where using the canonical JS prop name in Reason requires astring
argument.