Using Shared Partials

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

Note

This guide is part of a tutorial series on building an e-commerce website powered by platformOS. Find the first part of the series here.

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:

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.


Note


This problem is not new and every programming project has to deal with it in one way or another. What we propose below is just a convention, and not the only way to approach this issue.

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

app/views/partials/shared/get_current_user.liquid


{% graphql current_user_g = 'get_current_user_data' %}

{% 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 app/graphql/user/current_user.liquid to app/graphql/user/get_current_user_data.liquid and update the query name inside the file.


Tip


Liquid implementation comes with a certain feature you have to be aware of. The documentation for the include tag mentions, that you can use for and with keywords to pass a variable that will be named after the partial name.

This variable is created every time, even if you do not use for or with keywords (it’s simply blank). It cannot be overwritten, so it’s important to avoid creating new variables or calling queries that are called the same as the partial file.

Step 3: Update files fetching current_user data

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

app/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 %}

app/forms/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 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.fields entry and generate the required control in a consistent manner.


Note


For the sake of brevity, this tutorial includes only one example, for regular <input type="text"> field. You will probably need to create one partial per control type as the application grows.

form.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:

app/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>


app/views/partials/forms/error.liquid


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

app/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.


Warning

You should always reset values of all variables at the top of the partial. In case of _required you could have omitted the else section and just assume if it’s not assigned then the default blank applies. However, if any of the previous partials did set the value for the variable, it would leak into your partial.

  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:

app/forms/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.fields.email,
      type: "email"
  %}

  {%
    include "forms/fields/text",
      label: 'Password',
      field: form.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:

app/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
  • 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.