Using Shared Partials

Last edit: 

Contributors: 

This guide will show you how you can structure your code more efficiently by avoiding unnecessary repetition. Although you haven't written much code so far in this tutorial series, there are already some cases that will benefit greatly from refactoring:

  • using shared partials for fetching common data
  • creating form field partials

Requirements

To follow this tutorial, you should be familiar with basic platformOS concepts, and the topics in the Get Started section. You should have already created a secured admin section on your site, as described in a previous part of this tutorial series.

Steps

Refactoring the views we’ve built so far is a five-step process:

  1. Create shared partial with current user data
  2. Rename current_user query
  3. Update files fetching current_user data
  4. Create shared partials for form fields
  5. Update existing forms

Step 1. Create shared partial with current user data

Some information is reused over and over in your application. When you’ve built the admin panel logic, you had to keep checking over and over again in different places if current user has admin access flag set to true. So far each time you needed to do that, you called the current_user query and checked the appropriate field value. Imagine though, that further down the road in the project, additional business logic is put in place, that changes this condition. You will have to go in, and update all of these places in code.


Start with moving the part that fetches data to a separate partial in the shared namespace.

marketplace_builder/views/partials/shared/get_current_user.liquid


{% query_graph 'get_current_user_data', result_name: "current_user_g" %}

{% if current_user_g.current_user %}
  {{
    current_user_g.current_user
      | assign_to_hash_key: 'has_admin_access', current_user_g.current_user.profile.properties.admin
      | json
  }}
{% else %}
{}
{% endif %}


In the above code snippet you have:

  1. Fetched the data with query
  2. Added a new field has_admin_access to returned object (which is a reference to another field down the properties tree)
  3. Printed it out as JSON

Now, anytime you want to fetch current user data you can go ahead and write


{% parse_json current_user %}
  {% include 'shared/current_user' %}
{% endparse_json %}

You will then have access to the current_user.has_admin_access variable. This example might seem too convoluted, but there are certain upsides to this convention:

  1. Every variable is defined explicitly. By generating / parsing JSON, you avoided creating a variable that magically appears in the parent template, leaving any future developers scratching their heads.
  2. You can extend the logic attached to the current user in one place, and make it available everywhere else.

Step 2. Rename current_user query

Rename query marketplace_builder/graph_queries/user/current_user.liquid to marketplace_builder/graph_queries/user/get_current_user_data.liquid and update the query name inside the file.


Step 3. Update files fetching current_user data

Make changes to the two files that had this logic before.

marketplace_builder/authorization_policies/admin_user.liquid


---
name: admin_user
redirect_to: /unauthorized
flash_alert: Sorry, you have to be an admin user to access this page.
---
{% parse_json current_user %}
  {% include 'shared/get_current_user' %}
{% endparse_json %}
{% if current_user.has_admin_access %}true{% endif %}

marketplace_builder/form_configurations/session/sign_in_form.liquid


---
name: sign_in_form
resource: Session
flash_notice: 'You are now logged in'
redirect_to: >
  {%- parse_json current_user -%}
    {% include 'shared/get_current_user' %}
  {%- endparse_json -%}
  {%- if current_user.has_admin_access -%}
    /admin
  {%- else -%}
    /
  {%- endif -%}
...

Step 4. Create shared partials for form fields

Another prime candidate for refactoring are form field inputs. Form components can be rendered using the form_builder variable, available inside of every form configuration html content section.

This variable contains all information regarding every field - its name, current value, validation rules, and possible errors attached to the field.

Having this in place you can easily build shared partials, that will accept a form_builder.fields entry and generate the required control in a consistent manner.


form_builder.fields sample entry

{
  ...
  "fields": {
    "email": {
      "name": "form[email]",
      "value": "",
      "validation": {
        "errors": ["Please enter your email"],
        "rules": {
          "presence": {
            "message": "Please enter your email"
          },
          "email": {
            "message": "Provided email is invalid"
          }
        }
      },
      "property_options": null
    }
  }
}

Properties explained:

  • name and value represent matching html attributes for any html input
  • validation contains both the rules that should be applied to the input (very useful to add validation on the frontend as you’ll see in just a moment) and actual validation errors if any occurred during the last form submit
  • property_options contains additional configuration options but these do not apply to building shared form input partials

Take a look at how you could structure the shared partial, to make it configurable and use values provided by form_builder:

marketplace_builder/views/partials/forms/fields/text.liquid


{% comment %}
  Required params:
    field: hash
    label: string

  Optional:
    id: string
    type: string
    hint: string
    readonly: boolean
    disabled: boolean
{% endcomment %}

{%- assign _type = type | default: 'text' -%}
{%- assign _error = field.validation.errors.first -%}
{%- assign _default_id = field.name | slugify -%}
{%- assign _id = id | default: _default_id -%}

{%- assign _value = field.value -%}
{%- if _type == 'password' %}
  {% assign _value = '' %}
{%- endif -%}

{%- assign _readonly = readonly | default: false -%}
{%- assign _disabled = disabled | default: false -%}

{%- if field.validation.rules.presence != blank -%}
  {%- assign _required = true -%}
{%- else -%}
  {%- assign _required = false -%}
{%- endif -%}

<div class="form-group">
  {%
    include 'forms/label' with label,
      for_id: _id,
      hint: hint,
      required: _required
  %}

  <input
    type="{{ _type }}"
    class="form-control{% if _error != blank %} is-invalid{% endif %}"
    id="{{ _id }}"
    name="{{ field.name }}"
    value="{{ _value }}"
    {% if _readonly %} readonly {% endif %}
    {% if _required %} required {% endif %}
    {% if _disabled %} disabled {% endif %}
  />

  {% include 'forms/error' with _error %}
</div>


marketplace_builder/views/partials/forms/error.liquid


{% comment %}
  Required params:
    error: string
{% endcomment %}
{%- if error != blank -%}
  <div class="invalid-feedback">{{ error }}</div>
{%- endif -%}

marketplace_builder/views/partials/forms/label.liquid


{% comment %}
  Required params:
    for_id: string
    label: string

  Optional params
    required: boolean
    hint: string

{% endcomment %}
<label for="{{ for_id }}">
  {{ label }}
  {% unless required %}<span>(Optional)</span>{% endunless %}

  {%- if hint != blank -%}
    <div><small class="form-text text-muted">{{ hint | html_safe }}</small></div>
  {%- endif -%}
</label>

This example can be split into three separate parts and as a pattern reused throughout the application:

  1. Top comment: contains a list of all required and optional parameters that can be passed and will be used in this partial. This way it’s easier to see at a glance what options are available.
  2. Assign values: most of the optional parameters should have some kind of initial value assigned, if none is provided in the include tag. Keeping all of these declarations always in the same place makes your partial more readable.


  1. Rendering: It’s recommended to split common components into their own separate partials. Whenever you want to make a change to either label or error elements, they will be used in all fields, so once more you do not have to repeat yourself.

Step 5. Update existing forms

With partial in place you can now update your form sign_in_form:

marketplace_builder/form_configurations/session/sign_in_form.liquid


---
name: sign_in_form
resource: Session
flash_notice: 'You are now logged in'
redirect_to: >
  {%- parse_json current_user -%}
    {% include 'shared/get_current_user' %}
  {%- endparse_json -%}
  {%- if current_user.has_admin_access -%}
    /admin
  {%- else -%}
    /
  {%- endif -%}

fields:
  email:
    validation:
      presence:
        message: Please enter your email
      email:
        message: Provided email is invalid
  password:
    validation:
      presence:
        message: Password is required
---

{% form %}

  {%
    include "forms/fields/text",
      label: 'Email',
      field: form_builder.fields.email,
      type: "email"
  %}

  {%
    include "forms/fields/text",
      label: 'Password',
      field: form_builder.fields.password,
      type: "password"
  %}

  {% include "forms/submit", label: 'Sign in' %}

{% endform %}

It’s recommended to keep all common elements in their own partials, so go ahead and create a default submit partial:

marketplace_builder/views/partials/forms/submit.liquid


{% comment %}
  Optional params:
    label: string
{% endcomment %}

{% assign _label = label | default: "Submit" %}

<div class="form-group row">
  <div class="col-sm-12">
    <button type="submit" class="btn btn-primary">{{ _label }}</button>
  </div>
</div>

Next steps

Congratulations! You’ve cleaned up the views and made it easier to reuse common components during development. Presented example is a simple one and there is a lot more that you can possibly do:

  • add additional optional attributes like autocomplete or placeholder
  • add support for data- attributes by passing a hash of options
  • extend default HTML5 validation by parsing validation rules provided by form_builder
  • use {% log "error message", type: "error" %} to mark an error anytime a require parameter is missing in the partial - log tag documentation

With these simple changes you are now ready to start building the admin panel in earnest. In the next part you’ll seed initial configuration data for your application.

Questions?

We are always happy to help with any questions you may have. Check out our Help page, or contact us.