Supertokens: Adding custom form fields with modifier function capabilities - javascript

I'm adding custom fields to the Sign Up form in Supertokens. I see there is a builtin validate function and you can basically have a true or false return on some conditional. I was wondering if there was a way to apply a "mask" to a phone number field which forces a format ie: (123)456-7890 something like an "onBlur()" or "onKeydown()" type of event. Here's my frontendConfig() code. (Snipped for brevity):
function inputMask(value) {
console.log("value: ", value);
const _num = value.replace(/\D/g, "").match(/(\d{3})(\d{3})(\d{4})/);
const phoneNumber = `(${_num[1]})${_num[2]}-${_num[3]}`;
return phoneNumber;
}
EmailPasswordReact.init({
signInAndUpFeature: {
signUpForm: {
formFields: [
{
id: "firstname",
label: "First Name",
placeholder: "Jane",
},
{
id: "lastname",
label: "Last Name",
placeholder: "Doe",
},
{
id: "phone",
label: "Phone",
placeholder: "(123)456-7890",
// as an example
validate: (value) => {
return inputMask(value);
},
},
],
},
},```

The validate function is only meant for checking if the input is correct or not. It's not meant for normalisation.
The SuperTokens react library render react components for the login / sign up UI, so you can use plain JS to change the form behaviour:
You want to add an event listener on the custom input field which fires on the blur event. In order to add this, you need to find that input field using JS and call addEventListener on it - this should be done in the useEffect block of the page that shows the sign up form.
When the event is fired, you want to call the inputMask function and change the value of the form field to be equal to that function's output.
You should also implement logic in the validate function to check that the user's input (normalised or not) is correct.
Finally, in case the input field blur event doesn't happen, then you want to normalise the user's input when the sign up button is clicked as well. This can be achieved via the override feature.
First, in order to add the event listener, you want override the react component in the sign up form and use a useEffect hook in your custom component. The component to override is called EmailPasswordSignUpForm_Override (supertokens-auth-react SDK version >= 0.20.0)
EmailPasswordReact.init({
override: {
components: {
EmailPasswordSignUpForm_Override: ({ DefaultComponent, ...props }) => {
React.useEffect(() => {
// This will get fired each time the sign up form is shown to the user
// TODO: adding the event listener logic come in here...
}, [])
return <DefaultComponent {...props} />;
}
}
},
signInAndUpFeature: {...}
})
Then you want to find the input field representing the input for the phone number. You can find it like:
React.useEffect(() => {
// Note that the phone number input is the 5th element in the sign up form. That's why we do nth-child(5)
let input = document.querySelector("#supertokens-root").shadowRoot.querySelector("form > div:nth-child(5) > div:nth-child(2) > div > input");
function onBlur() {
let phoneNumber = input.value;
let normalisedPhoneNumber = inputMask(phoneNumber);
input.value = normalisedPhoneNumber;
}
input.addEventListener('blur', onBlur);
return () => {
input.removeEventListener('blur', onBlur);
}
}, [])
The above will noramlise the phone number when the phone number input is blurred. Whilst the above works from a UI point of view, when the sign up button is clicked, or when the validate function is called, the user's input may still be unnormalised (cause the react state in the library has not been updated). So you should also call the inputMask function during the validate function call and when signUp is clicked:
Normalise when signUp is clicked
EmailPasswordReact.init({
override: {
functions: (oI) => {
return {
...oI,
signUp: async function (input) {
// we normalise the phone input
input.formFields = input.formFields.map(f => {
if (f.id === "phone") {
return {
...f,
value: inputMask(f.value)
}
}
return f;
})
// we call the original implementation
return oI.signUp(input);
}
}
}
}
})
Normalise when validate function is called:
validate: (value) => {
value = inputMask(value);
// TODO: logic for checking input syntax. If there is an error, return a string else undefined
}
So the overall code looks like:
import React from 'react'
import EmailPasswordReact from 'supertokens-auth-react/recipe/emailpassword'
import SessionReact from 'supertokens-auth-react/recipe/session'
import { appInfo } from './appInfo'
export let frontendConfig = () => {
return {
appInfo,
recipeList: [
EmailPasswordReact.init({
override: {
functions: (oI) => {
return {
...oI,
signUp: async function (input) {
// we normalise the phone input
input.formFields = input.formFields.map(f => {
if (f.id === "phone") {
return {
...f,
value: inputMask(f.value)
}
}
return f;
})
// we call the original implementation
return oI.signUp(input);
}
}
},
components: {
EmailPasswordSignUpForm_Override: ({ DefaultComponent, ...props }) => {
React.useEffect(() => {
let input = document.querySelector("#supertokens-root").shadowRoot.querySelector("form > div:nth-child(5) > div:nth-child(2) > div > input");
function onBlur() {
let phoneNumber = input.value;
let normalisedPhoneNumber = inputMask(phoneNumber);
input.value = normalisedPhoneNumber;
}
input.addEventListener('blur', onBlur);
return () => {
input.removeEventListener('blur', onBlur);
}
}, [])
return <DefaultComponent {...props} />;
}
}
},
signInAndUpFeature: {
signUpForm: {
formFields: [
{
id: "firstname",
label: "First Name",
placeholder: "Jane",
},
{
id: "lastname",
label: "Last Name",
placeholder: "Doe",
},
{
id: "phone",
label: "Phone",
placeholder: "(123)456-7890",
validate: (value) => {
value = inputMask(value);
// TODO: logic for checking input syntax
return undefined;
}
},
],
},
}
}),
SessionReact.init(),
],
}
}
function inputMask(value) {
const _num = value.replace(/\D/g, "").match(/(\d{3})(\d{3})(\d{4})/);
const phoneNumber = `(${_num[1]})${_num[2]}-${_num[3]}`;
return phoneNumber;
}
Hope this helps!

Related

Watch property on Vue3

I have a form that I am editing, and i use the created method to prefill that form information from api call and that all works fine.
However I want to watch the fields in the form, and if they get edited i want to set a variable "isFormEdited" to be true.
I have it set up like this.
watch: {
'form': {
handler: function(v) {
console.log('form changed')
//here i would set the "isFormEdited" variable to be true.
},
deep: true
},
My problem is that the console log happens immediately on page load, because the form goes from empty, then the created function fills the values with the api call.
How can I get around this so that I can have a "isFormEdited" value only set true when the form was actually changed manually?
created() {
const getForm = axios.get(`api/dashboard/form/${this.$route.params.id}/edit`);
const getFormTypes = axios.get('api/dashboard/form/types');
axios.all([getForm, getFormTypes])
.then(
axios.spread((...responses) => {
// console.log(responses[0].data);
this.form = responses[0].data;
if(!this.form.type_id) this.form.type_id = ""; //Set Blank if null
this.form.fields.map((field) => {
// console.log(field);
if(field.options){ field.options = JSON.parse(field.options); }
if(field.mime_types_allowed){ field.mime_types_allowed = JSON.parse(field.mime_types_allowed); }
return field;
});
//get form types
this.types = responses[1].data.data;
})
)
.catch(errors => {
alert("Error");
console.log(errors);
});
Thank you in advance.
You could create an additional variable dataLoaded, set it to false by default, and change it to true when you have populated your form. Then in your watch handler, you would only set isFormEdited to true when dataLoaded is true as well.
Example:
export default {
data() {
return {
dataLoaded: false,
isFormEdited: false,
};
},
created() {
const getForm = axios.get(`api/dashboard/form/${this.$route.params.id}/edit`);
const getFormTypes = axios.get('api/dashboard/form/types');
axios
.all([getForm, getFormTypes])
.then(
axios.spread((...responses) => {
// console.log(responses[0].data);
this.form = responses[0].data;
if (!this.form.type_id) this.form.type_id = ''; //Set Blank if null
this.form.fields.map((field) => {
// console.log(field);
if (field.options) {
field.options = JSON.parse(field.options);
}
if (field.mime_types_allowed) {
field.mime_types_allowed = JSON.parse(field.mime_types_allowed);
}
return field;
});
//get form types
this.types = responses[1].data.data;
this.dataLoaded = true;
})
)
.catch((errors) => {
alert('Error');
console.log(errors);
});
},
watch: {
form: {
handler: function (v) {
if (this.dataLoaded) {
console.log('Form changed!');
this.isFormEdited = true;
}
},
deep: true,
},
},
};
If this is not an option, you could use the instance's $watch method to start watching a data element at any point in your component.

Change Nested State Value With A Single Function? (javascript/React)

I have some react user privilege state data I need to manage. I would like the ability to change the object privileges based on their property through a dynamic function. I'm not sure how to target the specific nested privilege property to change the value. Is this possible?
Question: How can I change the value of a nested privilege property to the functions type and value parameter?
Heres an Example:
const [userPrivilages, setUserPrivilages] = useState([{
_id: "123"
privilages: {
edit: true, //before!
share: true,
del: false
}
},
{
...more users
}
])
//my attempt
const changePrivilage = (type, value) => {
const newPrivilages = userPrivilages.map(user => {
return {
...user,
privilages: {
...privilages,
//change the privilage of "type" from the functions parameter to the value parameter
}
}) setUserPrivilages(newPrivilages)
}
changePrivilage("edit", false)
Desired output:
const [userPrivilages, setUserPrivilages] = useState([{
_id: "123"
privilages: {
edit: false, //After!
share: true,
del: false
}
},
{
...more users
}
])
Thanks!
You can use [] to refer to variable as a key like below:
const changePrivilage = (type, value) => {
const newPrivilages = userPrivilages.map(user => {
return {
...user,
privilages: {
...user.privilages,
[type]: value // here it is !!!
}
}) setUserPrivilages(newPrivilages)
}
Try this :
(see comments for understanding code)
const changePrivilage = (type,value) => {
const newUserPrivilages = userPrivilages.map(user => {
let newPrivilages = user.privilages; // get old privilages of user
newPrivilages[type] = value; // update type with new value
return {
...user,
privilages: newPrivilages, // set privilages as newPrivilages
};
});
setUserPrivilages(newUserPrivilages);
};
Note : this will change properties for all users. If you want to update only for specific user, pass _id as well to changePrivilage and execute newPrivilages[type] = value; // update type with new value inside if condition comparing user _id.

React useState hook and submit in multi step form

I am creating a multi step form using React JsonSchema Form. I want to update my state every time Next button is clicked on the page and finally Submit. React JsonSchema Form validates the entries only if the button is of type submit. So my Next button is submit button.
As the form will have multiple questions, my state is array of objects. Because of the asynchronous nature of useState, my updated state values are not readily available to save to the backend. How should I get final values?
When I debug I can see the data till previous step. Is there a way to make useState to behave like synchronous call?
Here is the full code:
const data = [
{
page: {
id: 1,
title: "First Page",
schema: {
title: "Todo 1",
type: "object",
required: ["title1"],
properties: {
title1: { type: "string", title: "Title", default: "A new task" },
done1: { type: "boolean", title: "Done?", default: false },
},
},
},
},
{
page: {
id: 1,
title: "Second Page",
schema: {
title: "Todo 2",
type: "object",
required: ["title2"],
properties: {
title2: { type: "string", title: "Title", default: "A new task" },
done2: { type: "boolean", title: "Done?", default: false },
},
},
},
},
];
interface IData {
id: Number;
data: any
};
export const Questions: React.FunctionComponent = (props: any) => {
const [currentPage, setCurrentPage] = useState(0);
const [surveyData, setSurveyData] = useState<IData[]>([]);
const handleNext = (e: any) => {
setSurveyData( previous => [
...previous,
{
id: currentPage,
data: e.formData,
},
]);
if (currentPage < data.length) setCurrentPage(currentPage + 1);
else //Here I want to submit the data
};
const handlePrev = () => {
setCurrentPage(currentPage - 1);
};
return (
<Form
schema={data[currentPage].page.schema as JSONSchema7}
onSubmit={handleNext}
>
<Button variant="contained" onClick={handlePrev}>
Prev
</Button>
<Button type="submit" variant="contained">
Next
</Button>
</Form>
);
};
You can incorporate useEffect hook which will trigger on your desired state change.
Something like this:
useEffect(() => {
// reversed conditional logic
if (currentPage > data.length) {
submit(surveyData);
}
}, [currentPage])
const handleNext = (e: any) => {
setSurveyData( previous => [
...previous,
{
id: currentPage,
data: e.formData,
},
]);
if (currentPage < data.length) setCurrentPage(currentPage + 1);
// remove else
};
On your last submit, you'll have all the previous data in surveyData, but you'll have the latest answer in e.formData. What you'll need to do is combine those and send that to the server.
// in your onSubmit handler
else {
myApiClient.send({ answers: [...surveyData, e.formData] })
}
I would refactor the new state structure to use the actual value of the state and not the callback value, since this will allow you to access the whole structure after setting:
const handleNext = (e: any) => {
const newSurveyData = [
...surveyData,
{
id: currentPage,
data: e.formData
}
];
setSurveryData(newSurveyData);
if (currentPage < data.length) {
setCurrentPage(currentPage + 1);
} else {
// submit newSurveryData
};
};
A side note: you'll also have to account for the fact that going back a page means you have to splice the new survey data by index rather than just appending it on the end each time.

MBDReact: How do I make a <datatable> row clickable?

I have setup my table in admin side of our application with MDBReact using the datatable. This table shows some small details of the stories that I have.
Now I have to make a row clickable i.e. add onClick to make a function call with the story id passed as an argument to this function.
Question:
How do I add onClick event to the datatable row?
(Below is my code.)
class Posts extends Component {
componentDidMount() {
this.getPosts();
}
getPosts = async () => {
const response = await fetch("http://****************/get_posts");
const post_items = await response.json();
this.setState({ posts: post_items.result }, () => {
console.log(this.state.posts);
this.setState({ tableRows: this.assemblePost() });
});
};
assemblePost = () => {
let posts = this.state.posts.map((post) => {
let mongoDate = post.dateAdded.toString();
let mainDate = JSON.stringify(new Date(mongoDate));
return {
postTitle: post.storyTitle,
// postDescription: post.storyDescription,
dateAdded: mainDate.slice(1, 11),
thankedBy: post.thankedBy.length,
reportedBy: post.reportedBy ? post.reportedBy.length : "",
userEmail: post.userEmail[0],
categoryName: post.categoryName[0],
};
});
console.log(posts);
return posts;
};
state = {
posts: [],
tableRows: [],
};
render() {
const data = {
columns: [
{
label: "Story Title",
field: "postTitle",
},
{ label: "Category Name", field: "categoryName" },
{
label: "User Email",
field: "userEmail",
},
{
label: "Date Added",
field: "dateAdded",
},
{
label: "Thanked",
field: "thankedBy",
},
{
label: "Reported",
field: "reportedBy",
},
],
rows: this.state.tableRows,
};
return (
<div className="MDBtable">
<p className="posts">Posts List</p>
<MDBDataTable striped bordered hover data={data} />
</div>
);
}
}
export default Posts;
To pull this off, here's what I did, but you'll need to appreciate these:
MDBDataTable requires you to manually define the columns and rows.
For data to render seamlessly, you define columns.field that correspond to rows[key]
Now, here's the logic, if you define a rows[key] that does not correspond to any columns.field, then that rows[key] is defined for an entire row just like we often pass index when working with map().
So based on the above observations,you can just pass the click event as a key/value pair to the row.And it will work just fine.
// ...
assemblePost = () => {
let posts = this.state.posts.map(
(post, i) => {
let mongoDate = post.dateAdded.toString();
let mainDate = JSON.stringify(new Date(mongoDate));
return {
index: i + 1, // advisable to pass a unique identifier per item/row
clickEvent: () => this.handleClick(storyId), // pass it a callback function
postTitle: post.storyTitle,
// ...others
categoryName: post.categoryName[0],
};
});
console.log(posts);
return posts;
};
// ...
Notice this clickEvent: () => this.handleClick(storyId), will be attached to the entire row.

How can I disable checkboxes and radio buttons from separate components?

Running into this ReactJS (with Redux) issue:
If premium white isn’t selected, gloss finish should be disabled. The radio button (premium) and checkbox (gloss) have separate methods in separate components – looks like they are both using state to send data.
Here’s the checkbox
buildCheckbox(item) {
return (
<Checkbox
key={item.key}
label={item.display}
name={item.key}
checked={this.props.order[item.key] || false}
onChange={checked => this.handleCheck(checked, item.key)}
disabled={item.disabled(this.props.order)}
/>
);
}
And the handleclick method used
handleCheck(checked, key) {
const { params, updateOrder } = this.props;
const { sessionId } = params;
// if doulbeSided option is removed, then clear the inside file.
if (key === 'doubleSided' && !checked) {
updateOrder(sessionId, { inside: null });
}
// set ink coverage based on printed flag
if (key === 'printed') {
const inkCoverage = checked ? 100 : 0;
updateOrder(sessionId, { inkCoverage });
}
// if unprinted, remove doublesided and gloss options
if (key === 'printed' && !checked) {
updateOrder(sessionId, { doubleSided: false });
updateOrder(sessionId, { gloss: false });
}
updateOrder(sessionId, { [key]: checked });
}
And the radio button’s method
onClick(id, ordAttribute) {
const { updateOrder, sessionId, validator } = this.props;
updateOrder(sessionId, { [ordAttribute]: id });
if (validator) validator(ordAttribute);
}
I saw that gloss has a service which is toggling disabled or not via the printed key on state here
gloss: {
display: 'Gloss Finish',
key: 'gloss',
component: 'checkbox',
disabled: state => !state.printed,
},
I’ve thought about creating a fourth radio button and just deleting the gloss option but I’m not sure where it’s being populated from – also thought about using a display none on the styles of the gloss that is activated via the radio button – but am not sure where to start.
just stated a new job and this is the previous employee's code - trying to figure it out. looks like the state is activated via this Action method:
export const updateOrder = (sessionId, payload) => (dispatch, getState) => {
dispatch(updateAction({ ...payload }));
const state = getState();
const ord = getNewOrderForm(state);
const minOrdValue = getMinOrdValue(state);
const { length, width, height, style, blankLength, blankWidth, qty, leadTime, sqFeet } = ord;
const priceMatrix = style ? getPriceMatrix(state)[style.priceMatrix] : null;
if (priceMatrix && style && style.calcPrice) {
dispatch(dispatchNewPrice(ord, style, priceMatrix, minOrdValue));
}
if (shouldCalcBlank({width, length, height}, style)) {
calcBlanks(style, {width, length, height})
.then(blanks => dispatch(updateAction(blanks)))
.catch(err => console.log('error', err))
}
if (blankLength && blankWidth && qty) {
calcSquareFeet({ blankLength, blankWidth, qty })
.then(sqFeet => {
dispatch(updateAction({ sqFeet }));
return sqFeet;
})
.then(sqFeet => sqFeet > 1000)
.then(lrgSqFeet => {
dispatch(updateAction({ lrgSqFeet }));
return lrgSqFeet;
})
.then(lrgSqFeet => {
if (lrgSqFeet && leadTime === 'rush') {
dispatch(updateAction({ leadTime: 'standard' }));
}
});
}
if (sqFeet && (!blankLength || !blankWidth || !qty)) {
dispatch(updateAction({ sqFeet: 0 }));
}
localStorage.setItem(sessionId, JSON.stringify(getNewOrderForm(getState())));
};
i thought about adding a the radio button has an id of 'clearwater' so i thought about adding a bool to this method that could then be accessed as clearwater: false (and when onClick is activated, updateOrder then changes it to clearwater: true, and then the gloss object in the service would then check disabled: state => !state.printed && !state.clearwater (this didn't work):
export const generateNewOrder = (userid, style, sessionId = uuid()) => dispatch => {
localStorage.setItem(
sessionId,
JSON.stringify({
userid,
style,
sessionId,
blindShip: true,
inkCoverage: '100',
printed: true,
})
);
history.push(`/order/new/${style.styleCode}/${sessionId}`);
dispatch(
newOrder({
userid,
style,
sessionId,
blindShip: true,
inkCoverage: '100',
printed: true,
})
);
if (style.type === 'static') {
const { dims, blankLength, blankWidth } = style;
const payload = {
...dims,
blankLength,
blankWidth,
};
dispatch(updateOrder(sessionId, payload));
}
};
I was hoping by changing the Service attached to the checkbox, I could add an additional condition that would cause the disabled functionality to be dependent on the state.boardStyle, but this doesn't seem to work (picture below isn't accurate, i changed it to boardStyle):
http://oi65.tinypic.com/wknzls.jpg
This is using redux -- kind of new to redux -- let me know if I'm missing any info -- I will post anything to get this solved.
Any help would be huge – thanks so much!
i think i figured it out . . .
there's probably a drier way to do this, but here goes:
first i created a new key [bool] (clearwater, set to false) on generateNewOrder method in Actions:
export const generateNewOrder = (userid, style, sessionId = uuid()) => dispatch => {
localStorage.setItem(
sessionId,
JSON.stringify({
userid,
style,
sessionId,
blindShip: true,
inkCoverage: '100',
printed: true,
clearwater: false,
})
);
history.push(`/order/new/${style.styleCode}/${sessionId}`);
dispatch(
newOrder({
userid,
style,
sessionId,
blindShip: true,
inkCoverage: '100',
printed: true,
clearwater: false
})
);
if (style.type === 'static') {
const { dims, blankLength, blankWidth } = style;
const payload = {
...dims,
blankLength,
blankWidth,
};
dispatch(updateOrder(sessionId, payload));
}
};
that gave me access to this state value, which i could then use in the onclick when the radio button was pressed. if the id was clearwater, the bool would be set to true, else it was set to false (only for the other two options because this code is used for other IDs)
onClick(id, ordAttribute) {
const { updateOrder, sessionId, validator } = this.props;
updateOrder(sessionId, { [ordAttribute]: id });
if (validator) validator(ordAttribute);
if (id === 'clearwater') {
updateOrder(sessionId, { clearwater: true });
} else if (id === 'kraft' || id === 'std_white_two_side'){
updateOrder(sessionId, { clearwater: false });
}
}
then all i needed to do was add this to the Service. if it was not printed !printed or not clearwater !clearwater, the checkbox would be disabled
const printing = {
doubleSided: {
display: 'Two Sided Print',
key: 'doubleSided',
component: 'checkbox',
disabled: state => !state.printed,
},
printed: {
display: 'Printed?',
key: 'printed',
component: 'checkbox',
disabled: () => false,
},
gloss: {
display: 'Gloss Finish',
key: 'gloss',
component: 'checkbox',
disabled: state => !state.printed || !state.clearwater,
},
};
I have a working example answering a similar problem, please, have a look. The whole logic is done in redux:
Want to uncheck the node of the tree structure in React JS

Categories

Resources