Feedback

Customizing Layout

#Element Layout

The basic concept you need to know about an element's layout is that it has 3 different parts:

  1. The first is element itself which acts like a container.
  2. The second is label which renders the label if there's any.
  3. The third is field where the element component will be placed.

Here it is more visually:

element
label
field

Each time an element is used, whether it's regular or nested will follow the same pattern. In case of a nested element the field part will contain all other elements:

element
label
field
element
label
field
element
label
field

Labels of course are not always meant to be next to the input field, so let's see how size can be defined for different parts of the element layout.

#Defining Column Sizes

Most modern CSS frameworks use columns to easily define the width of different parts of elements. Laraform aims to accommodate to this concept by having columns property for it's elements.

The simplest way to define the element size is by adding a columns property to it's schema as a single number:

public function schema() {
  return [
    'name' => [
      'type' => 'text',
      'label' => 'Name',
      'columns' => 6  
    ]
  ];
}

This will render the element 6 columns wide, which is 50% of a 12 column grid theme (like Bootstrap by default).

public function schema() {
  return [
    'name' => [
      'type' => 'text',
      'label' => 'Name',
      'columns' => [
        'element' => 6,
        'label' => 4,
        'field' => 8
      ]
    ]
  ];
}

In this case the element will be the same 6 columns wide, but now label will take up 4 columns of the element's space and field will be 8 columns.

When defining label and field you set the column size relative to the element which contains both of them.

Defining Column Sizes For Different Breakpoints

Of course it's not enough to set static column sizes for our elements, we need to think about responsivity. To tackle this Laraform provides a simple way to define column sizes for different breakpoints:

'columns' => [
  'element' => [
    'md' => 6,
    'sm' => 12
  ]
]

Breakpoints are defined by the theme uniquely for different CSS frameworks.

Defining Column Sizes For Form

It would be a nightmare to define column sizes for each element in our form, so luckily we can do that on a form level too:

<?php

namespace App\Forms;

class ColumnsForm extends \Laraform
{
  public $columns = [
    'element' => [
      'md' => 6,
      'sm' => 12
    ]
  ];

  public function schema() {
    // ...
  }
}

This form will have all of it's elements in full width while label sizes specified for different breakpoints. Elements which have their own columns specified will override form rules.

Defining Column Sizes Globally

It's not much fun to define column sizes for even each of our forms if we are building an admin panel with 50+ forms for example. Therefore you may define default column sizes in global Configuration:

/*
|--------------------------------------------------------------------------
| Columns
|--------------------------------------------------------------------------
|
| Default column settings.
|
*/
'columns' => [
    'element' => 12,
    'label' => 12,
    'field' => 12,
],

Of course if a form or an element has it's own columns property defined the default config values will be overwritten by them.

Rendering Column Classes

In fact when you set the columns as defined above the final result will be some extra classes added to the element based on the theme's column definition pattern. In Bootstrap 3 for example this pattern is col-${breakpoint}-${size}. So column sizes are transformed to classes and added to different parts of the element layout in templates the theme provides.

That's all you need to know right now, if you want to understand themes better check out Style & Theme chapter and see the next section to learn more about binding classes the element layout.

#Element Slots

Basically each part of the elements, like label, error message or the field itself can be replaced via slots. To demonstrate this let's look at an example:

<template>
  <form submit.prevent="submit">
    <TextElement name="code" :schema="schema.code" v-ref:elements$>
        <label
          slot="label"
          slot-scope="{ el$ }"
          :for="el$.id"
        >{{ el$.label }}</label>
    </TextElement>
  </form>
</template>

<script>
  export default {
    mixins: [Laraform],
    data: () => ({
      schema: {
        code: {
          type: 'text',
          label: 'Code'
        }
      }
    })
  }
</script>

In this scenario we are using inline element and we have an easy job using slots. You can find the list of available slots at each element's API reference and the common ones at element$.

As elements are usually not inline, but defined in the schema, let's see how we can still use slots in them:

export default {
  mixins: [Laraform],
  data: () => ({
    schema: {
      code: {
        type: 'text',
        label: 'Code',
        slots: {
          label: Vue.extend({
            props: ['el$'],
            template: `
              <label
                slot="label"
                slot-scope="{ el$ }"
                :for="el$.id"
              >{{ el$.label }}</label>
            `
          })
        }
      }
    }
  })
}

Using the slots option of the element will enable you to achieve the same results by creating components on the fly as if they were inline slots.

#Form Layout

By default Laraform renders a complete form with all the elements included in schema and also helper components for displaying errors, showing buttons, etc. In case you need to define a less straightforward form with unique template you might render a custom structure, or even elements or groups on their own as you'll see later in this section.

#Custom Form Layout

When you create a new form component using Laraform mixin, it uses a default <template> Laraform provides. You can override it simply and create more advanced structure, if you need to:

<template>
  // Your template
</template>

<script>
  export default {
    mixins: [Laraform],
    // ...
  }
</script>

Check out @laraform/laraform/src/components/Laraform.vue to see how the default template looks like.

#Inline Elements

When form loops through it's schema property actually what it does is that it renders element components and pass on their element schema and name. For example a text type will render <TextElement> a group a <GroupElement> and so on. As you will see in the following, these elements can also used as inline components within the form.

Rendering Single Elements

Laraform renders all elements defined in schema by default. This is not the neccesarily the expected result you always want to have, so let's see how you can render elements one by one and place whatever else you want in a form's template.

Let's create a simple form with basic fields at resources/js/components/forms/SingleElementsForm.vue

<script>
  export default {
    mixins: [Laraform],
    data: () => ({
      schema: {
        name: {
          type: 'text',
          label: 'Name'
        },
        email: {
          type: 'text',
          label: 'Email'
        },
        phone: {
          type: 'text',
          label: 'Phone'
        }
      }
    })
  }
</script>

If everything stays like that, this form will render the elements under each other as you would expect.

Now let's change our form and define a custom <template> where we place some custom HTML along with inline elements:

<template>
  <form
    @submit.prevent="submit"
  >
    <h5>Personal details</h5>
    <TextElement
      name="name"
      :schema="schema.name"
      v-ref:elements$ 
    />
    <h5>Contact details</h5>
    <TextElement
      name="email"
      :schema="schema.email"
      v-ref:elements$ 
    />
    <TextElement
      name="phone"
      :schema="schema.phone"
      v-ref:elements$ 
    />
  </form>
</template>

<script>
  export default {
    mixins: [Laraform],
    data: () => ({
      schema: {
        name: {
          type: 'text',
          label: 'Name'
        },
        email: {
          type: 'text',
          label: 'Email'
        },
        phone: {
          type: 'text',
          label: 'Phone'
        }
      }
    })
  }
</script>

Let's see what we have here. First, there's a <form> tag which will invoke the form's .submit().

Then we have <h5> headers and <TextElement> components which have two attributes: name and schema. The name must be the same as the element's key in the schema, while :schema references the element's schema.

And there is the v-ref directive, which is a custom directive enabling us to collect multiple elements under the same reference. This special directive must be used for every single element component, because that's how Laraform knows they should be part of the form.

When using inline elements like <TextElement> always make sure to add the v-ref:elements$ directive, because that's how Laraform knows they are part of the form.

So what all of this gives us is a nice form where fields are placed under different headers:

          
<template>
  <form
    @submit.prevent="submit"
  >
    <h5>Personal details</h5>
    <TextElement
      name="name"
      :schema="schema.name"
      v-ref:elements$ 
    />
    <h5>Contact details</h5>
    <TextElement
      name="email"
      :schema="schema.email"
      v-ref:elements$ 
    />
    <TextElement
      name="phone"
      :schema="schema.phone"
      v-ref:elements$ 
    />
  </form>
</template>

<script>
export default {
  mixins: [Laraform],
  data: () => ({
    schema: {
      name: {
        type: 'text',
        label: 'Name'
      },
      email: {
        type: 'text',
        label: 'Email'
      },
      phone: {
        type: 'text',
        label: 'Phone'
      }
    }
  })
}
</script>
        

This is great, but does it really make sense to render the elements under Contact details one by one? Wouldn't it be better to render a single element which collects all of them so we don't have to add them manually? That's where using a Group element can help us!

Rendering Element Groups

We've already discussed Group element previously where we used it as a form of nesting elements. It's specialty was that it can collect elements but does not modify their data model. It can also be very useful to render more elements when used as an inline element.

Let's change our previous example a bit and create a group element type for the contact details:

          
<template>
  <form
    @submit.prevent="submit"
  >
    <h5>Personal details</h5>
    <TextElement
      name="name"
      :schema="schema.name"
      v-ref:elements$ 
    />
    <h5>Contact details</h5>
    <GroupElement
      name="contect"
      :schema="schema.contact"
      v-ref:elements$ 
    />
  </form>
</template>

<script>
export default {
  mixins: [Laraform],
  data: () => ({
    schema: {
      name: {
        type: 'text',
        label: 'Name'
      },
      contact: {
        type: 'group',
        schema: {
          email: {
            type: 'text',
            label: 'Email'
          },
          phone: {
            type: 'text',
            label: 'Phone'
          }
        }
      }
    }
  })
}
</script>
        

That's pretty much it. As you can see nothing has changed, we just simplified our template. Now instead of adding the email and phone elements to the template one by one, we added the contact group which contains these two elements.