Vuestorefront + Magento 2: How to create a contact form and display it on the contact page
Before we begin, it’s assumed that you have basic Vue and Magento 2 knowledge.
By default, Vuestorefront doesn’t include a form on the contact page. Here, we’ll discuss how to create one in simple steps.
Create a contact form component in Vuestorefront and display it on the Contact Us page.
The contact page is a CMS page, as the main contents are loaded from the Magento 2 CMS. In your Vuestorefront project directory, locate the CMS.vue component in the pages directory.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 | v-if="page && page.content && page.identifier !== 'contact-us'" :content="page.content" /> <ContactPage v-if="page && page.identifier === 'contact-us'" :content="page.content" /> replace the HTMLContent tag with the above, This is for loading the contact form from different component if the page url/identifier = 'contact-us'. add the following code to the js part import the ContactPage component import ContactPage from '~/components/ContactPage.vue'; In components/ContactPage.vue <template> <div class="contact-wrapper"> <div class="contact-info"> <component :is="tag" v-html="$dompurify(content)" /> </div> <ContactForm data-testid="contact-form" :form="contactForm" :loading="isSubmitting" @submit="onContactFormSubmit" /> </div> </template> <script lang="ts"> import { ref, defineComponent } from '@nuxtjs/composition-api'; import { useApi } from '~/composables/useApi'; import { useUiNotification } from '~/composables/useUiNotification'; import ContactForm from './ContactForm/ContactForm.vue'; import { ContactFormFields } from './ContactForm/types'; export default defineComponent({ name: 'ContactPage', components: { ContactForm }, props: { tag: { type: String, default: 'div', }, content: { type: String, default: '', }, }, setup(_, { emit }) { const { mutate } = useApi(); const { send: sendNotification } = useUiNotification(); const contactForm = ref < ContactFormFields > ({ email: '', name: '', telephone: '', comment: '', }); const isSubmitting = ref(false); const observer = ref(null); const onContactFormSubmit = async (form: ContactFormFields) => { contactForm.value = form; isSubmitting.value = true; const request = ` mutation SubmitContactForm($name: String!, $email: String!, $comment: String!, $telephone: String!) { submitContactForm(input: { name: $name, email: $email, comment: $comment, telephone: $telephone }) { success message } } `; const variables = { name: form.name, email: form.email, comment: form.comment, telephone: form.telephone, }; const { data, errors } = await mutate(request, variables); if (errors) { console.log('GraphQL Errors:', errors); // Log GraphQL errors errors.forEach((error, i) => sendNotification({ icon: 'error', id: Symbol(`contact_error-${i}`), message: error.message, persist: false, title: 'OTP Login error', type: 'danger', })); }else{ sendNotification({ id: Symbol(`contact_success`), message: 'Thank you for reaching out! Your message has been successfully sent. We will get back to you shortly.', persist: false, title: 'Success', type: 'success', icon: 'check', }); contactForm.value = { email: '', name: '', telephone: '', comment: '', }; // Reset the validation state if (observer.value) { observer.value.reset(); } isSubmitting.value = false; } }; return { contactForm, onContactFormSubmit, isSubmitting, }; }, }); </script> <style lang="scss" scoped> .contact-wrapper { display: flex; margin-bottom: var(--spacer-2xl); gap: var(--spacer-2xl); width: 100%; @media (max-width: 1024px) { gap: var(--spacer-xl); margin-top: var(--spacer-xl); flex-direction: row-reverse; flex-wrap: wrap; } .contact-info { width: 30%; background-color: var(--c-light); padding: var(--spacer-2xl); @media (max-width: 1024px) { order: 2; padding: var(--spacer-base); } } .contact-form { width: 40%; @media (max-width: 1024px) { order: 1; } } @media (max-width: 1024px) { .contact-info, .contact-form { width: 100%; } } .msg-field { width: 100%; margin-top: var(--spacer-sm); } .contact-btn { margin-top: var(--spacer-xl); } } </style> |
In the code above, we have added the necessary codes to display the contents from the backend. Additionally, we have added a new component to display the form. In JavaScript, we have added the validations and handlers for the form submission actions. After submission, the form will reset and show a success notification.
Please refer to the form component located in components/ContactForm/ContactForm.vue for more details.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 | <template> <div class="contact-form"> <h2>Get in touch</h2> <p>Please fill out the quick form and we will be in touch with lightning speed</p> <ValidationObserver v-slot="{ handleSubmit }"> <form @submit.prevent="handleSubmit(onSubmit)"> <div class="flex flex-col gap-y-5"> <ValidationProvider v-slot="{ errors }" rules="required"> <SfInput v-model="formCopy.name" size="sm" aria-label="Enter your name" placeholder="Enter your name" required /> <div class="sf-input__error-message"><div aria-live="assertive">{{ errors[0] }}</div></div> </ValidationProvider> <ValidationProvider v-slot="{ errors }" rules="required|email"> <SfInput v-model="formCopy.email" size="sm" aria-label="Enter your email" placeholder="Enter a valid email address" required /> <div class="sf-input__error-message"><div aria-live="assertive">{{ errors[0] }}</div></div> </ValidationProvider> <ValidationProvider v-slot="{ errors }" rules="required"> <SfInput v-model="formCopy.telephone" size="sm" aria-label="Enter mobile number" placeholder="Enter mobile number" required /> <div class="sf-input__error-message"><div aria-live="assertive">{{ errors[0] }}</div></div> </ValidationProvider> <SfInput v-model="formCopy.location" size="sm" aria-label="Enter your location" placeholder="Enter your location" required /> <ValidationProvider v-slot="{ errors }" rules="required"> <label> <SfTextarea v-model="formCopy.comment" placeholder="Enter your message" class="msg-field" /> <div class="sf-input__error-message"><div aria-live="assertive">{{ errors[0] }}</div></div> </label> </ValidationProvider> <p class="typography-hint-xs text-disabled-500 mt-0.5"> By continuing/submitting, I agree to the <a href="/terms-of-service" target="_blank" style="color:blue">T&C</a> and <a href="/privacy-policy" target="_blank" style="color:blue">Privacy Policy</a> and I am giving my consent to receive updates through sms/email. </p> <SfButton v-e2e="'contact-submit'" type="submit" class="sf-button--full-width form__button" :disabled="loading" data-testid="login-form-submit" > <SfLoader :class="{ loader: loading }" :loading="loading" > <div>{{ $t('Submit') }}</div> </SfLoader> </SfButton> </div> </form> </ValidationObserver> </div> </template> <script lang="ts"> import { defineComponent, ref, watch } from '@nuxtjs/composition-api'; import { PropType } from '@nuxtjs/composition-api'; import { ValidationProvider, ValidationObserver, extend } from 'vee-validate'; import { SfInput, SfButton, SfTextarea } from '@storefront-ui/vue'; import { required, email } from 'vee-validate/dist/rules'; import { ContactFormFields } from './types'; extend('email', { ...email, message: 'Invalid email', }); extend('required', { ...required, message: 'This field is required', }); export default defineComponent({ name: 'ContactForm', components: { SfInput, SfButton, SfTextarea, ValidationObserver, ValidationProvider }, props: { form: { type: Object as PropType < ContactFormFields > , required: true, }, loading: { type: Boolean, default: false, }, tag: { type: String, default: 'div', }, content: { type: String, default: '', }, }, setup(props, { emit }) { const formCopy = ref < ContactFormFields > ({ ...props.form }); watch( () => props.form, (newForm) => { formCopy.value = { ...newForm }; }, { immediate: true, deep: true } ); const onSubmit = () => { emit('submit', formCopy.value); }; return { formCopy, onSubmit, }; }, }); </script> |
Here we are adding components/ContactForm/types.ts for the ContactFormFields to be sent over Magento 2 API via GraphQL mutation.
1 2 3 4 5 6 7 8 9 10 11 12 13 | export type ContactFormFields = { Â Â Â Â email: string, Â Â Â Â name: string, Â Â Â Â telephone: string, Â Â Â Â comment: string, }; Â Â export type FormName = 'contact'; |
The Vue Storefront part has been completed. Once you run the yarn build in your Vue Storefront, the form will be displayed on the “Contact Us” page. Next, we need to create a new module to handle the contact form mutation. Create a custom module and add the following schema.graphqls file in the etc folder of the Magento 2 module.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | etc/schema.graphqls type Mutation { submitContactForm(input: ContactFormInput!): ContactFormOutput @resolver(class: "Hatslogic\\ContactForm\\Model\\Resolver\\SubmitContactForm") @doc(description: "Recieve the details from contact form and send mail to admin.") } input ContactFormInput { name: String! email: String! telephone: String! comment: String! } type ContactFormOutput { success: Boolean message: String } Next define the Hatslogic/ContactForm/Model/Resolver, <?php namespace Hatslogic\ContactForm\Model\Resolver; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\Mail\Template\TransportBuilder; use Magento\Framework\Translate\Inline\StateInterface; use Magento\Store\Model\StoreManagerInterface; use Magento\Framework\App\Config\ScopeConfigInterface; use Psr\Log\LoggerInterface; use Magento\Framework\App\ObjectManager; class SubmitContactForm implements ResolverInterface { protected $transportBuilder; protected $inlineTranslation; protected $storeManager; protected $scopeConfig; protected $logger; public function __construct( TransportBuilder $transportBuilder, StateInterface $inlineTranslation, StoreManagerInterface $storeManager, ScopeConfigInterface $scopeConfig, LoggerInterface $logger ) { $this->transportBuilder = $transportBuilder; $this->inlineTranslation = $inlineTranslation; $this->storeManager = $storeManager; $this->scopeConfig = $scopeConfig; $this->logger = $logger; } public function resolve( Field $field, $context, ResolveInfo $info, array $value = null, array $args = null ) { $this->logger->info('SubmitContactForm resolver called'); $input['data'] = $args['input']; $this->logger->info('Input data: ' . json_encode($input)); try { $this->sendEmail($input); $this->logger->info('Email sent successfully'); } catch (\Exception $e) { $this->logger->error('Error sending email: ' . $e->getMessage()); return [ 'success' => false, 'message' => 'Error submitting form' ]; } return [ 'success' => true, 'message' => 'Form submitted successfully' ]; } protected function sendEmail($input) { $this->inlineTranslation->suspend(); $store = $this->storeManager->getStore()->getId(); $recipientEmail = $this->scopeConfig->getValue( 'contact/email/recipient_email', \Magento\Store\Model\ScopeInterface::SCOPE_STORE, $store $templateIdentifier = $this->scopeConfig->getValue( 'contact/email/email_template', \Magento\Store\Model\ScopeInterface::SCOPE_STORE, $store ) ?: '15'; $transport = $this->transportBuilder ->setTemplateIdentifier($templateIdentifier) ->setTemplateOptions(['area' => 'frontend', 'store' => $store]) ->setTemplateVars($input) ->setFrom('general') ->addTo($recipientEmail) ->getTransport(); $transport->sendMessage(); $this->inlineTranslation->resume(); } } |
Conclusion
You are done. Now the process is complete, you have to run the below commands in the Magento 2 root directory to activate the module:
1 2 3 4 5 6 7 | php bin/magento setup:upgrade php bin/magento setup:di:compile php bin/magento cache:clear php bin/magento cache:flush |
If you are looking for a technical expert to handle this feel free to Contact us.
Recent help desk articles
Greetings! I'm Aneesh Sreedharan, CEO of 2Hats Logic Solutions. At 2Hats Logic Solutions, we are dedicated to providing technical expertise and resolving your concerns in the world of technology. Our blog page serves as a resource where we share insights and experiences, offering valuable perspectives on your queries.