Feedback Laraform is now in maintenance mode. Read more on our GitHub.

Dynamic Form Loading

#When To Use?

We've learned in Rendering chapter that if we want to render a backend form we need to use render() function in blade template:

<div id="app">
  {!! $form->render() !!}
</div>

While this is true, it can become quite cumbersome to load backend forms within components.

Let's say we render a login-page component in our blade template:

<div id="app">
  <login-page></login-page>
</div>

That looks like this:

// LoginPage.vue

<template>
  <div class="login-page">
    <login-form/>
  </div>
</template>

<script>
  export default {
    name: 'LoginPage',
    // ...
  }
</script>

This login-page component has a LoginForm which should render the actual login form but should also be able to authenticate the user once submitted. Therefore we need to use a backend form for sure, we're just not sure how to pass that to the component.

Our first idea is to pass it as a <slot>, but then we realize that once the component will have more children and the form is needed to be passed down to them we'll pull our hair out. Also we'll not be able to use the form's API because we can't use ref on a <slot>.

So we come up with the idea to load the form dynamically from the backend, that will enable us to render the form within a component but still relying on its backend features. Let's see how we can do that.

#Set Up Backend

Create Controller & Routing

First we need to have a controller, that will serve our forms to the backend. Let's create app/Http/Controllers/FormController.php:

<?php

namespace App\Http\Controllers;

use App\Forms\LoginForm;

class FormController extends Controller
{
  public $forms = [
    'login-form' => LoginForm::class
  ];

  public function load($form) {
    $formInstance = app($this->forms[$form]);

    return [
      'component' => $formInstance->getComponent(),
      'data' => $formInstance,
    ];
  }
}

We're going to define the forms that can be served by the backend within the $forms property. We've already added LoginForm that we're going to create soon.

Before that let's add a route to routes/web.php that will handle the endpoint:

Route::get('/form/{form}', 'FormController@load');

Create Login Form

Let's create app\Forms\LoginForm.php that we'll use as our login form:

<?php

namespace App\Forms;

class LoginForm extends \Laraform
{
  public $component = 'login-form';

  public function schema() {
    return [
      'email' => [
        'type' => 'text',
        'placeholder' => 'Email address',
        'floating' => 'Email address',
        'rules' => 'email'
      ],
      'password' => [
        'type' => 'password',
        'placeholder' => 'Password',
        'floating' => 'Password'
      ],
      'remember' => [
        'type' => 'checkbox',
        'text' => 'Remember me'
      ]
    ];
  }

  public function buttons() {
    return [[
      'label' => 'Login',
      'class' => 'btn-primary'
    ]];
  }

  public function after () {
    if (\Auth::attempt([
      'email' => $this->data['email'],
      'password' => $this->data['password']
    ], $this->data['remember'])) {
      return $this->success('You are authenticated!');
    }

    return $this->fail('Authentication failed');
  }
}

This has everything we need for login, elements, login button and the authentication logic in after hook.

#Create FormLoader

Now we have all we need on the backend, so let's create our form loader that we'll use to load the form dynamically.

Let's create a new component at resources/js/components/FormLoader.vue:

<template>
  <component
    v-if="formComponent"
    :is="formComponent"
    :form="formData"
    @mounted="handleFormMounted"
    ref="form$"
  />
</template>

<script>
  export default {
    props: {
      form: {
        type: String,
        required: true
      }
    },
    data() {
      return {
        endpoint: '/form/{form}',
        formComponent: null,
        formData: {},
        form$: {},
      }
    },
    created() {
      axios.get(this.endpoint.replace('{form}', this.form)).then(({data}) => {
        this.formComponent = data.component
        this.formData = data.data
      })
    },
    methods: {
      handleFormMounted() {
        this.form$ = this.$refs.form$

        this.$emit('load', this.form$)
      }
    }
  }
</script>

As you can see when the component is created it sends a GET request for the backend to receive the form's properties that we need in order to render the form.

We receive a component property that will define what type of component should the form use. We also get a data property which contains all the information regarding our form, eg. schema, buttons, theme, etc. We pass this as :form property to our component, just like render() would do when using $form->render() in blade template.

We're also listening to loaded event, which is emitted by Laraform when the form is fully loaded so that we can add custom logic upon that.

We're setting form$ data property so that the form's API can be accessed and certain actions like update() or submit() can be performed remotely.

This is all we need right now, let's move on to creating the remaining components.

#Create Components

Create Frontend Form

As we're using login-form as the $component in our form, we need to create resources/js/components/forms/LoginForm.vue:

<script>
  export default {
    mixins: [Laraform],
    mounted() {
      console.log('LoginForm is mounted')
    }
  }
</script>

Create LoginPage Component

We need the actual LoginPage component that we're going to use in the blade template. Let's create that at resources/js/components/LoginPage.vue and place the <form-loader> component we created previously within:

<template>
  <div class="login-page">
    <form-loader
      form="login-form"
      @load="handleLoginFormLoaded"
    />
  </div>
</template>

<script>
  export default {
    name: 'LoginPage',
    methods: {
      handleLoginFormLoaded(form$) {
        console.log('LoginForm is loaded')
        console.log(form$)
      }
    }
  }
</script>

We're going to load the login-form backend form which points to the $forms property in FormController.php:

public $forms = [
  'login-form' => LoginForm::class
];

We're also going to console log some message to see if everything is working well.

#Put Them Together

Include Components In app.js

The last thing we need to do is to add our LoginPage.vue, LoginForm.vue and FormLoader.vue to our app.js or main JS file.

import Vue from 'vue'
import Laraform from '@laraform/laraform'

import LoginPage from './components/LoginPage'
import FormLoader from './components/FormLoader'
import LoginForm from './components/forms/LoginForm'

Vue.use(Laraform)

Vue.component('login-page', LoginPage)
Vue.component('form-loader', FormLoader)
Vue.component('login-form', TestForm)

const app = new Vue({
  el: '#app'
})

Render Template

Now let's add the login-page component to our blade template:

<div id="app">
  <login-page></login-page>
</div>

Compile & Run

To finish compile your assets:

npm run dev

If you refresh page you should see the form loading after the backend sends the response. The console log should also be like:

LoginForm is loaded
VueComponent {_uid: 2, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: VueComponent, …}
LoginForm is mounted

#Extending

If you want to load other forms dynamically just simply extend the $forms property in the FormController, eg:

public $forms = [
  'login-form' => LoginForm::class,
  'forgotten-password-form' => ForgottenForm::class,
  // ...
];

And use then in your component with the same <form-loader>:

<template>
  <div class="login-page">

    <div class="login">
      <form-loader
        form="login-form"
        @load="handleLoginFormLoaded"
      />
    </div>

    <div class="forgotten-password">
      <form-loader
        form="forgotten-password-form"
        @load="handleForgottenPasswordFormLoaded"
      />
    </div>

  </div>
</template>

This is just a very basic implementation of dynamic form loading to give you an idea how it can be achieved. You are free to create your own solution or extend this to cover all your needs.