TYPO3-Headless / nuxt-typo3

TYPO3 Frontend rendered in Vue.js and Nuxt (frontend for EXT:headless)
https://t3headless.macopedia.io/nuxt-typo3/
90 stars 35 forks source link

middleware - POST-Requests #142

Closed kubilaymelnikov closed 3 years ago

kubilaymelnikov commented 3 years ago

Introduction:

We are missing the functionality to send POST-request via router. In this particular project we have a multi-page form in TYPO3. There is no way to handle TYPO3 responses and a way to deal with POST requests in combination with the routing. This problem probably affects other areas (for example news pagination).

Example problem:

form-with-steps

My current solution is to put the response in the store:

this.$typo3.api.$http
  .$post(this.link.url, this.getFormData())
  .then(data => {
    this.toggleState('loading')
    this.toggleState('success')

    if (this.slug !== data.page.navigation.slug) {
      setTimeout(() => {
        this.$router.push(data.page.navigation.slug)
      }, 400)
    } else {
      this.resetState()
      this.$store.commit(SET_PAGE, data)
    }
  })
  .catch(err => {
    console.log(err)
    this.toggleState('loading')
    this.toggleState('error')
  })

To refresh the page I had to make changes to the way page content is handed over:

// mixins/page/default.js
computed: {
  ...mapState({
    page: state => state.typo3.page
  })
},
asyncData({ pageContent, backendLayout }) {
  if (pageContent) {
    return {
      backendLayout
    }
  }
}

This however, has two negative side effects:

  1. routing is not being used.
  2. page transitions are not possible anymore, since all pages obtain their new content from the store (A possible solution could be to scope the states via the URL, then one could check in the middleware if the page has already been loaded)

Possible Solution

Add a postPage(path, data) here: https://github.com/TYPO3-Initiatives/nuxt-typo3/blob/6bde261c1ee20809f633f92ff5080f590031d3fe/lib/templates/lib/api.js#L36-L44

Check if parameters have been handed over and call the respective action at this point: https://github.com/TYPO3-Initiatives/nuxt-typo3/blob/6bde261c1ee20809f633f92ff5080f590031d3fe/lib/templates/middleware/context.js#L35-L62

However, I’m still uncertain how to hand the parameters to the middleware in combination with the router: https://router.vuejs.org/guide/essentials/navigation.html

mercs600 commented 3 years ago

@kubilaymelnikov I see. I'm not sure we are able to call POST method from vue routing on client side, but at this moment I can show you how we have handled it without the performing of the vuex action to setup the new page content.

This is our submit action

  /**
   * UiForm Submit callback
   * @param {UiFormSubmitObject} FormObject
   */
  async onSubmit ({ validator, form, formRef } : UiFormSubmitObject) {
    const { flags, setErrors } = validator
    const { resetForm } = form
    const formData = new FormData(formRef)
    if (flags.valid) {
      this.setState('sending')
      try {
        const page = await this.sendForm(formData, this.link.url)
        this.response = this.findFormElementById(page, this.id)

        if (this.apiResponse?.status === 'success') {
          this.setState('success')
          resetForm()
          return this.onSuccess(this.apiResponse.actionAfterSuccess)
        }
        if (this.apiResponse?.status === 'failure') {
          setErrors(this.prepareErrors(this.apiResponse.errors))
          this.setState('failure')
        }
      } catch (err) {
        this.setState('error')
      }
    }
  },

So we can start from const page = await this.sendForm(formData, this.link.url) which consume formData object and link.url - which is form action.

It looks simple:

    async sendForm (formData: FormData, url: string): Promise<TYPO3.Response.Page> {
      return await (this.$typo3 as TYPO3.Plugin).api.$http.$post(url, formData, {
        headers: { 'Content-Type': 'multipart/form-data' }
      })
    },

we purposely called it page because response from this API call will be whole page object - with page.content field which includes all content elements for this page. So the next step is find the CE responsible for this specific form, because there we can find response (sick I know 😸 ).

 this.response = this.findFormElementById(page, this.id)
   /**
   * Find form content element from page colpos objects
   * @param {TYPO3.Response.Page} TYPO3 page object
   * @param {number} id of current content element
   * @returns {FormContentElement} content elemenet data
   */
  findFormElementById (page: TYPO3.Response.Page, id : number) : FormContentElement {
    let formContent : FormContentElement
    if (Object.values(page.content)) {
      formContent = Object.values(page.content).map(contentArr => contentArr.find(element => element?.id === id))?.[0]
    }
    return formContent
  },

Next we have check what is in the this.apiResponse - this is computed value based on this.response we got from content element responsible for form:

    apiResponse () : FormApi | undefined {
      return this.response?.content?.form?.api
    },

So it response contain success then we can perform action on success this.apiResponse.actionAfterSuccess - this is response from finishers.

    /**
     * Action to call on success
     * @param {ActionAfterSuccess} actionAfterSuccess object
     * @returns {void}
     */
    onSuccess (actionAfterSuccess: ActionAfterSuccess): void {
      if (actionAfterSuccess?.redirectUri) {
        this.$router.push(actionAfterSuccess.redirectUri)
      }

So this is tricky a bit, but doable ;-)

mercs600 commented 3 years ago

let me know if you need more details about it.

kubilaymelnikov commented 2 years ago

@mercs600, thank you for the detailed answer! I forgot that I have written this here...