I am having issues sending data to a server using remix run - I'm not sure I fully understand how useAction data works. I understand how the useLoaderData functions work but when you are trying to send data to a server I get errors.
What I want to do is send a post request to my server when I click a button - if I try and call create cart in the handleCLick event it says that createCart is not a function when it is
const submit = useSubmit()
function action({ request }) {
is this where i do my POST api call?
}
async function handleClick(event) {
await createCart(id, amount)
}
Cant seem to find any documentation which tells you how to do this?
EDIT: Hit send too early
With Remix, actions always run on the server. It is the method that Remix will call when you POST to a route.
// route.tsx
import { json, type ActionArgs, type LoaderArgs } from '#remix-run/node'
import { Form, useActionData, useLoaderData, useSubmit } from '#remix-run/react'
import { createCart } from '~/models/cart.server' // your app code
import { getUserId } from '~/models/user.server'
// loader is called on GET
export const loader = async ({request}: LoaderArgs) => {
// get current user id
const id = await getUserId(request)
// return
return json({ id })
}
// action is called on POST
export const action = async ({request}: ActionArgs) => {
// get the form data from the POST
const formData = await request.formData()
// get the values from form data converting types
const id = Number(formData.get('id'))
const amount = Number(formData.get('amount'))
// call function on back end to create cart
const cart = await createCart(id, amount)
// return the cart to the client
return json({ cart })
}
// this is your UI component
export default function Cart() {
// useLoaderData is simply returning the data from loader, it has already
// been fetched before component is rendered. It does NOT do the actual
// fetch, Remix fetches for you
const { id } = useLoaderData<typeof loader>()
// useActionData returns result from action (it's undefined until
// action has been called so guard against that for destructuring
const { cart } = useActionData<typeof action>() ?? {}
// Remix handles Form submit automatically so you don't really
// need the useSubmit hook
const submit = useSubmit()
const handleSubmit = (e) => {
submit(e.target.form)
}
return (
<Form method="post">
{/* hidden form field to pass back user id *}
<input type="hidden" name="id"/>
<input type="number" name="amount"/>
{/* Remix will automatically call submit when you click button *}
<button>Create Cart</button>
{/* show returned cart data from action */}
<pre>{JSON.stringify(cart, null, 2)}</pre>
</Form>
)
}
Related
I have a multistep form on vue js. I need to dynamically during input send requests, validate on the server and receive a response to display validation errors.
My data:
data() {
return {
form: {...this.candidate}, // we get an object with fields already filled *
errors: new Errors(), // object with validation errors
}
},
Now for each input I have computed property:
characteristicFuturePlans: {
get() {
return this.form.characteristic_future_plans;
},
set(value) {
this.saveField('characteristic_future_plans', value);
}
},
saveField method sends data:
saveField(field, value) {
this.form[field] = value; // keep the data in the object relevant
axios.put(`/api/candidate/${this.candidate.token}`, {[field]: value})
.then(() => { this.errors.clear(field) })
.catch((error) => {
this.errors.record(field, error.response.data.errors[field]);
});
},
Now, with each change of input, a request will be sent. But with this approach, when we quickly type text in a field, sometimes the penultimate request sent comes after the last. It turns out that if you quickly write "Johnny", sometimes a query with the text "Johnn" will come after a query with the text "Johnny" and the wrong value will be saved in the database.
Then I made sure that the data was sent 1 second after the termination of text input. Added timerId: {} to data(){} and then:
saveField(field, value) {
if(this.timerId[field]) {
clearTimeout(this.timerId[field]);
}
this.timerId[field] = setTimeout(this.send, 1000, field, value);
},
send(field, value) {
this.form[field] = value;
axios.put(`/api/candidate/${this.candidate.token}`, {[field]: value})
.then(() => { this.errors.clear(field) })
.catch((error) => {
this.errors.record(field, error.response.data.errors[field]);
});
},
But now, if after filling in the input in less than a second, press the button to go to the next step of the form, the page will simply refresh. (the button to go to the next step will send a request to the server to check if the required fields are filled)
How correctly save data to the database during text input? Can I do this without setTimeout()? If so, how can I make sure that the data of the last request, and not the penultimate, is stored in the database?
I will be glad to any tip.
Updated. Attach some template code.
Part of Step[i].vue component:
<div class="row">
<div class="form-group col-md-6">
<element-form title="Title text" :error="errors.get('characteristic_future_plans')" required isBold>
<input-input v-model="characteristicFuturePlans" :is-error="errors.has('characteristic_future_plans')"
:placeholder="'placeholder text'"/>
</element-form>
</div>
</div>
template of input-input component:
<input :value="value" :type="type" class="form-control" :class="isErrorClass"
:placeholder="placeholder" #input="$emit('input',$event.target.value)">
Components of form steps are called from the Page component. Nearby is the component of the button for moving to the next step.
<component :is="currentStep"
:candidate="candidate"
// many props
:global-errors="globalErrors"/>
<next-step :current-step="step" :token="candidate.token"
#switch-step-event="switchStep" #throw-errors="passErrors"></next-step>
NextStep component sends a request to the server, it is checking whether the required fields are filled in the database. If not, throw out a validation error. If so, go to the next form step.
you could try with watching the input values, and then use _.debounce() from underscore.js (src: https://underscorejs.org/#debounce) to delay the method call that makes a server request:
watch: {
fieldName: _.debounce(function(value) {
if(value === ''){
return;
}
this.saveField(this.fieldName, value);
},
...
Currently I have a useLazyQuery hook which is fired on a button press (part of a search form).
The hook behaves normally, and is only fired when the button is pressed. However, once I've fired it once, it's then fired every time the component re-renders (usually due to state changes).
So if I search once, then edit the search fields, the results appear immediately, and I don't have to click on the search button again.
Not the UI I want, and it causes an error if you delete the search text entirely (as it's trying to search with null as the variable), is there any way to prevent the useLazyQuery from being refetched on re-render?
This can be worked around using useQuery dependent on a 'searching' state which gets toggled on when I click on the button. However I'd rather see if I can avoid adding complexity to the component.
const AddCardSidebar = props => {
const [searching, toggleSearching] = useState(false);
const [searchParams, setSearchParams] = useState({
name: ''
});
const [searchResults, setSearchResults] = useState([]);
const [selectedCard, setSelectedCard] = useState();
const [searchCardsQuery, searchCardsQueryResponse] = useLazyQuery(SEARCH_CARDS, {
variables: { searchParams },
onCompleted() {
setSearchResults(searchCardsQueryResponse.data.searchCards.cards);
}
});
...
return (
<div>
<h1>AddCardSidebar</h1>
<div>
{searchResults.length !== 0 &&
searchResults.map(result => {
return (
<img
key={result.scryfall_id}
src={result.image_uris.small}
alt={result.name}
onClick={() => setSelectedCard(result.scryfall_id)}
/>
);
})}
</div>
<form>
...
<button type='button' onClick={() => searchCardsQuery()}>
Search
</button>
</form>
...
</div>
);
};
You don't have to use async with the apollo client (you can, it works). But if you want to use useLazyQuery you just have to pass variables on the onClick and not directly on the useLazyQuery call.
With the above example, the solution would be:
function DelayedQuery() {
const [dog, setDog] = useState(null);
const [getDogPhoto] = useLazyQuery(GET_DOG_PHOTO, {
onCompleted: data => setDog(data.dog)
})
return (
<div>
{dog && <img src={dog.displayImage} />}
<button
onClick={() => getDogPhoto({ variables: { breed: 'bulldog' }})}
>
Click me!
</button>
</div>
);
}
The react-apollo documentation doesn't mention whether useLazyQuery should continue to fire the query when variables change, however they do suggest using the useApolloClient hook when you want to manually fire a query. They have an example which matches this use case (clicking a button fires the query).
function DelayedQuery() {
const [dog, setDog] = useState(null);
const client = useApolloClient();
return (
<div>
{dog && <img src={dog.displayImage} />}
<button
onClick={async () => {
const { data } = await client.query({
query: GET_DOG_PHOTO,
variables: { breed: 'bulldog' },
});
setDog(data.dog);
}}
>
Click me!
</button>
</div>
);
}
The Apollo Client documentation isn't explicit about this, but useLazyQuery, like useQuery, fetches from the cache first. If there is no change between queries, it will not refetch using a network call. In order to make a network call each time, you can change the fetchPolicy to network-only or cache-and-network depending on your use case (documentation link to the fetchPolicy options). So with a fetchPolicy change of network-only in your example, it'd look like this:
const AddCardSidebar = props => {
const [searching, toggleSearching] = useState(false);
const [searchParams, setSearchParams] = useState({
name: ''
});
const [searchResults, setSearchResults] = useState([]);
const [selectedCard, setSelectedCard] = useState();
const [searchCardsQuery, searchCardsQueryResponse] =
useLazyQuery(SEARCH_CARDS, {
variables: { searchParams },
fetchPolicy: 'network-only', //<-- only makes network requests
onCompleted() {
setSearchResults(searchCardsQueryResponse.data.searchCards.cards);
}
});
...
return (
<div>
<h1>AddCardSidebar</h1>
<div>
{searchResults.length !== 0 &&
searchResults.map(result => {
return (
<img
key={result.scryfall_id}
src={result.image_uris.small}
alt={result.name}
onClick={() => setSelectedCard(result.scryfall_id)}
/>
);
})}
</div>
<form>
...
<button type='button' onClick={() => searchCardsQuery()}>
Search
</button>
</form>
...
</div>
);
};
When using useLazyQuery, you can set nextFetchPolicy to "standby". This will prevent the query from firing again after the first fetch. For example, I'm using the hook inside of a Cart Context to fetch the cart from an E-Commerce Backend on initial load. After that, all the updates come from mutations of the cart.
I'm using a laravel-vue-boilerplate .In the package there is User CRUD. I made a same thing,copy/paste,change couple of details to have Item CRUD.Working fine. The issue is after action (edit) I want to add a new Item,the form is filled already with the edited Item values. The form is in a modal which is a component.
Don't know which part of the code I paste here,Looking forward!
Modal :
addItem(): void {//this is the actions to call the modal
this.isModalAdd = true;
this.setModalVisible(true);
this.form=this.new_form;
}
edit(item:Item):void{
this.isModalAdd = false;
this.setModalVisible(true);
this.form = { ...item };
}
<ItemsModal v-bind:form='form' v-bind:is-add='isModalAdd' v-bind:is-visible='isModalVisible' ></ItemsModal>//added in the Items template
<script lang="ts">//Items Modal
import { Component, Emit, Prop, Vue } from 'vue-property-decorator';
import { Action, State, namespace } from 'vuex-class';
import checkPassword from '#/utils/checkPassword';
const iStore = namespace('items');
#Component
export default class ItemsModal extends Vue {
#Prop() form;
#Prop() isAdd;
#Prop() isVisible;
#iStore.Action addItem;
#iStore.Action editItem;
#iStore.Action setModalVisible;
#iStore.State isModalLoading;
handleOk() {
if (this.isAdd) {
this.addItem(this.form);
} else {
this.editItem(this.form);
}
}
handleClose() {
this.setModalVisible(false);
}
}
</script>
<template lang="pug">
b-modal(
hide-header-close=true,
:visible='isVisible',
:cancel-title='$t("buttons.cancel")',
:ok-disabled='isModalLoading',
:ok-title='isModalLoading ? $t("buttons.sending") : isAdd ? $t("buttons.add") : $t("buttons.update")',
:title='isAdd ? $t("users.add_user") : $t("users.edit_user")',
#hide='handleClose',
#ok.prevent='handleOk',
)
b-form
b-form-group(
:label='$t("strings.name")'
label-for='name',
)
b-form-input#name(
type='text',
v-model='form.name',
maxlength='191',
required,
)
</template>
Your code seems incomplete to me. As per my guess, after your form submit, you should empty your form data. Means, at the end of addItem(this.form), this.editItem(this.form), setModalVisible(false) these methods, You should empty your this.form data or nullified form's properties. Like,
this.form = {}
or
this.form.name = null
After completing action from your api, try to empty or null your Datas related to that form.
editItem (form) {
// work with your backend
this.form = {}
}
I have a form in which I need to call two action methods, one after the other. This is how the flow goes.
First I check if the prerequisite data is entered by the user. If not then I show a message that user needs to enter the data first.
If all the prerequisite data is entered, I call an action method which return data. If there is no data returned then I show a message "No data found" on the same page.
If data is returned then I call another action method present in a different controller, which returns a view with all the data, in a new tab.
The View:
#using (Ajax.BeginForm("Index", "OrderListItems", null, new AjaxOptions { OnBegin = "verifyRequiredData"}, new { #id = "formCreateOrderListReport", #target = "_blank" }))
{
//Contains controls and a button
}
The Script in this View:
function verifyRequiredData() {
if ($("#dtScheduledDate").val() == "") {
$('#dvValidationSummary').html("");
var errorMessage = "";
errorMessage = "<span>Please correct the following errors:</span><ul>";
errorMessage += "<li>Please enter Scheduled date</li>";
$('#dvValidationSummary').append(errorMessage);
$('#dvValidationSummary').removeClass('validation-summary-valid').addClass('validation-summary-errors');
return false;
}
else {
$('#dvValidationSummary').addClass('validation-summary-valid').removeClass('validation-summary-errors');
$('#dvValidationSummary').html("");
$.ajax({
type: "GET",
url: '#Url.Action("GetOrderListReport", "OrderList")',
data: {
ScheduledDate: $("#dtScheduledDate").val(),
Crews: $('#selAddCrewMembers').val(),
Priorities: $('#selPriority').val(),
ServiceTypes: $('#selServiceTypes').val(),
IsMeterInfoRequired: $('#chkPrintMeterInfo').val()
},
cache: false,
success: function (data) {
debugger;
if (data !== "No data found") {
//var newUrl = '#Url.Action("Index", "OrderListItems")';
//window.open(newUrl, '_blank');
return true;
} else {
//Show message "No data found"
return false;
}
}
});
return false;
}
}
The "GetOrderListReport" Action method in "OrderList" Controller:
public ActionResult GetOrderListReport(OrderListModel model)
{
var contract = new OrderReportDrilldownParamDataContract
{
ScheduledDate = model.ScheduledDate
//Setting other properties as well
};
var result = OrderDataModel.GetOrderList(contract);
if (string.IsNullOrWhiteSpace(result) || string.IsNullOrEmpty(result))
{
return Json("No data found", JsonRequestBehavior.AllowGet);
}
var deserializedData = SO.Core.ExtensionMethods.DeserializeObjectFromJson<OrderReportDrilldownDataContract>(result);
// send it to index method for list
TempData["DataContract"] = deserializedData;
return Json(deserializedData, JsonRequestBehavior.AllowGet);
}
The last action method present in OrderListItems Controller, the result of which needs to be shown in a new tab:
public ActionResult Index()
{
var deserializedData = TempData["DataContract"] as OrderReportDrilldownDataContract;
var model = new OrderListItemViewModel(deserializedData);
return View(model);
}
The problem is that I am not seeing this data in a new tab, although I have used #target = "_blank" in the Ajax.BeginForm. I have also tried to use window.open(newUrl, '_blank') as can be seen above. But still the result is not shown in a new tab.
Please assist as to where I am going wrong?
If you are using the Ajax.BeginForm you shouldn't also be doing an ajax post, as the unobtrusive ajax library will automatically perform an ajax post when submitting the form.
Also, if you use a view model with data annotation validations and client unobtrusive validations, then there would be no need for you to manually validate the data in the begin ajax callback as the form won't be submitted if any validation errors are found.
The only javascript code you need to add in this scenario is a piece of code for the ajax success callback. That will look as the one you currently have, but you need to take into account that opening in new tabs depends on the browser and user settings. It may even be considered as a pop-up by the browser and blocked, requiring the user intervention to allow them as in IE8. You can give it a try on this fiddle.
So this would be your model:
public class OrderListModel
{
[Required]
public DateTime ScheduledDate { get; set; }
//the other properties of the OrderListModel
}
The form will be posted using unobtrusive Ajax to the GetOrderListReport of the OrderList controller. On the sucess callback you will check for the response and when it is different from "No data found", you will then manually open the OrderListItems page on a new tab.
This would be your view:
#model someNamespace.OrderListModel
<script type="text/javascript">
function ViewOrderListItems(data){
debugger;
if (data !== "No data found") {
var newUrl = '#Url.Action("Index", "OrderListItems")';
//this will work or not depending on browser and user settings.
//passing _newtab may work in Firefox too.
window.open(newUrl, '_blank');
} else {
//Show message "No data found" somewhere in the current page
}
}
</script>
#using (Ajax.BeginForm("GetOrderListReport", "OrderList", null,
new AjaxOptions { OnSucces= "ViewOrderListItems"},
new { #id = "formCreateOrderListReport" }))
{
#Html.ValidationSummary(false)
//input and submit buttons
//for inputs, make sure to use the helpers like #Html.TextBoxFor(), #Html.CheckBoxFor(), etc
//so the unobtrusive validation attributes are added to your input elements.
//You may consider using #Html.ValidationMessageFor() so error messages are displayed next to the inputs instead in the validation summary
//Example:
<div>
#Html.LabelFor(m => m.ScheduledDate)
</div>
<div>
#Html.TextBoxFor(m => m.ScheduledDate, new {id = "dtScheduledDate"})
#Html.ValidationMessageFor(m => m.ScheduledDate)
</div>
<input type="submit" value="Get Report" />
}
With this in place, you should be able to post the data in the initial page using ajax. Then based on the response received you will open another window\tab (as mentioned, depending on browser and user settings this may be opened in a new window or even be blocked) with the second page content (OrderListItems).
Here's a skeleton of what I think you are trying to do. Note that window.open is a popup though and most user will have popups blocked.
<form id="formCreateOrderListReport">
<input type="text" vaule="testing" name="id" id="id"/>
<input type="submit" value="submit" />
</form>
<script type="text/javascript">
$('#formCreateOrderListReport').on('submit', function (event) {
$.ajax({
type: "POST",
url: '/home/test',
data: { id: $('#id').val()},
cache: false
}).done(function () {
debugger;
alert("success");
var newUrl = '/home/contact';
window.open(newUrl, '_blank');
}).fail(function () {
debugger;
alert("error");
});
return false;
});
</script>
Scale down the app to get the UI flow that you want then work with data.
My controller action is being executed twice. Fiddler shows two requests and responses, and for the first one has an icon that indicates "Session was aborted by the client, Fiddler, or the Server."
But I can't figure out where this is happening, or why.
Here are the specifics:
I have a section of a view (ThingFinancials) that looks like this:
#{ using (Html.BeginForm("ConfirmThing", "Thing", null, FormMethod.Get, new { id = "frmGo" }))
{
#Html.HiddenFor(model => model.ThingID)
<button id="btnGo">
Thing is a Go - Notify People</button>
}
}
The javascript for btnGo looks like this:
$("#btnGo").click(function () {
var form = $("#frmGo");
form.submit();
});
The action (stripped down) looks like this:
public ActionResult ConfirmThing(int thingID)
{
[do some database stuff]
[send some emails]
var financials = GetFinancials(thingID);
return View("ThingFinancials", financials);
}
The only thing that looks unusual to me is that the URL you'd see would start out as [Website]/Thing/ThingFinancials/47, and after submission the URL would be [Website]/Thing/ConfirmThing?ThingID=47.
(If you're wondering why the Action name doesn't match the View name, it's because there are multiple form tags on ThingFinancials, and they can't all have the same action name.)
Is there a Server.Transfer happening behind the scenes, or something like that?
If you are using a submit button then you need to cancel the default behaviour when submitting with javascript, otherwise you will submit it twice. Try this:
$("#btnGo").click(function () {
var form = $("#frmGo");
// event.preventDefault(); doesn't work in IE8 so do the following instead
(event.preventDefault) ? event.preventDefault() : event.returnValue = false;
form.submit();
});
Your int thingID is a query string parameter that stays with the request. At the end of ActionResult ConfirmThing(int thingID), all you're doing is returning a view. If you'd rather see the clean URL ([Website]/Thing/ThingFinancials/47) you can make the following changes.
public ActionResult ConfirmThing(int thingID)
{
[do some database stuff]
[send some emails]
// This logic is probably in the 'ThingFinancials' action
// var financials = GetFinancials(thingID);
// I'll assume we're in the same controller here
return RedirectToAction("ThingFinancials", new { thingID });
}
This is because of your jquery event just add stopImmediatePropagation() to your jquery event.
$("#btnGo").click(function (event){
event.stopImmediatePropagation();
});