Skip to content

Instantly share code, notes, and snippets.

@steniowagner
Last active January 13, 2022 04:20
Show Gist options
  • Save steniowagner/e1cb6e8ff2595f3410554b10df9714fa to your computer and use it in GitHub Desktop.
Save steniowagner/e1cb6e8ff2595f3410554b10df9714fa to your computer and use it in GitHub Desktop.
Simple code to perform "swipe-actions" with flatlist in react-native
import React, { useState } from "react";
import LoginComponent from "./LoginComponent";
const usersTest = Array(8)
.fill({ name: "", cns: "" })
.map((_, index) => ({
name: `User ${index}`,
cns: `123.456.${index}`,
}));
const Login: React.FC = () => {
const [usersSelected, setUsersSelected] = useState<Array<string>>([]);
const [users, setUsers] = useState(usersTest);
const onSwipe = (userId: string) => {
const isUserExistsSelectedList = usersSelected.includes(userId);
if (isUserExistsSelectedList) {
return;
}
setUsersSelected(previousUsersSelected => [
...previousUsersSelected,
userId,
]);
};
const onUnswipe = (userId: string) => {
const isUserExistsSelectedList = usersSelected.includes(userId);
if (!isUserExistsSelectedList) {
return;
}
setUsersSelected(previousUsersSelected =>
previousUsersSelected.filter(
previousUserSelected => previousUserSelected !== userId
)
);
};
const onRemoveUser = (idUserToRemove: string) => {
setUsers(prevUsers =>
prevUsers.filter(prevUser => prevUser.cns !== idUserToRemove)
);
onUnswipe(idUserToRemove);
};
return (
<LoginComponent
usersSelected={usersSelected}
onRemoveUser={onRemoveUser}
onUnswipe={onUnswipe}
onSwipe={onSwipe}
users={users}
/>
);
};
export default Login;
import React from "react";
import { FlatList, View } from "react-native";
import styled from "styled-components";
import SwipeListItem from "./SwipeListItem";
import UsersListItem from "./UsersListItem";
const Container = styled(View)`
width: ${({ theme }) => theme.metrics.width}px;
height: 100%;
justify-content: space-between;
background-color: ${({ theme }) => theme.colors.white};
`;
const Divider = styled(View)`
width: 100%;
height: ${({ theme }) => theme.metrics.smallSize}px;
`;
interface Props {
onRemoveUser: (userSelectedId: string) => void;
onUnswipe: (cns: string) => void;
onSwipe: (cns: string) => void;
usersSelected: Array<string>;
users: Array<any>;
}
const LoginComponent: React.FC<Props> = ({
usersSelected,
onRemoveUser,
onUnswipe,
onSwipe,
users,
}: Props) => {
const swipeOptions = [
{
color: "#20BCB5",
icon: "❌",
},
{
color: "#0647A6",
icon: "✏️",
},
];
return (
<Container>
<FlatList
renderItem={({ item }) => (
<SwipeListItem
background="#bbb"
updateRule={usersSelected.includes(item.cns)}
onUnswipe={() => onUnswipe(item.cns)}
onSwipe={() => onSwipe(item.cns)}
border={6}
options={swipeOptions.map((swipeOption, optionIndex) => {
let action;
if (optionIndex === 0) {
action = () => onRemoveUser(item.cns);
}
if (optionIndex === 1) {
action = () => console.warn("Edit: ", item);
}
return {
...swipeOption,
action,
};
})}
autoclose
>
<UsersListItem
name={item.name}
cns={item.cns}
/>
</SwipeListItem>
)}
contentContainerStyle={{
padding: 14,
}}
ItemSeparatorComponent={() => <Divider />}
keyExtractor={item => item.cns}
data={users}
/>
</Container>
);
};
export default LoginComponent;
import React, { memo, useCallback, useState, useRef } from "react";
import {
LayoutChangeEvent,
LayoutAnimation,
PanResponder,
UIManager,
Animated,
View,
} from "react-native";
import styled from "styled-components";
import SwipeOptionButton from "./SwipeOptionButton";
const Wrapper = styled(View)`
background-color: ${({ background }) => background};
border-radius: ${({ border }) => border}px;
`;
const OptionsWrapper = styled(View)`
height: 100%;
flex-direction: row;
position: absolute;
`;
interface OptionProps {
action: () => void;
color: string;
icon: string;
}
interface Props {
options: Array<OptionProps>;
children: JSX.Element;
onUnswipe: () => void;
onSwipe: () => void;
updateRule: boolean;
autoclose?: boolean;
background: string;
border?: number;
}
const AREA_OCCUPIED_OPTION = 0.2;
const MIN_PIXELS_TO_MOVE = -10;
const MAX_OPTIONS_ALLOWED = 3;
const shouldComponentUpdate = (prevProps: Props, nextProps: Props) => {
if (prevProps.updateRule !== nextProps.updateRule) {
return false;
}
return true;
};
const SwipeListItem: React.FC<Props> = memo<Props>(
({
background,
onUnswipe,
autoclose,
children,
onSwipe,
options,
border,
}: Props) => {
const [isContainerRefSet, setIsContainerRefSet] = useState(false);
const panRef = useRef(new Animated.ValueXY());
const containerRef = useRef(null);
if (UIManager.setLayoutAnimationEnabledExperimental) {
UIManager.setLayoutAnimationEnabledExperimental(true);
}
const animateWithSpring = (newX: number): void => {
const bounciness = newX === 0 ? 0 : 8;
Animated.spring(panRef.current, {
bounciness,
toValue: {
x: newX,
y: 0,
},
}).start();
};
const getMaxAreaOccupiedByOptions = (): number => {
if (!containerRef.current) {
return 0;
}
const { width } = containerRef.current;
const maxAreaOccupiedByOptions =
AREA_OCCUPIED_OPTION * options.length * width;
return maxAreaOccupiedByOptions;
};
const checkIsSwipingNegatively = (dx: number): boolean =>
(panRef.current.x as any)._value === 0 && dx < 0;
const onPanResponderRelease = (dx: number): void => {
if (checkIsSwipingNegatively(dx)) {
return;
}
const maxAreaOccupiedByOptions = getMaxAreaOccupiedByOptions();
const swipedEnoughToShowOptions = dx >= maxAreaOccupiedByOptions / 2;
const newX = swipedEnoughToShowOptions ? maxAreaOccupiedByOptions : 0;
if (swipedEnoughToShowOptions) {
onSwipe();
}
if (!swipedEnoughToShowOptions) {
onUnswipe();
}
animateWithSpring(newX);
};
const onPanResponderMove = (dx: number): void => {
if (checkIsSwipingNegatively(dx)) {
return;
}
const maxAreaOccupiedByOptions = getMaxAreaOccupiedByOptions();
const currentPosition = (panRef.current.x as any)._value;
const isSwipingBeoyndOptionsArea =
dx + currentPosition >= maxAreaOccupiedByOptions;
if (isSwipingBeoyndOptionsArea) {
const { width } = containerRef.current;
const newX = maxAreaOccupiedByOptions + width * 0.1;
animateWithSpring(newX);
return;
}
const isSwipedEnough = dx > Math.abs(MIN_PIXELS_TO_MOVE);
if (isSwipedEnough) {
panRef.current.setValue({ x: dx + MIN_PIXELS_TO_MOVE, y: 0 });
}
if (dx < 0) {
animateWithSpring(0);
}
};
const panResponder = PanResponder.create({
onPanResponderRelease: (_, { dx }) => onPanResponderRelease(dx),
onPanResponderMove: (_, { dx }) => onPanResponderMove(dx),
onPanResponderTerminationRequest: () => false,
onStartShouldSetPanResponder: () => false,
onMoveShouldSetPanResponder: () => true,
});
const onLayout = useCallback((event: LayoutChangeEvent): void => {
containerRef.current = event.nativeEvent.layout;
setIsContainerRefSet(true);
}, []);
const onPressSwipeOptionButton = ({ action }: OptionProps) => {
if (autoclose) {
animateWithSpring(0);
}
LayoutAnimation.configureNext(
LayoutAnimation.create(300, "linear", "opacity")
);
action();
};
const maxAreaOccupiedByOptions = getMaxAreaOccupiedByOptions();
return (
<Wrapper
background={background}
onLayout={onLayout}
border={border}>
<OptionsWrapper>
{isContainerRefSet && (
<>
{options
.slice(0, MAX_OPTIONS_ALLOWED)
.map((item, optionIndex) => (
<SwipeOptionButton
width={maxAreaOccupiedByOptions / options.length}
onPress={() => onPressSwipeOptionButton(item)}
key={String(optionIndex)}
index={optionIndex}
color={item.color}
icon={item.icon}
border={border}
/>
))}
</>
)}
</OptionsWrapper>
<Animated.View
// eslint-disable-next-line react/jsx-props-no-spreading
{...panResponder.panHandlers}
style={panRef.current.getLayout()}
>
{children}
</Animated.View>
</Wrapper>
);
},
shouldComponentUpdate
);
export default SwipeListItem;
import React from "react";
import { TouchableOpacity, View, Text } from "react-native";
import styled from "styled-components";
interface Style {
border?: number;
color?: string;
width: number;
index: number;
}
const Wrapper = styled(View)`
width: ${({ width }) => width}px;
height: 100%;
background-color: ${({ color }: Style) => color};
border-top-left-radius: ${({ index, border }: Style) =>
border && index === 0 ? border : 0}px;
border-bottom-left-radius: ${({ index, border }: Style) =>
border && index === 0 ? border : 0}px;
`;
const OptionButton = styled(TouchableOpacity)`
height: 100%;
justify-content: center;
align-items: center;
`;
interface Props {
onPress: () => void;
border?: number;
index: number;
width: number;
color: string;
icon: string;
}
const SwipeOptionButton: React.FC<Props> = ({
onPress,
border,
width,
index,
color,
icon,
}: Props) => (
<Wrapper border={border} index={index} width={width} color={color}>
<OptionButton onPress={onPress}>
<Text>{icon}</Text>
</OptionButton>
</Wrapper>
);
export default SwipeOptionButton;
import React from "react";
import { View, Text } from "react-native";
import styled from "styled-components";
const Wrapper = styled(View)`
width: 100%;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: ${({ theme }) => theme.metrics.largeSize}px;
background-color: ${({ theme }) => theme.colors.white};
border-radius: ${({ theme }) => theme.metrics.extraSmallSize}px;
`;
const TextContenWrapper = styled(View)`
width: 70%;
`;
const UserNameText = styled(Text).attrs({
numberOfLines: 1,
})`
font-family: CircularStd-Bold;
color: ${({ theme }) => theme.colors.textColor};
font-size: ${({ theme }) => theme.metrics.extraLargeSize};
`;
const CNSText = styled(Text).attrs({
numberOfLines: 1,
})`
font-family: CircularStd-Medium;
color: ${({ theme }) => theme.colors.secondaryTextColor};
font-size: ${({ theme }) => theme.metrics.largeSize * 1.2};
`;
type Props = {
name: string;
cns: string;
};
const UsersListItem = ({ name, cns }: Props) => (
<Wrapper
style={{
shadowColor: "#000",
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.25,
shadowRadius: 3.84,
elevation: 5,
}}
>
<TextContenWrapper>
<UserNameText>{name.toUpperCase()}</UserNameText>
<CNSText>{cns}</CNSText>
</TextContenWrapper>
</Wrapper>
);
export default UsersListItem;
@igor-nm
Copy link

igor-nm commented Oct 11, 2019

It's amazing feature dude, make a repository to share it with community!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment