I'm sending form data from React Hook Form to Netlify via their submission-created function. I don't have any problem with encoding individual form field values, but now I'm trying to encode an array of objects.
Here is an example of my form data:
{
_id: "12345-67890-asdf-qwer",
language: "Spanish",
formId: "add-registration-form",
got-ya: "",
classType: "Private lessons",
size: "1",
days: [
{
day: "Monday",
start: 08:00",
end: "09:30"
},
{
day: "Wednesday",
start: "08:00",
end: "09:30"
}
]
}
The only problem I have is with the "days" array. I've tried various ways to encode this and this is the function I've currently been working with (which isn't ideal):
const encode = (data) => {
return Object.keys(data).map(key => {
let val = data[key]
if (val !== null && typeof val === 'object') val = encode(val)
return `${key}=${encodeURIComponent(`${val}`.replace(/\s/g, '_'))}`
}).join('&')
}
I tried using a library like qs to stringify the data, but I can't figure out how to make that work.
And here is the function posting the data to Netlify:
// Handles the post process to Netlify so I can access their serverless functions
const handlePost = (formData, event) => {
event.preventDefault()
fetch(`/`, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: encode({ "form-name": 'add-registration-form', ...formData }),
})
.then((response) => {
if(response.status === 200) {
navigate("../../")
} else {
alert("ERROR!")
}
console.log(response)
})
.catch((error) => {
setFormStatus("error")
console.log(error)
})
}
Finally, here is a sample of my submission-created file to receive and parse the encoded data:
const sanityClient = require("#sanity/client")
const client = sanityClient({
projectId: process.env.GATSBY_SANITY_PROJECT_ID,
dataset: process.env.GATSBY_SANITY_DATASET,
token: process.env.SANITY_FORM_SUBMIT_TOKEN,
useCDN: false,
})
const { nanoid } = require('nanoid');
exports.handler = async function (event, context, callback) {
// Pulling out the payload from the body
const { payload } = JSON.parse(event.body)
// Checking which form has been submitted
const isAddRegistrationForm = payload.data.formId === "add-registration-form"
// Build the document JSON and submit it to SANITY
if (isAddRegistrationForm) {
// How do I decode the "days" data from payload?
let schedule = payload.data.days.map(d => (
{
_key: nanoid(),
_type: "classDayTime",
day: d.day,
time: {
_type: "timeRange",
start: d.start,
end: d.end
}
}
))
const addRegistrationForm = {
_type: "addRegistrationForm",
_studentId: payload.data._id,
classType: payload.data.classType,
schedule: schedule,
language: payload.data.language,
classSize: payload.data.size,
}
const result = await client.create(addRegistrationForm).catch((err) => console.log(err))
}
callback(null, {
statusCode: 200,
})
}
So, how do I properly encode my form data with a nested array of objects before sending it to Netlify? And then in the Netlify function how do I parse / decode that data to be able to submit it to Sanity?
So, the qs library proved to be my savior after all. I just wasn't implementing it correctly before. So, with the same form data structure, just make sure to import qs to your form component file:
import qs from 'qs'
and then make your encode function nice and succinct with:
// Transforms the form data from the React Hook Form output to a format Netlify can read
const encode = (data) => {
return qs.stringify(data)
}
Next, use this encode function in your handle submit function for the form:
// Handles the post process to Netlify so we can access their serverless functions
const handlePost = (formData, event) => {
event.preventDefault()
fetch(`/`, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: encode({ "form-name": 'add-registration-form', ...formData }),
})
.then((response) => {
reset()
if(response.status === 200) {
alert("SUCCESS!")
} else {
alert("ERROR!")
}
console.log(response)
})
.catch((error) => {
console.log(error)
})
}
Finally, this is what your Netlify submission-created.js file should look like more or less:
const sanityClient = require("#sanity/client")
const client = sanityClient({
projectId: process.env.GATSBY_SANITY_PROJECT_ID,
dataset: process.env.GATSBY_SANITY_DATASET,
token: process.env.SANITY_FORM_SUBMIT_TOKEN,
useCDN: false,
})
const qs = require('qs')
const { nanoid } = require('nanoid');
exports.handler = async function (event, context, callback) {
// Pulling out the payload from the body
const { payload } = JSON.parse(event.body)
// Checking which form has been submitted
const isAddRegistrationForm = payload.data.formId === "add-registration-form"
// Build the document JSON and submit it to SANITY
if (isAddRegistrationForm) {
const parsedData = qs.parse(payload.data)
let schedule = parsedData.days
.map(d => (
{
_key: nanoid(),
_type: "classDayTime",
day: d.day,
time: {
_type: "timeRange",
start: d.start,
end: d.end
}
}
))
const addRegistrationForm = {
_type: "addRegistrationForm",
submitDate: new Date().toISOString(),
_studentId: parsedData._id,
classType: parsedData.classType,
schedule: schedule,
language: parsedData.language,
classSize: parsedData.size,
}
const result = await client.create(addRegistrationForm).catch((err) => console.log(err))
}
callback(null, {
statusCode: 200,
})
}
I have been staring at these action creators:
import * as types from './constants';
import * as endpoints from 'endpoints';
import * as requester from 'services/Requester';
import * as helpers from 'account-settings/helpers/data-helpers';
export function fetchPrefences({Key}) {
return dispatch => {
const url = `${endpoints.v2.INDIVIDUALS}/${Key}/preferences`;
requester.sendGet(url).then(data => {
const payload = helpers.sortPreferences(data);
dispatch({
type: types.SET_USER_PREFERENCES,
payload,
});
});
};
}
export function fetchTopics() {
return dispatch => {
requester.sendGet(endpoints.TOPICS_OF_CONCERN).then(data => {
dispatch({
type: types.SET_USER_TOPICS,
payload: data.Items,
});
});
};
}
export function handleStateChange(payload) {
return {
type: types.SET_NEW_PREFERENCES,
payload,
};
}
export function handleUpdateTopics({topics, involved}, updateBoth = false) {
return dispatch => {
return requester
.sendPut(endpoints.TOPICS_OF_CONCERN, {
Items: topics,
})
.then(data => {
dispatch({
type: types.SET_USER_TOPICS,
payload: data.Items,
});
if (updateBoth) {
dispatch(handleUpdatePreferences({involved}));
}
});
};
}
export function handleUpdateGetInvoved({involved}) {
return (dispatch, getState) => {
const {auth} = getState();
//prettier-ignore
const url = `${endpoints.v2.INDIVIDUALS}/${auth.user.Key}/preferences`;
return requester
.sendPut(url, {
Items: involved,
})
.then(data => {
const payload = helpers.sortPreferences(data);
dispatch({
type: types.SET_USER_PREFERENCES,
payload,
});
});
};
}
And it's clear I am getting data as undefined with this message:
What is not clear is why? When I do a curl, the data is there:
{"items":[{"category":"None","key":"2883040c-88b8-4899-bd47-114a560d085b","displayText":"Energy
Costs","isSelected":false,"order":1},{"category":"None","key":"a745a3d6-0f64-4595-8734-6082d9c914f7","displayText":"Regulations","isSelected":false,"order":7},{"category":"None","key":"51797a61-8016-4817-a46e-72dee3d8239a","displayText":"Minimum
Wage","isSelected":false,"order":5},{"category":"None","key":"381e24d0-2668-4a69-a993-7d5e1ecaec3b","displayText":"Taxes","isSelected":false,"order":8},{"category":"None","key":"dfaf22cb-111a-46f3-bce3-93fbf4a91490","displayText":"Unemployment
Insurance","isSelected":false,"order":9},{"category":"None","key":"c55b5d2a-a0f3-4c35-bf59-b433259b2059","displayText":"Workers
Compensation","isSelected":false,"order":10},{"category":"None","key":"d4b787d4-550b-4866-a5cc-c6a2de61a91a","displayText":"Healthcare","isSelected":false,"order":4},{"category":"None","key":"c2557854-421d-4b2f-810f-caadf938cded","displayText":"Government
Spending","isSelected":false,"order":3},{"category":"None","key":"cf91f638-c5fa-4252-be01-dce504ae369d","displayText":"Private
Property
Rights","isSelected":false,"order":6},{"category":"None","key":"0eae5ccf-2ba5-41bd-9111-efe7acafa512","displayText":"Finding
Qualified Employees","isSelected":false,"order":2}]}%
In Swagger, I check, the data is there:
{
"items": [
{
"category": "None",
"key": "2883040c-88b8-4899-bd47-114a560d085b",
"displayText": "Energy Costs",
"isSelected": false,
"order": 1
},
{
"category": "None",
"key": "a745a3d6-0f64-4595-8734-6082d9c914f7",
"displayText": "Regulations",
"isSelected": false,
"order": 7
},
{
"category": "None",
"key": "51797a61-8016-4817-a46e-72dee3d8239a",
"displayText": "Minimum Wage",
"isSelected": false,
"order": 5
},
{
"category": "None",
"key": "381e24d0-2668-4a69-a993-7d5e1ecaec3b",
"displayText": "Taxes",
"isSelected": false,
"order": 8
},
{
"category": "None",
"key": "dfaf22cb-111a-46f3-bce3-93fbf4a91490",
"displayText": "Unemployment Insurance",
"isSelected": false,
"order": 9
},
{
"category": "None",
"key": "c55b5d2a-a0f3-4c35-bf59-b433259b2059",
"displayText": "Workers Compensation",
"isSelected": false,
"order": 10
},
{
"category": "None",
"key": "d4b787d4-550b-4866-a5cc-c6a2de61a91a",
"displayText": "Healthcare",
"isSelected": false,
"order": 4
},
{
"category": "None",
"key": "c2557854-421d-4b2f-810f-caadf938cded",
"displayText": "Government Spending",
"isSelected": false,
"order": 3
},
{
"category": "None",
"key": "cf91f638-c5fa-4252-be01-dce504ae369d",
"displayText": "Private Property Rights",
"isSelected": false,
"order": 6
},
{
"category": "None",
"key": "0eae5ccf-2ba5-41bd-9111-efe7acafa512",
"displayText": "Finding Qualified Employees",
"isSelected": false,
"order": 2
}
]
}
I noticed that in the code the items property was written as Items, I tried to change it to items to match the data property, that did nothing.
A colleague suggested the issue could be in the requester object, I do have a question about it too:
import axios from 'axios';
import LocalStorage from './LocalStorage';
import env from 'env';
import * as appcenter from 'utils/appcenterLogger';
import * as titlesHelper from 'utils/titleCaser';
let expired = false;
export const instance = axios.create({
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
NFIBAppID: env.NFIBAppID,
},
});
let preHeaders = {};
async function mergeConfig(config) {
try {
const access = await LocalStorage.get('access');
preHeaders = access;
return {...config, headers: {...access}};
} catch (error) {
return {...config};
}
}
export async function sendGet(url, config = {}) {
if (expired) {
return;
}
const now = new Date();
return instance
.get(url, await mergeConfig(config))
.then(response => {
return saveHeaders(response, now, url);
})
.catch(error => {
return catchErros(error, now, url);
});
}
export async function sendPost(url, data, config = {}) {
if (expired) {
return;
}
const now = new Date();
return instance
.post(url, titlesHelper.lowerCaser(data), await mergeConfig(config))
.then(response => {
console.log(response);
return saveHeaders(response, now, url);
})
.catch(error => {
return catchErros(error, now, url);
});
}
export async function sendPut(url, data, config = {}) {
if (expired) {
return;
}
const now = new Date();
return instance
.put(url, titlesHelper.lowerCaser(data), await mergeConfig(config))
.then(response => {
return saveHeaders(response, now, url);
})
.catch(error => {
return catchErros(error, now, url);
});
}
export async function sendPatch(url, data, config = {}) {
if (expired) {
return;
}
const now = new Date();
return instance
.patch(url, data, await mergeConfig(config))
.then(response => {
return saveHeaders(response, now, url);
})
.catch(error => {
return catchErros(error, now, url);
});
}
export async function sendDelete(url, data, config = {}) {
if (expired) {
return;
}
const now = new Date();
return instance
.delete(url, await mergeConfig(config))
.then(response => {
return saveHeaders(response, now, url);
})
.catch(error => {
return catchErros(error, now, url);
});
}
export function saveHeaders({data, headers}, timeSent, url) {
try {
if (headers && headers.authorizationtoken) {
LocalStorage.save('access', {
AuthorizationToken: headers.authorizationtoken,
});
}
const timeReceived = new Date();
LocalStorage.save('lastTimeRequestSent', timeReceived);
appcenter.trackRequestTiming(timeSent, timeReceived, headers, url, false);
return titlesHelper.toTitleCase(data);
} catch (_e) {
return false;
}
}
function catchErros({error, timeSent}, url) {
try {
const timeReceived = new Date();
LocalStorage.save('lastTimeRequestSent', timeReceived);
appcenter.trackRequestTiming(timeSent, timeReceived, error, url, true);
if (error && error.response) {
saveHeaders({
headers: preHeaders,
});
const {data} = error.response;
const message = data.message || data.Message;
if (message.includes('TokenExpired')) {
expired = true;
}
}
return Promise.reject(titlesHelper.toTitleCase(error.response.data));
} catch (_e) {
return error;
}
}
export function resetTokenExpired() {
expired = false;
}
I am seeing Promise syntax being mixed with async/await syntax, could this be causing an issue?
I tried to see if perhaps the issue was with the authorization token, so I console logged it:
let preHeaders = {};
async function mergeConfig(config) {
try {
const access = await LocalStorage.get('access');
console.log(access);
preHeaders = access;
return {...config, headers: {...access}};
} catch (error) {
return {...config};
}
}
but I am successfully getting that back:
{AuthorizationToken: "<bunch-o-numbers>"}
What I know at this point is that without the logic inside of the saveHeaders() function, a registered users' password will return undefined.
To complicate things, this application uses action helpers, which I have never implemented, but I see that Items property in there all over the place, keep in mind that the original warning read Items, but I changed it everywhere to items to make it match the JSON item property in the hope that would be the fix.
However, I have now come across these action helper files with the following code, action-helpers.js:
import * as endpoints from 'endpoints';
import * as requester from 'services/Requester';
import compareDesc from 'date-fns/compare_desc';
export async function fetchTransaction() {
try {
const response = await requester.sendGet(endpoints.TRANSACTIONS);
const {Items = []} = response;
return Items.sort((a, b) => compareDesc(a.DateTime, b.DateTime));
} catch (error) {
return [];
}
}
and in data-helpers.js:
export function sortPreferences(data) {
const sorted = data.Items.sort((a, b) => a.Order - b.Order);
const communications = sorted.filter(
p => p.Category === 'CommunicationPreferences'
);
const privacy = sorted.filter(p => p.Category === 'MemberPrivacy');
const involved = sorted.filter(p => p.Category === 'GetInvolved');
const format = data.EmailFormatType === 'HTML' ? 'HTML' : 'Plain Text';
return {
communications,
privacy,
involved,
emailFormatType: format,
isEmailAllowed: data.IsEmailAllowed,
isPhoneAllowed: data.IsPhoneAllowed,
};
}
Most probably you are not getting the expected response from your requester function.
Try logging the response from the requester and see the output. You might have to use response.json() to resolve the promise correctly. This is assuming your requester class/function works like that.
Im on a find situacion because the result of the Parse.query is just the id and I had other columns :(
Response of find:
[{"_objCount":0,"className":"DivorceCase","id":"8Ab15ASBX3"}]
Actual Code:
// Your corresponding keys
Parse.initialize("xxx", "xxx");
// For back4app applications
Parse.serverURL = 'https://parseapi.back4app.com'
var app = new Vue({
el: "#AttorneyCases",
data: {
DivorceCase :{},
},
mounted:function(){
new Parse.Query("DivorceCase").descending("createdAt").find()
.then((DivorceCaseResponse) => {
this.DivorceCase = DivorceCaseResponse;
})
}
})
to get the value of the other columns field, you need to use the code below:
var columnName = DivorceCaseResponse.get("columnName");
You can check more about retrieving objects here.
Thanks, nataliec!
The final code is:
mounted: function() {
var Case = Parse.Object.extend("DivorceCase");
var query = new Parse.Query(Case);
query.find()
.then((Ids) => {
Ids.forEach(element => {
var Case = Parse.Object.extend("DivorceCase");
var query = new Parse.Query(Case);
query.get(element.id)
.then((DivorceCaseResponse) => {
// The object was retrieved successfully.
this.DivorceCases.push(DivorceCaseResponse.get("BasicInfo"));
},
(e) => {
// The object was not retrieved successfully.
// error is a Parse.Error with an error code and message.
swal({
position: 'center',
type: 'error',
title: e.message,
customClass: 'swal-wide'
})
});
});
},
(e) => {
// The object was not retrieved successfully.
// error is a Parse.Error with an error code and message.
swal({
position: 'center',
type: 'error',
title: e.message,
customClass: 'swal-wide'
})
});
}
I am creating a simple chat app. I have three entities: rooms, messages and users.
I have a fake API that returns a response like this:
[{
id: 1,
name: 'room1',
avatar: 'some img url',
messages: [
{
id: 1,
text: 'some text',
user: {
id: 1,
username: 'Peter Peterson',
avatar: 'some img url'
}
]
}]
And my action looks like this:
getAllRooms({ commit }) {
commit(GET_ALL_ROOMS_REQUEST);
return FakeApi.getAllRooms()
.then(
rooms => {
const { entities } = normalize(rooms, room);
console.log(entities);
commit(GET_ALL_ROOMS_SUCCESS, {
rooms: entities.rooms, byId: rooms.map(room => room.id)
});
commit(GET_ALL_MESSAGES_SUCCESS, { messages: entities.messages });
commit(GET_ALL_USERS_SUCCESS, { users: entities.users });
},
err => commit(GET_ALL_ROOMS_ERROR)
)
}
And my mutations look like this:
[GET_ALL_ROOMS_REQUEST](state) {
state.loading = true;
},
[GET_ALL_ROOMS_SUCCESS](state, payload) {
state.rooms = payload.rooms;
state.byId = payload.byId;
state.loading = false;
},
[GET_ALL_ROOMS_ERROR]() {
state.error = true;
state.loading = false;
}
And my component calls the action like this:
{
mounted() {
this.getAllRooms();
}
}
These are my schema definitions:
const user = new schema.Entity('users');
const message = new schema.Entity('messages', {
user: user
});
const room = new schema.Entity('rooms', {
messages: [message]
})
when i check the response in then method after FakeApi.getAllRooms() every object is wrapped in some weird Observer, and I pass it like that to normalize and normalize returns some weird response.
What am I doing wrong?
The problem wasn't with vuejs, it was with the way I made the normalizr schemas. Because my response is an array at the root I should have had a new rooms array schema, like so:
const user = new schema.Entity('users');
const message = new schema.Entity('messages', {
user: user
});
const room = new schema.Entity('rooms', {
messages: [message]
});
const roomsSchema = [room];
And then use it like this: normalize(rooms, roomsSchema)
Can I make a custom validation rule that returns true/false based on a AJAX request? the problem is that the validate call has finished running when the AJAX call completes.
Do I need to have the rule set/unset a boolean variable based on which the field is valid/invalid?
const isValidNameRule = {
getMessage(field)
{
return "The name must be unique."
},
validate(validatingName)
{
var formData = new FormData();
formData.append("validatingName", validatingName);
this.$http.post("/api/isValid?name=" + validatingName, formData)
.then(function (response) {
// success
return true;
}, function (response) {
// error
return false;
});
}
};
Didn't know how to work with Promises.
Eventually got it working by extending one of the official samples:
const customRule = {
getMessage(field, params, data) {
return (data && data.message) || 'Something went wrong';
},
validate(aValue) {
return new Promise(resolve => {
var formData = new FormData();
formData.append("nameFilter", aValue);
$.ajax({
type: "POST",
url: url,
data: {
action: "validate",
value: aValue,
}
}).done(function (data) {
if (!ok)
{
resolve({
valid: false,
data: {message: "Condition not met"}
});
}
else
{
resolve({
valid: !! aValue,
data: undefined
});
}
});
});
}
};