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.