Related
i want to implement collapse and hidden in vuejs
but i think, ref it does not work in vue3
i am getting this error Header.vue?76f0:68 Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'myText')
this is my code
<button class="border-0 navbar" #click="toggle()">
<Links
class="color-dark"
ref="myText"
:style="[isActive ? { height: computedHeight } : {}]"
/>
function toggle() {
this.isActive = !this.isActive
}
function initHeight() {
this.$refs.myText.style.height = 'auto'
this.$refs.myText.style.position = 'absolute'
this.$refs.myText.style.visibility = 'hidden'
this.$refs.myText.style.display = 'block'
const height = getComputedStyle(this.$refs['myText']).height
this.computedHeight = height
this.$refs.myText.style.position = null
this.$refs.myText.style.visibility = null
this.$refs.myText.style.display = null
this.$refs.myText.style.height = 0
}
watchEffect(async () => {
initHeight()
})
i was copying this code to vuejs3 (this worked but i need to vuejs3)
https://jsfiddle.net/rezaxdi/tgfabw65/9/
Looks like there's something more to it than what's in the code. A simple vue2=>vue3 conversion from example works just fine
example:
Vue.createApp({
data() {
return {
isActive: false,
computedHeight: 0
}
},
methods: {
toggle: function() {
this.isActive = !this.isActive;
},
initHeight: function() {
this.$refs['myText'].style.height = 'auto';
this.$refs['myText'].style.position = 'absolute';
this.$refs['myText'].style.visibility = 'hidden';
this.$refs['myText'].style.display = 'block';
const height = getComputedStyle(this.$refs['myText']).height;
this.computedHeight = height;
this.$refs['myText'].style.position = null;
this.$refs['myText'].style.visibility = null;
this.$refs['myText'].style.display = null;
this.$refs['myText'].style.height = 0;
}
},
mounted: function() {
this.initHeight()
}
}).mount("#app");
p {
height: 0;
overflow: hidden;
transition: 1s;
}
<script src="https://unpkg.com/vue#3.2.31/dist/vue.global.prod.js"></script>
<div id="app">
<p ref="myText" :style="[isActive ? { height : computedHeight } : {}]">
Now it's smooth - getting closer to BS4 collapse, but now I need to know the exact height of each section at a particular screen size to set the max-height. Over-compensating max-width work ok, but is still a bit 'jerky'. Is there a way to calculate an
elements height before starting the show/hide? If the max-height value is too small, elements overlap.
</p>
<button #click="toggle">Open / Close</button>
</div>
however, I see you are using a watchEffect, so I surmise that you might be using (some🤷♂️) composition API functionality. In this case, the watch will execute before mount, so it will run initHeight which will fail.
If you are using composition api, there's more things there that might cause it to not work, so you may need to show more of the code. Alternatively, you can stick to the Class API, which works same as it did in vue2.
I have a function which can receive a value of type either TLocation or {height: number, width: number}.
TLocation is defined as follows:
type TLocation = {
height: number,
width: number,
x: number,
y: number
}
Here is my function:
function transformer<
L: { height: number, width: number } | TLocation,
V: { height: number, width: number }
>(location: L, viewbox: V): L {
if (location.hasOwnProperty('height')) {
return {
height: location.height / viewbox.height,
width: location.width / viewbox.width,
x: location.x / viewbox.width,
y: location.y / viewbox.height
};
} else {
return {
height: location.height / viewbox.height,
width: location.width / viewbox.width
};
}
}
Here is a screenshot of where I have flow errors:
Both errors are:
Flow: Cannot return object literal because object literal [1] is incompatible with `L` [2].
I know I can't use hasOwnProperty as I am trying to do, but I'm not sure how to properly return the correct value (and therefore type).
If there is anything special I need to do when calling the function, I'd really appreciate an example of the usage as well.
The short answer is that no, you can't do this with flow, not in the way you're trying to.
Fundamentally, you have two different main things going on. You have a type refinement problem, and you have a problem with returning a generic type.
Type Refinement
Type refinement is an area where the official flow docs are really insufficient. It would be a lot more useful if they were just clear about what can't be accomplished with type refinement currently. It's really pretty simplistic.
You basically have two object types, A and B, but you know that A has a member B doesn't, so you should be able to refine the object to A or B based on whether or not that member exists. It makes sense to you, and it seems like it should make sense to flow. But flow doesn't implement a mechanism for that.
type A = {| +aMember: string |};
type B = {| +bMember: string |};
// `thing` is a union of `A` and `B` who have different properties
declare var thing: A | B;
// Only `A` has `aMember`, so we should be able to check for that
// to refine the type, right?
if (typeof thing.aMember === 'string') {
(thing: A); // ERROR! `thing` is still a union of `A` and `B`!
}
(try)
Unfortunately type refinement in flow is a bit too naive to figure this out. What this particular check does accomplish is that it refines the type of the property:
if (typeof thing.aMember === 'string') {
// we can now use `thing.aMember` as a string just fine, but
// `thing` is still a union of `A` and `B`
(thing.aMember: string);
}
(try)
The only way that we can refine whole objects into different types in flow is with a disjoint union. This would mean that we would introduce a literal field to distinguish between the two object types, which is probably not what you want to do in this case:
type A = {| type: 'a', +aMember: string |};
type B = {| type: 'b', +bMember: string |};
// `thing` is a union of `A` and `B` who have different properties
declare var thing: A | B;
if (thing.type === 'a') {
// flow now knows that `thing` is of type `A`
(thing.aMember: string);
} else {
// and here in the else, flow knows that `thing` is of type `B`
(thing.bMember: string);
}
(try)
Generic Return Type
So what happens if we decide to use a disjoint union to resolve this?
type Common = {| width: number, height: number |};
type Size = {| ...Common, type: 'size' |};
type Rect = {| ...Common, type: 'rect', x: number, y: number |};
const func = <T: Size | Rect>(thing: T): T => {
// Here we've bypassed the type refinement problem by using a
// disjoint union, but we're not returning `thing`, are we?
if (thing.type === 'size') {
// ERROR!
// Cannot return object literal because object literal [1]
// is incompatible with `T` [2].
return { type: 'size', width: 0, height: 0};
} else {
// ERROR!
// Cannot return object literal because object literal [1]
// is incompatible with `T` [2].
return {
type: 'rect',
width: 0,
height: 0,
x: 0,
y: 0,
}
}
}
(try)
The problem is that our function expects to return exactly T, but we're not returning T, we're returning some other type. You might think, "we're gonna return something of a type that T is compatible with at the time it gets returned," but that's not actually what we're telling flow. We're telling flow to expect us to return exactly T.
Let's look at an even simpler example:
const f = <T: number | string>(thing: T): T => {
if (typeof thing === 'number') {
return 0;
// ERROR!
// Cannot return `0` because number [1] is incompatible
// with `T` [2].
} else {
return 'stuff';
// ERROR!
// Cannot return `stuff` because string [1] is incompatible
// with `T` [2].
}
}
(try)
Why does this not work? Well, I think that the best mental model to use to consider this, is templates. Let's consider that the way that generic types work is by creating versions of the function for each member of the set that the generic type bounds define. This obviously isn't how this actually works, but it'll be useful when considering this example.
Our generic in this case defines a set of two types, string and number, so let's consider that this results in two versions of this function:
const fForNumber = (thing: number): number => {
if (typeof thing === 'number') {
return 0;
// Works fine.
} else {
return 'stuff';
// ERROR!
// Cannot return `stuff` because string [1] is incompatible
// with `number` [2].
}
}
const fForString = (thing: string): string => {
if (typeof thing === 'number') {
return 0;
// Cannot return `0` because number [1] is incompatible
// with string [2].
} else {
return 'stuff';
// Works fine.
}
}
(try)
So basically we've just copied and pasted the function, then replaced T with number in the case of the first function, and we've replaced it with string in the second case. It's pretty clear when looking at each of these versions independently why they would have errors. Now we just need to consider that the version with the generic is basically these two functions superimposed on top of each other, and the result is that we get both errors. To not get any errors in our generic functions, every type defined by the bounds of the generic must independently make sense in place of the generic through every path of the function.
Solutions
Given this formulation, the most immediate solution is to just ignore the errors basically:
type TLocation = {
height: number,
width: number,
x: number,
y: number
}
function transformer<
L: { height: number, width: number } | TLocation,
V: { height: number, width: number }
>(location: L, viewbox: V): L {
// Refine both of these properties so we can use them without errors:
if (typeof location.x === 'number' && typeof location.y === 'number') {
return (({
height: location.height / viewbox.height,
width: location.width / viewbox.width,
x: location.x / viewbox.width,
y: location.y / viewbox.height
// Type through `any` to `L`
}: any): L);
} else {
return (({
height: location.height / viewbox.height,
width: location.width / viewbox.width
// Type through `any` to `L`
}: any): L);
}
}
This is sort of okay because we (the authors, not flow) know what the type we should be returning is, so as long as we are returning that correctly it should work fine. But obviously this is sort of ugly since we're basically just turning off the type system. Unfortunately we're gonna have to make a compromise somewhere to make this particular formulation work.
Usually in this type of situation it makes the most sense to back up and implement a more explicit design:
type Size = {| width: number, height: number |};
type Rect = {| ...Size, x: number, y: number |};
function sizeTransformer(size: Size, viewbox: Size): Size {
return {
height: size.height / viewbox.height,
width: size.width / viewbox.width,
}
}
function rectTransformer(rect: Rect, viewbox: Size): Rect {
return {
...sizeTransformer({ width: rect.width, height: rect.height }, viewbox),
x: rect.x / viewbox.width,
y: rect.y / viewbox.height
}
}
(try)
Obviously you have to consider how this is currently being used, but usually there is a way to implement a more specific solution.
I think that one thing people tend to do when they start working with strongly typed code is to try and fit strong typing onto patterns they're used to from more dynamic environments. In my experience, this is often a code smell. It's often revealing that the code could be considered more from a data-first perspective. Generics are a big and powerful tool, but if you're asking yourself whether or not you can use them to solve a really small use case, then it might be a good time to step back and reconsider the approach.
I need to change the number of elements to show in my grid view with respect to the device type. For mobile views and for desktop views. I went through so many suggetions like
check device screen size
check device type
and finally I ended up checking the device type inside a method.
methods: {
isMobile() {
if (screen.width <= 760) {
return true
} else {
return false
}
},
},
and I used isMobile() method to define my conditon for each device. My question is how can I use this isMobile() method as computed property as I only return a boolean value. And is it ok to use like this without a even listner. Because it's so far working fine. But I'm looking for a more unified solution as I'm new to VueJs. Thanks in advance.
You can use a created hook instead of methods, it will execute the isMobile function before the DOM loads meaning that you can identify the device type before loading the number of grids
like this:
created(){
isMobile() {
if (screen.width <= 760) {
return true
} else {
return false
}
},
}
Great article that shows a couple of different ways to do this in JavaScript right here
for vue3js using setup(){
<div class="mobile" v-if="isMobile()">
setup(){
const isMobile = () => screen.width <= 760
return {isMobile}
}
created() {
window.addEventListener("resize", this.resizeHandler); // extra bonus not needed for this question.
const orientation = window.screen.orientation.type
if (orientation === "portrait-primary") {
this.colwidth = 320; // example1
this.popupWidth = "100%"; // example2
} else if (orientation === "landscape-primary") {
this.popupWidth = "50%";
this.colwidth = 600;
}
I have a button in SwiftUI and I would like to be able to have a different action for "tap button" (normal click/tap) and "long press".
Is that possible in SwiftUI?
Here is the simple code for the button I have now (handles only the "normal" tap/touch case).
Button(action: {self.BLEinfo.startScan() }) {
Text("Scan")
} .disabled(self.BLEinfo.isScanning)
I already tried to add a "longPress gesture" but it still only "executes" the "normal/short" click. This was the code I tried:
Button(action: {self.BLEinfo.startScan() }) {
Text("Scan")
.fontWeight(.regular)
.font(.body)
.gesture(
LongPressGesture(minimumDuration: 2)
.onEnded { _ in
print("Pressed!")
}
)
}
Thanks!
Gerard
I tried many things but finally I did something like this:
Button(action: {
}) {
VStack {
Image(self.imageName)
.resizable()
.onTapGesture {
self.action(false)
}
.onLongPressGesture(minimumDuration: 0.1) {
self.action(true)
}
}
}
It is still a button with effects but short and long press are different.
Combining a high priority gesture and a simultaneous gesture should do the trick.
Button(action: {})
{
Text("A Button")
}
.simultaneousGesture(
LongPressGesture()
.onEnded { _ in
print("Loooong")
}
)
.highPriorityGesture(TapGesture()
.onEnded { _ in
print("Tap")
})
Found this a handy pattern when interacting with other views as well.
I just discovered that the effect depends on the order of the implementation. Implementing the detection of gestures in the following order it seems to be possible to detect and identify all three gestures:
handle a double tap gesture
handle a longPressGesture
handle a single tap gesture
Tested on Xcode Version 11.3.1 (11C504)
fileprivate func myView(_ height: CGFloat, _ width: CGFloat) -> some View {
return self.textLabel(height: height, width: width)
.frame(width: width, height: height)
.onTapGesture(count: 2) {
self.action(2)
}
.onLongPressGesture {
self.action(3)
}
.onTapGesture(count: 1) {
self.action(1)
}
}
Here is my implementation using a modifier:
struct TapAndLongPressModifier: ViewModifier {
#State private var isLongPressing = false
let tapAction: (()->())
let longPressAction: (()->())
func body(content: Content) -> some View {
content
.scaleEffect(isLongPressing ? 0.95 : 1.0)
.onLongPressGesture(minimumDuration: 1.0, pressing: { (isPressing) in
withAnimation {
isLongPressing = isPressing
print(isPressing)
}
}, perform: {
longPressAction()
})
.simultaneousGesture(
TapGesture()
.onEnded { _ in
tapAction()
}
)
}
}
Use it like this on any view:
.modifier(TapAndLongPressModifier(tapAction: { <tap action> },
longPressAction: { <long press action> }))
It just mimics the look a button by scaling the view down a bit. You can put any other effect you want after scaleEffect to make it look how you want when pressed.
I had to do this for an app I am building, so just wanted to share. Refer code at the bottom, it is relatively self explanatory and sticks within the main elements of SwiftUI.
The main differences between this answer and the ones above is that this allows for updating the button's background color depending on state and also covers the use case of wanting the action of the long press to occur once the finger is lifted and not when the time threshold is passed.
As noted by others, I was unable to directly apply gestures to the Button and had to apply them to the Text View inside it. This has the unfortunate side-effect of reducing the 'hitbox' of the button, if I pressed near the edges of the button, the gesture would not fire. Accordingly I removed the Button and focused on manipulating my Text View object directly (this can be replaced with Image View, or other views (but not Button!)).
The below code sets up three gestures:
A LongPressGesture that fires immediately and reflects the 'tap' gesture in your question (I haven't tested but this may be able to replaced with the TapGesture)
Another LongPressGesture that has a minimum duration of 0.25 and reflect the 'long press' gesture in your question
A drag gesture with minimum distance of 0 to allow us to do events at the end of our fingers lifting from the button and not automatically at 0.25 seconds (you can remove this if this is not your use case). You can read more about this here: How do you detect a SwiftUI touchDown event with no movement or duration?
We sequence the gestures as follows: Use 'Exclusively' to combine the "Long Press" (i.e. 2 & 3 above combined) and Tap (first gesture above), and if the 0.25 second threshold for "Long Press" is not reached, the tap gesture is executed. The "Long Press" itself is a sequence of our long press gesture and our drag gesture so that the action is only performed once our finger is lifted up.
I also added code in the below for updating the button's colours depending on the state. One small thing to note is that I had to add code on the button's colour into the onEnded parts of the long press and drag gesture because the minuscule processing time would unfortunately result in the button switching back to darkButton colour between the longPressGesture and the DragGesture (which should not happen theoretically, unless I have a bug somewhere!).
You can read more here about Gestures: https://developer.apple.com/documentation/swiftui/gestures/composing_swiftui_gestures
If you modify the below and pay attention to Apple's notes on Gestures (also this answer was useful reading: How to fire event handler when the user STOPS a Long Press Gesture in SwiftUI?) you should be able to set up complex customised button interactions. Use the gestures as building blocks and combine them to remove any deficiency within individual gestures (e.g. longPressGesture does not have an option to do the events at its end and not when the condition is reached).
P.S. I have a global environment object 'dataRouter' (which is unrelated to the question, and just how I choose to share parameters across my swift views), which you can safely edit out.
struct AdvanceButton: View {
#EnvironmentObject var dataRouter: DataRouter
#State var width: CGFloat
#State var height: CGFloat
#State var bgColor: Color
#GestureState var longPress = false
#GestureState var longDrag = false
var body: some View {
let longPressGestureDelay = DragGesture(minimumDistance: 0)
.updating($longDrag) { currentstate, gestureState, transaction in
gestureState = true
}
.onEnded { value in
print(value.translation) // We can use value.translation to see how far away our finger moved and accordingly cancel the action (code not shown here)
print("long press action goes here")
self.bgColor = self.dataRouter.darkButton
}
let shortPressGesture = LongPressGesture(minimumDuration: 0)
.onEnded { _ in
print("short press goes here")
}
let longTapGesture = LongPressGesture(minimumDuration: 0.25)
.updating($longPress) { currentstate, gestureState, transaction in
gestureState = true
}
.onEnded { _ in
self.bgColor = self.dataRouter.lightButton
}
let tapBeforeLongGestures = longTapGesture.sequenced(before:longPressGestureDelay).exclusively(before: shortPressGesture)
return
Text("9")
.font(self.dataRouter.fontStyle)
.foregroundColor(self.dataRouter.darkButtonText)
.frame(width: width, height: height)
.background(self.longPress ? self.dataRouter.lightButton : (self.longDrag ? self.dataRouter.brightButton : self.bgColor))
.cornerRadius(15)
.gesture(tapBeforeLongGestures)
}
}
This isn't tested, but you can try to add a LongPressGesture to your button.
It'll presumably look something like this.
struct ContentView: View {
#GestureState var isLongPressed = false
var body: some View {
let longPress = LongPressGesture()
.updating($isLongPressed) { value, state, transaction in
state = value
}
return Button(/*...*/)
.gesture(longPress)
}
}
Kevin's answer was the closest to what I needed. Since ordering the longPressGesture before the tapGesture broke ScrollViews for me, but the inverse made the minimumDuration parameter do nothing, I implemented the long press functionality myself:
struct TapAndLongPressModifier: ViewModifier {
#State private var canTap = false
#State private var pressId = 0
let tapAction: (()->())
let longPressAction: (()->())
var minimumDuration = 1.0
func body(content: Content) -> some View {
content
.onTapGesture {
if canTap {
tapAction()
}
}
.onLongPressGesture(
minimumDuration: 1.0,
pressing: { (isPressing) in
pressId += 1
canTap = isPressing
if isPressing {
let thisId = pressId
DispatchQueue.main.asyncAfter(deadline: .now() + minimumDuration) {
if thisId == pressId {
canTap = false
longPressAction()
}
}
}
},
// We won't actually use this
perform: {}
)
}
}
just do this:
the first modifier should be onLongPressGesture(minumumDuration: (the duration you want))
and the following mondifier should be onLongPressGesture(minimumDuration: 0.01) <- or some other super small numbers
this works for me perfectly
As a follow up, I had the same issue and I tried all of these answers but didn't like how they all worked.
I ended up using a .contextMenu it was way easier and produces pretty much the same effect.
Check link here
and here is an example
UPDATE:
As of iOS 16, .contextMenu has been depreciated. So I ended up using .simultaneousGesture on the Button, not the content in the button's label block.
i.e.
Button {
// handle Button Tap
} label: {
// button label content here
}
.simultaneousGesture(LongPressGesture()
.onEnded { _ in
// handle long press here
}
)
This still preserves the button animations as well.
Note tested before iOS 16 however.
Thought I'd post back on this, in case anyone else is struggling. Strange that Apple's default behaviour works on most controls but not buttons. In my case I wanted to keep button effects while supporting long press.
An approach that works without too much complexity is to ignore the default button action and create a simultaneous gesture that handles both normal and long clicks.
In your view you can apply a custom long press modifier like this:
var body: some View {
// Apply the modifier
Button(action: self.onReloadDefaultAction) {
Text("Reload")
}
.modifier(LongPressModifier(
isDisabled: self.sessionButtonsDisabled,
completionHandler: self.onReloadPressed))
}
// Ignore the default click
private func onReloadDefaultAction() {
}
// Handle the simultaneous gesture
private func onReloadPressed(isLongPress: Bool) {
// Do the work here
}
My long press modifier implementation looked like this and uses the drag gesture that I found from another post. Not very intuitive but it works reliably, though of course I would prefer not to have to code this plumbing myself.
struct LongPressModifier: ViewModifier {
// Mutable state
#State private var startTime: Date?
// Properties
private let isDisabled: Bool
private let longPressSeconds: Double
private let completionHandler: (Bool) -> Void
// Initialise long press behaviour to 2 seconds
init(isDisabled: Bool, completionHandler: #escaping (Bool) -> Void) {
self.isDisabled = isDisabled
self.longPressSeconds = 2.0
self.completionHandler = completionHandler
}
// Capture the start and end times
func body(content: Content) -> some View {
content.simultaneousGesture(DragGesture(minimumDistance: 0)
.onChanged { _ in
if self.isDisabled {
return
}
// Record the start time at the time we are clicked
if self.startTime == nil {
self.startTime = Date()
}
}
.onEnded { _ in
if self.isDisabled {
return
}
// Measure the time elapsed and reset
let endTime = Date()
let interval = self.startTime!.distance(to: endTime)
self.startTime = nil
// Return a boolean indicating whether a normal or long press
let isLongPress = !interval.isLess(than: self.longPressSeconds)
self.completionHandler(isLongPress)
})
}
}
Try this :)
Handles isInactive, isPressing, isLongPress and Tap(Click)
based on this
I tried to make this as a viewmodifier without success. I would like to see an example with #GestureState variable wrapper used in same manner as #State/#Published are bound to #Binding in view components.
Tested: Xcode 12.0 beta, macOS Big Sur 11.0 beta
import SwiftUI
enum PressState {
case inactive
case pressing
case longPress
var isPressing: Bool {
switch self {
case .inactive:
return false
case .pressing, .longPress:
return true
}
}
var isLongPress: Bool {
switch self {
case .inactive, .pressing:
return false
case .longPress:
return true
}
}
var isInactive : Bool {
switch self {
case .inactive:
return true
case .pressing, .longPress:
return false
}
}
}
struct ContentView: View {
#GestureState private var pressState: PressState = PressState.inactive
#State var showClick: Bool = false
var press: some Gesture {
LongPressGesture(minimumDuration: 0.8, maximumDistance: 50.0)
.sequenced(before: LongPressGesture(minimumDuration: .infinity, maximumDistance: 50.0))
.updating($pressState) { value, state, transaction in
switch value {
case .first(true): // first gesture starts
state = PressState.pressing
case .second(true, nil): // first ends, second starts
state = PressState.longPress
default: break
}
}
}
var body: some View {
ZStack{
Group {
Text("Click")
.offset(x: 0, y: pressState.isPressing ? (pressState.isLongPress ? -120 : -100) : -40)
.animation(Animation.linear(duration: 0.5))
.opacity(showClick ? 1 : 0 )
.animation(Animation.linear(duration: 0.3))
Text("Pressing")
.opacity(pressState.isPressing ? 1 : 0 )
.offset(x: 0, y: pressState.isPressing ? (pressState.isLongPress ? -100 : -80) : -20)
.animation(Animation.linear(duration: 0.5))
Text("Long press")
.opacity(pressState.isLongPress ? 1 : 0 )
.offset(x: 0, y: pressState.isLongPress ? -80 : 0)
.animation(Animation.linear(duration: 0.5))
}
Group{
Image(systemName: pressState.isLongPress ? "face.smiling.fill" : (pressState.isPressing ? "circle.fill" : "circle"))
.offset(x: 0, y: -100)
.font(.system(size: 60))
.opacity(pressState.isLongPress ? 1 : (pressState.isPressing ? 0.6 : 0.2))
.foregroundColor(pressState.isLongPress ? .orange : (pressState.isPressing ? .yellow : .white))
.rotationEffect(.degrees(pressState.isLongPress ? 360 : 0), anchor: .center)
.animation(Animation.linear(duration: 1))
Button(action: {
showClick = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: {
self.showClick = false
})
}, label: {
ZStack {
Circle()
.fill(self.pressState.isPressing ? Color.blue : Color.orange)
.frame(width: 100, height: 100, alignment: .center)
Text("touch me")
}}).simultaneousGesture(press)
}.offset(x: 0, y: 110)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Here is a demo of the problem on tryflow
Essentially I have a class that operates on an array of gerically typed items.
type Props<ItemType> = {
items: ItemType[],
onSelect: (item: ItemType) => void
}
class List<ItemType> {
props: Props<ItemType>
activeIndex: number
constructor(props: Props<ItemType>) {
this.props = props;
this.activeIndex = 0;
}
getActiveItem() : ?ItemType {
return this.props.items[this.activeIndex];
}
submitItem(item: ?ItemType){
if(item) {
this.props.onSelect(item)
}
}
onClick() {
this.submitItem(this.getActiveItem())
}
}
let numbers: number[] = [1,2,3];
let onSelect = (value: number) => {};
let numberList: List<number> = new List({ items: numbers, onSelect: onSelect})
This example comes from a react component that I stripped down to more clearly demonstrate the problem.
It mostly works but ran into problems when I converted submitItem() to a bound method:
submitItem = (item: ?ItemType) => {
if(item) {
this.props.onSelect(item)
}
}
This causes the following error:
27: this.submitItem(this.getActiveItem())
^ Cannot call `this.submitItem` with `this.getActiveItem()` bound to `item` because `ItemType` [1] is incompatible with `ItemType` [2].
References:
8: class List<ItemType> {
^ [1]
20: submitItem = (item: ?ItemType) => {
^ [2]
The method needs to be bound to the class because it will be triggered as a callback from a DOM event.
How do I get the bound method to understand the generic type.
There appears to a problem there with property initialiser syntax (the recommended method of ensuring the method is bound to this) not creating the same type signature as when a regular class property is used. I've raised an issue with a simplified example.
However, in your example you don't seem to need to do this, as your onClick method is the one that needs to be bound and passed as the event handler
// ...
submitItem(item: ?ItemType): void {
if(item) {
this.props.onSelect(item)
}
}
onClick = () => {
this.submitItem(this.getActiveItem())
}
// ...