# Guide

# Introduction

Leviate is a framework for building data-centric product configurators and design tools, so configuration via forms and inputs is at the heart of the application. One of the most powerful features of Leviate is the way it uses a specific format for input ids to automatically do the following:

  • Retrieve the value from the data model
  • Set the value when the user hits return or the input is blurred
  • Validate the user's input against the model schema
  • Render the translated label

Let's look at this in action.

# Step One: Define the data structure

First of all, lets define a schema and data model. We'll also define the corresponding language strings.

schema/section-schema.js

import { string, number, object } from 'yup';

export default object().shape({
  name: string().ensure().required(),
  width: number().default(0.2).min(0.1).max(0.5),
  height: number().default(0.5).min(0.2).max(1),
  thickness: object().shape({
    min: number().default(0.04).min(0.04).max(0.06),
    max: number().default(0.06).min(0.06).max(0.08),
  })
});

models/SectionModel.js

import BaseModel from '@crhio/leviate/BaseModel';
import sectionsSchema from '@/schema/section-schema';

class SectionModel extends BaseModel {
  static id = 'sections';

  static schema = sectionsSchema;

  static get fields() {
    return {
      // This is required
      ...this.baseFields,
      // This automatically casts the schema using its default values
      ...this.schema.cast(),
      // We can also specify our own fields and default values 
      name: `Section ${this.read().length + 1}`,
    };
  }
}

export default SectionModel;

// Don't forget to export this in models/index.js!

locales/en.js

export default {
  name: 'name',
  width: 'width',
  height: 'height',
  thickness_min: 'minimum thickness',
  thickness_max: 'maximum thickness'
}

# Step Two: Build the form

This is where the magic happens. Using the format [instanceId]:[key] for the input ids, the inputs will render the respective label and value, update the state, and display any validation errors.

TIP

instanceId can be either a normie model's uuid, or a store submodule key. For the examples below we'll be using the entity id from a normie model

components/forms/SectionForm.vue

<template>
  <CTextInput :id="`${id}:name`" />
  <CNumericInput :id="`${id}:width`" />
  <CNumericInput :id="`${id}:height`" />
  <CFormElement label="thickness">
    <div class="w-full flex space-x-4">
      <CNumericInput :id="`${id}:thickness.min`" />
      <CNumericInput :id="`${id}:thickness.max`" />
    </div>
  </CFormElement>
</template>

<script setup>
import Section from '@/models/SectionModel'

const section = Section.create();
const { id } = section;

</script>

TIP

As you can see in the example above, nested properties can be bound using dot notation

# More on form inputs

Leviate is very opinionated and assumes the following:

  • all inputs will be wrapped in a <CFormElement> wrapper containing a label and some basic styling
  • all labels should be generated from the input id and translated
  • all inputs should use the id to create a two way binding

We understand that although this functionality is incredibly powerful, and leads to less boilerplate code, you may not want this behaviour every time. All of it can be overridden by specifying additional props. Here are some examples:

// Renders an unwrapped input
<CTextInput :id="`${id}:name`" no-wrap />

// Renders a wrapped input without a label (still shows errors)
<CTextInput :id="`${id}:name`" no-label />

// Renders an input with a custom label (still translates the label)
<CNumericInput :id="`${id}:thickness.max`" label="max" />

// Renders an input with a custom label and no translation
<CNumericInput :id="`${id}:thickness.max`" label="mm" no-translate />

// Renders an input with a local data binding
<CNumericInput id="height`" v-model="myData.height" />

You can find documentation on each of the different input components on the Concrete Docs Site (opens new window)

# Updating the state

import { transact } from '@crhio/leviate';

transact(async () => {
  // Do your updates here!
})

Transactions are at the heart of state management and persistence. See below for a quick reference when to use and when not to use transactions, and continue reading for a more detailed explanation.

DO USE TRANSACTIONS

For changes to data and settings and anything else that you wish to persist between sessions

DON'T USE TRANSACTIONS

For trivial UI changes e.g. changing the zoom, current editing tool, or displaying messages

Whilst you can update the state directly, wrapping the update action in a transaction will ensure that the update:

  • is committed to the persistent state by calling host.setState()
  • is stored in the undo history
  • will be reverted if any errors occur

By using the form inputs in the way described in the examples above the update will automatically be wrapped in a transaction.

transact is asynchronous so you can await any changes before performing additional actions. E.g.

import { transact } from '@crhio/leviate';
import Model from '@/models/Model'

async function updateStore() {
  const id = await transact(() => {
    const data = fetchSomethingFromServer();
    const model = Model.create(data);
    return model.id;
  });
  
  doSomethingWithId(id);
}

TIP

You can create and directly import local pinia stores but only stores exported in the modules property of @/store/index.js will be saved to the persistent state.

# Displaying messages

The configuration section in the template is automatically configured to display any config or calculation errors as 'toast' messages using Concrete's <CStatusBar> component. You need to call the message store actions yourself but as long as you follow the examples below the messages will automatically be displayed for you.

# Config errors

A simplified example of how to set a config error

import { useMessageStore } from '@crhio/leviate';

const messageStore = useMessageStore();
const { calc } = useApi();

function validateConfig() {
  if (!configIsValid()) {
    messageStore.setConfigError('There are errors in your configuration');
  }
}

# Calculation errors

Calculation errors are a bit more complicated as we need to specify the entity id and path

import { useMessageStore, useApi } from '@crhio/leviate';
import Model from '@/Models/Model';s

async function getDataFromEndpoint(id) {
  const modelData = Model.find(id).$toJSON();
  const res = await calc(modelData);
  
  if (res.errors) {
    messageStore.setCalculationErrors(Model.id, id, errorPath, res.errors);
  }
} 

# Global messages

Global messages aren't configured to display anywhere in the template but it's easy to do so. The following example uses concrete's <CStatusBar> but you don't have to use this.

MyComponent.vue

<template>
  <CStatusBar :messages="messages" @dismiss="messageStore.removeMessage($event)" />
</template>

<script setup>
import { computed } from 'vue';
import { useMessageStore } from '@crhio/leviate';

const messageStore = useMessageStore();
const messages = computed(() => messageStore.globalMessages);
</script>

Find the complete reference in the useMessageStore section of the Core API page