I want to create a contact form where the form fields are changed after they are filled.
For example when you fill the email field, you press enter or click the Next button it disappears and the next field appears. There will be different form field types (text, email, select, etc).
What i want to achieve is based on this form, but it is coded in vanilla js and i want to write it in React from scratch.
For now my basic code is this:
ContactForm.js file:
import { useState, useEffect } from "react";
import Fields from "./Fields";
const ContactForm = ({ fields }) => {
const [current, setCurrent] = useState(0);
const [show, setShow] = useState(true);
return (
<div className='container'>
<div className='fs-form-wrap' id='fs-form-wrap'>
<Fields fields={fields} isCurrent={current} />
<button onClick={() => setCurrent(current + 1)}>Continue</button>
</div>
</div>
);
};
Fields.js file:
import { useState } from "react";
import Field from "./Field";
const Fields = ({ fields, isCurrent }) => {
return (
<form id='myform' className='fs-form fs-form-full'>
<ol className='fs-fields'>
{fields.map((field, index) => {
return (
<Field
placeHolder={field.placeholder}
title={field.title}
type={field.type}
key={index}
isCurrent={isCurrent}
/>
);
})}
</ol>
</form>
);
};
export default Fields;
Field.js file:
import { useState, useEffect } from "react";
import styled from "styled-components";
const Field = ({ placeHolder, type, title, isCurrent }) => {
return (
<TheField isCurrent={isCurrent}>
<label className='fs-field-label fs-anim-upper' htmlFor='q2'>
{title}
</label>
<input
className='fs-anim-lower'
id='q2'
name='q2'
type={type}
placeholder={placeHolder}
required
/>
</TheField>
);
};
export default Field;
const TheField = styled.li`
visibility: ${(props) => (props.isCurrent === 0 ? "visible" : "hidden")};
`;
Based on this code, initially i get my 2 fields which are coming from my dummy-data.json file but when i click on the button, both of them disappear.
I know that my code is still a mess, but i first want to make them appear one by one, then i think i know the logic for the other parts.
Any help would be appreciated.
EDIT with the solution from #zergski below:
import { useState, useEffect } from "react";
import Field from "./Field";
import { BidirectionalIterator } from "./Iterator";
const ContactForm = ({ fields }) => {
const [current, setCurrent] = useState(0);
const [show, setShow] = useState(true);
const options = { startIndex: 0, loop: false, clamp: true };
const list = new BidirectionalIterator(fields, options);
return (
<div className='container'>
<div className='fs-form-wrap' id='fs-form-wrap'>
<form id='myform' className='fs-form fs-form-full'>
<ol className='fs-fields'>
<li>{list}</li>
{/* {fields.map((field, index) => {
return (
<Field
placeHolder={field.placeholder}
title={field.title}
type={field.type}
key={index}
/>
);
})} */}
</ol>
{/* <Submit clickHandler={submitClickHandler} text={submitText} /> */}
</form>
<button onClick={() => list.next()}>Continue</button>
</div>
</div>
);
};
you need to create your own iterator... either through the use of generators or a custom class.. here's one I've written
export class BidirectionalIterator {
// TODO: support for other data types
// #iterate_over: string = 'index'
static index: number = 0
static start_index: number = 0
static looped: boolean = false
static clamped: boolean = true
static data: PropertyKey[]
static ct: number = 0
static data_len: number = 0
/** Only supports iterables for now.
* #param data - the object to be iterated
* #param options.startIndex - A negative value of less than 0 sets the index at the end of the iterable
* #param options.loop - loop to the opposite end of iterable (overrides the clamp option setting)
* #param options.clamp - iterator won't finish upon reaching iterable bounds
*
* #caution - DO NOT use a for of/in loop on the iterator if either the loop option is set to true!
*/
constructor(data: any[] = [], { startIndex = 0, loop = false, clamp = true }: BidirectionalIteratorOptions = {}) {
BidirectionalIterator.setData(data)
BidirectionalIterator.start_index = startIndex
BidirectionalIterator.clamped = loop ? false : clamp
BidirectionalIterator.looped = loop
}
static [Symbol.iterator]() {
return this
}
static setData(data: any) {
this.data = data
// TODO: finish support for different collection types
let [ct, data_len] =
data instanceof Array ? [1, data.length]
: data instanceof Map ? [2, data.size]
: data instanceof Set ? [3, data.size]
: [4, -1]
this.ct = ct
this.data_len = data_len
this.resetIndex()
}
static resetIndex() {
this.setIndex(this.start_index < -1 ? this.len : this.start_index)
}
static setIndex(idx: number) {
this.index = idx - 1
}
static get len(): number {
return this.data.length
}
// ! well I'm definitely repeating myself....
static get entry() {
this.index = num_between(this.index, 0, this.len - 1)
return {
index: this.index,
value: this.data[this.index]
}
}
static get next_entry() {
this.index = num_between(this.index, 0, this.len - 1)
return {
index: this.index + 1,
value: this.data[this.index] || (this.looped ? this.data[0] : null)
}
}
static get prev_entry() {
this.index = num_between(this.index, 0, this.len - 1)
return {
index: this.index - 1,
value: this.data[this.index + 1] || (this.looped ? this.data[this.len - 1] : null)
}
}
static next() {
let value, done
(done = this.index >= this.len)
? this.index = this.len
: done = ++this.index >= this.len
// value = this.data[done ? this.len-1 : this.index]
value = this.data[num_between(this.index, 0, this.len)]
if (done)
this.looped
? value = this.data[this.index = 0]
: this.clamped
? value = this.data[this.len - 1] : null
return {
index: this.index,
value,
done
}
}
static prev() {
let value, done
(done = this.index <= -1)
? this.index = -1
: done = --this.index <= -1
// value = this.data[done ? 0 : this.index]
value = this.data[num_between(this.index, 0, this.len)]
if (done)
this.looped
? value = this.data[this.len - 1]
: this.clamped
? value = this.data[0] : null
return {
index: this.index,
value,
done
}
}
}
so to use it just instantiate the class..
const list = new BidirectionalIterator(data_array, options)
// and use with .next() & .prev() methods on mouse input
// e.g this will return the next entry in given array
list.next()
it's written in typescript though, so you need to remove all the type declarations
Related
I'm currently trying to implement a chess variant, it is played on a 10x8 board instead of the normal 8x8. I'm doing it in React and using the DnD library, but I don't know how to implement the drop functionality. I'm not using the react chess library for this project. I can drag my piece as of now but it returns to its original position after I let go and doesn't drop.
My approach is to have a component for a chessboard, tile, and piece component.
So I am able to drag and piece and my console log shows the id and position that I created in my piece component useDrag hook. I've already started implement my useDrop hook in my tile class but my big issue is figuring out where the coordinates are when I drop my piece. How can I get that info? Once I have that I assume I can pass this it down as props to my piece and update the position like that. I've tried reading the docs but honestly I don't really understand it.
chess board component
import './Chessboard.css';
import Tile from '../Tile/Tile';
const verticalAxis = ["1","2","3","4","5","6","7","8"];
const horizonalAxis = ["a","b","c","d","e","f","g","h","i","j"];
const ChessBoard = ()=>{
const getPosition = () => {
//Don't know what to put here
}
let board = [];
let pieces = {1:"white_pawn", 6: "black_pawn", P03: "white_queen", P04: "white_empress", P05: "white_king", P06: "white_princess",
P73: "black_queen", P74: "black_empress", P75: "black_king", P76: "black_princess"};
pieces.P00 = pieces.P09 = "white_rook";
pieces.P01 = pieces.P08 = "white_knight";
pieces.P02 = pieces.P07 = "white_bishop";
pieces.P70 = pieces.P79 = "black_rook";
pieces.P71 = pieces.P78 = "black_knight";
pieces.P72 = pieces.P77 = "black_bishop";
for(let j = verticalAxis.length - 1; j >= 0; j--){
let row = [];
for(let i = 0; i < horizonalAxis.length; i++){
let type = undefined;
if(j === 1 || j === 6){
type = pieces[j];
}else{
type = pieces[`P${j}${i}`];
}
let color = (i + j + 2) % 2 === 0 ? "tile white-tile" : "tile black-tile";
row.push(<Tile key={`${j},${i}`} id={`${j},${i}`} type={type} color={color} position={`${j}${i}`}/>);
}
board.push(row);
}
return(
<div id="chessboard">{board}</div>
)
}
export default ChessBoard;
Tile Component
import './Tile.css';
import Piece from '../Piece/Piece';
import { useDrop } from 'react-dnd';
const Tile = ({color, type, id, position})=>{
const [{ isOver }, drop] = useDrop({
accept: "piece",
drop: (item) => console.log(item),
collect: (monitor) => ({
isOver: !!monitor.isOver(),
}),
});
const movePiece = (from, to) => {
}
return(
<div className={color} ref={drop}>
{type ? <Piece type={type} id={id} position={position}/> : ""}
</div>
)
};
export default Tile;
Piece component
import './Piece.css';
import { useDrag } from 'react-dnd';
const Piece = ({type, id, position}) =>{
const pieceImage = require(`../../assets/${type}.png`);
const [{isDragging}, drag] = useDrag(() => ({
type: "piece",
item: {id: type, position: position},
collect: (monitor) => ({
isDragging: !!monitor.isDragging(),
})
}));
const handleClick = e => {
console.log(e.currentTarget.id);
}
return(
<div className="chess-piece" id={id} ref={drag} onClick={handleClick} style={{opacity: isDragging ? 0 : 1}}>
<img src={pieceImage}/>
</div>
)
};
export default Piece;
I want my function to roll 5 dices and store them in my dices state.
In the recent version, i am only storing one object for the next click, but i made the for loop to generate 5 dices at once, how can i store the previous dce object(s) for the next iteration in the for loop?
import './App.css';
import Subgroup from './Subgroup'
import React, {useState} from 'react'
function App() {
const [numberRolls, setNumberRolls] = useState(3);
const [dices, setDices] = useState([]);
return (
<div className="App">
<Subgroup
numberRolls = {numberRolls}
setNumberRolls = {setNumberRolls}
dices = {dices}
setDices = {setDices}
/>
</div>
);
}
export default App;
import React from 'react'
const Subgroup = ({numberRolls, setNumberRolls, dices, setDices}) => {
const rollTheDice = () => {
if(numberRolls > 0) {
setNumberRolls(numberRolls - 1);
for(let i = 0; i < 5; i++) {
setDices([...dices, // i think sth should be different here????
{id: Math.random()*100,
number: Math.ceil(Math.random()*6)-1,
selected: false}
])
}
}
console.log(dices)
}
return (
<div>
<button onClick={ rollTheDice }>blablabala </button>
</div>
)
}
export default Subgroup;
Try saving the result in an array, then set the state, something like:
const result = [];
for(let i = 0; i < 5; i++) {
result.push({
id: Math.random() * 100,
number: Math.ceil(Math.random() * 6) -1,
selected: false
});
}
setDices(prev => [...prev, ...result]);
I am using React map and I am getting the following error
TypeError: Cannot read property 'PreviewThemeSideBar' of undefined
I don't understand in any way why I get undefined because I checked the imported component where I pass props and there I get all the data, see
SeleceColorsTheme.js
export default function SelectColorsTheme(props) {
const groupSize = 3;
const [selectedIndex, setSelectedIndex] = useState(false);
const setBorder = (index) => {
setSelectedIndex(index);
};
const rows = SideBarColors.map(function (col, index) {
const selectBorder = classNames({
'builtin_theme_preview': true,
'selectBorder': index === selectedIndex ? 'selectBorder' : null
});
// map content to html elements
return <SelectThemeContent {...props} selectBorder={selectBorder}
col={col} setBorder={setBorder} index={index}/>
}).reduce(function (r, element, index) {
// create element groups with size 3, result looks like:
// [[elem1, elem2, elem3], [elem4, elem5, elem6], ...]
index % groupSize === 0 && r.push([]);
r[r.length - 1].push(element);
return r;
}, []).map(function (rowContent) {
// surround every group with 'row'
return <div className="SelectThemePictures_Separator">{rowContent}</div>;
});
return <div className="container">{rows}</div>;
};
SelectThemeContent.js
export default function SelectThemeContent(props) {
const selectBorder = props.selectBorder;
const col = props.col;
const setBorder = props.setBorder;
const index = props.index;
return(
<div className={selectBorder} key={index} onClick={() => props.SideBarPageContent(col) || setBorder(index)}>
<div style={{ background: col.PreviewThemeSideBar }} className="builtin_theme_preview__nav">
<div className="builtin_theme_preview__search" />
...
</div>
</div>
);
}
I just added a check condition to each props, example
<div style={{ background: col && col.PreviewThemeSideBar }}</div>
Hello I had an idea to make a hook to increase the font size and save preferences in localStorage
basically I have a state that goes from 1 to 4, and then when I click the button add I add +1 to the state until I reach number 4
and on the remove button I remove 1 from the state until 1
But I have doubts on how to save this to my location
basically if i don't use my useState just with getInitialValue It works normally.
like this gif, If I add the value manually it works:
but if I try to use my setFont I have problems (as it is saved in localStorage):
and i got this on localStorage :
code:
export default function App() {
const { fontSize, setSize } = useFontSize();
console.log(fontSize);
return (
<div className="App">
<button
onClick={() => {
setSize(fontSize + 1);
}}
>
add
</button>
<button
onClick={() => {
setSize(fontSize + 1);
}}
>
remove
</button>
</div>
);
}
hook:
export default function useFontSize(defaultSize = { size: 1 }) {
const [fontSize, _setSize] = useState(getInitialSize);
function getInitialSize() {
const savedSize = localStorage.getItem('_size_acessibility_font');
const parsedSize = JSON.parse(savedSize);
if (parsedSize) {
const { size } = parsedSize;
if (size >= 1 && size <= 4) {
return size;
}
} else {
return defaultSize.size;
}
}
useEffect(() => {
console.log(fontSize, 'on useEffect to set on localStorage');
localStorage.setItem(
'_size_acessibility_font',
JSON.stringify({ size: fontSize }),
);
}, [fontSize]);
return {
fontSize,
setSize: ({ setSize, ...size }) => {
console.log(size, 'on function set size');
if (size > 4) {
return _setSize(4);
}
if (size < 1) {
return _setSize(1);
}
return _setSize(size);
},
};
}
example:
https://codesandbox.io/s/focused-newton-x0mqd
I don't know if this is the best logic for this context, if someone can help me.
This seems a tad overengineered and upsets a few hooks idioms. For example, returning a named object pair for a hook is less typical than an array pair. The set function itself is complex and returns the result of the _setSize calls. Naming could be clearer if fontSize matched setSize by using setFontSize.
({ setSize, ...size }) is problematic since the caller is (correctly) providing an integer.
Here's a minimal, complete version that fixes these issues (local storage is mocked since Stack Snippets is sandboxed):
const localStorageMock = (() => {
const storage = {};
return {
getItem: k => storage[k],
setItem: (k, v) => {storage[k] = v.toString();}
};
})();
const {useState, useEffect} = React;
const useFontSize = (defaultSize=1) => {
const clamp = (n, lo=1, hi=4) => Math.min(hi, Math.max(n, lo));
const clean = n => isNaN(n) ? defaultSize : clamp(+n);
const storageName = "_size_acessibility_font";
const fromStorage = clean(localStorageMock.getItem(storageName));
const [fontSize, setFontSize] = useState(fromStorage);
useEffect(() => {
localStorageMock.setItem(storageName, fontSize);
}, [fontSize]);
return [fontSize, size => setFontSize(clean(size))];
};
const App = () => {
const [fontSize, setFontSize] = useFontSize();
return (
<div>
<div>Font size: {fontSize}</div>
<button onClick={() => setFontSize(fontSize + 1)}>
+
</button>
<button onClick={() => setFontSize(fontSize - 1)}>
-
</button>
</div>
);
};
ReactDOM.createRoot(document.querySelector("#app"))
.render(<App />);
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="app"></div>
In useFontSize, you return
return {
fontSize,
setSize: ({ setSize, ...size }) => {
console.log(size, 'on function set size');
if (size > 4) {
return _setSize(4);
}
if (size < 1) {
return _setSize(1);
}
return _setSize(size);
},
};
However, in App, you call setSize with just a number setSize(fontSize + 1); when it is expecting an object.
If you change useFontSize to return
return {
fontSize,
setSize: (size) => {
console.log(size, 'on function set size');
if (size > 4) {
return _setSize(4);
}
if (size < 1) {
return _setSize(1);
}
return _setSize(size);
},
};
It should work.
Note, you will want to clear your current local storage, or add some error checking.
Also note, although it is just an example, both add and remove use fontSize + 1
I'm recreating this module in my app. I'm using AntDesign.
But I want to have a duplicate function of it and getting the values that has been filled also.
Here's my code but it's not working
Link
import React from "react";
import ReactDOM from "react-dom";
import "antd/dist/antd.css";
import "./index.css";
import { Form, Input, Icon, Button } from "antd";
let id = 0;
class DynamicFieldSet extends React.Component {
remove = k => {
const { form } = this.props;
// can use data-binding to get
const keys = form.getFieldValue("keys");
// We need at least one passenger
if (keys.length === 1) {
return;
}
// can use data-binding to set
form.setFieldsValue({
keys: keys.filter(key => key !== k)
});
};
add = () => {
const { form } = this.props;
// can use data-binding to get
const keys = form.getFieldValue("keys");
const nextKeys = keys.concat(id++);
// can use data-binding to set
// important! notify form to detect changes
form.setFieldsValue({
keys: nextKeys
});
};
duplicate = k => {
const { form } = this.props;
const keys = form.getFieldValue("keys");
form.setFieldsValue({
keys: keys.find(key => key === k)
});
};
handleSubmit = e => {
e.preventDefault();
this.props.form.validateFields((err, values) => {
if (!err) {
const { keys, names } = values;
console.log("Received values of form: ", values);
console.log("Merged values:", keys.map(key => names[key]));
}
});
};
render() {
const { getFieldDecorator, getFieldValue } = this.props.form;
const formItemLayout = {
labelCol: {
xs: { span: 24 },
sm: { span: 4 }
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 20 }
}
};
const formItemLayoutWithOutLabel = {
wrapperCol: {
xs: { span: 24, offset: 0 },
sm: { span: 20, offset: 4 }
}
};
getFieldDecorator("keys", { initialValue: [] });
const keys = getFieldValue("keys");
const formItems = keys.map((k, index) => (
<Form.Item
{...(index === 0 ? formItemLayout : formItemLayoutWithOutLabel)}
label={index === 0 ? "Passengers" : ""}
required={false}
key={k}
>
{getFieldDecorator(`names[${k}]`, {
validateTrigger: ["onChange", "onBlur"],
rules: [
{
required: true,
whitespace: true,
message: "Please input passenger's name or delete this field."
}
]
})(
<Input
placeholder="passenger name"
style={{ width: "60%", marginRight: 8 }}
/>
)}
{keys.length > 1 ? (
<div>
<Icon
className="dynamic-delete-button"
type="minus-circle-o"
onClick={() => this.remove(k)}
/>
<Icon
className="dynamic-delete-button"
type="copy"
onClick={() => this.duplicate(k)}
/>
</div>
) : null}
</Form.Item>
));
return (
<Form onSubmit={this.handleSubmit}>
{formItems}
<Form.Item {...formItemLayoutWithOutLabel}>
<Button type="dashed" onClick={this.add} style={{ width: "60%" }}>
<Icon type="plus" /> Add field
</Button>
</Form.Item>
<Form.Item {...formItemLayoutWithOutLabel}>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
}
}
const WrappedDynamicFieldSet = Form.create({ name: "dynamic_form_item" })(
DynamicFieldSet
);
ReactDOM.render(
<WrappedDynamicFieldSet />,
document.getElementById("container")
);
In your duplicate method you get the key or index of the field that was clicked. The keys array in form values tracks the number of fields you have and their indices. The names array in form values tracks the value for each of those fields. In your duplicate method you need to add one field by adding to keys array then set value for that field by adding to names array. See the method below:
Note: In your add method use keys.length instead of id to increment. This will always keep all keys unique
add = () => {
const { form } = this.props
// can use data-binding to get
const keys = form.getFieldValue("keys")
const nextKeys = keys.concat(keys.length)
// can use data-binding to set
// important! notify form to detect changes
form.setFieldsValue({
keys: nextKeys
})
}
duplicate = k => {
const { form } = this.props
// We are using keys to track number of fields
// and using names to track the value of each field
const { keys, names } = form.getFieldsValue()
// We can use the key to access the value of the field for
// for which the button was clicked
const nameToDuplicate = names[k]
/**
* Add a key to 'keys' array in antd form to render extra field
* Once the field is rendered we can add the name to the
* 'names' array of antd form.
*
* Note: We add keys first and then add name in callback method because
* we cannot set a value for a form field before rendering it.
*/
const newKeys = [...keys, keys.length]
form.setFieldsValue({ keys: newKeys }, () => {
const newNames = [...names, nameToDuplicate]
form.setFieldsValue({ names: newNames })
})
}
Sandbox Link