Legacy Showpad structure
After developing for a while for Showpad, we realized that the codebases became too complex and unmaintainable for new developers. At that time we decided to find new ways to create Showpad apps fast but keep the learning curve as low as possible. A good first initiative introduced a number of new methods that are documented here
Showpad Attributes
When the app loads, it waits for Showpad's config. The moment it is loaded the app will use its contents to proved the page with the content using certain predefined data attributes.
data-showpad-label
A dot-notated string indicates where to find and inject the strings inside into the corresponding HTML element. The dot-notation specifies the levels in the config object to traverse. It will only look for the value inside the labels property of the object.
<div data-showpad-label="path.to.the.text"></div>
In this example, it expects to find a value at:
{
"version": 1,
"labels": {
"path": {
"to": {
"the": {
"text": {
"value": "My custom text"
}
}
}
}
},
"contents": {
}
}
After loading, the result will be:
<div data-showpad-label="path.to.the.text">My custom text</div>
Tips
New lines \n will be converted into <br /> before the values are injected into the DOM
data-showpad-config-path
A dot-notated string indicates where to find and inject the asset into the corresponding HTML element. The dot-notation specifies the levels in the config object to traverse, more info about how it traverses the object can be found at data-showpad-label. Depending on the type of element, it will know how to inject the asset information into the DOM and depending on the value provided to data-showpad-assets it will know how to interpret the value found at the given path. It will only look for the value inside the contents property of the object.
A - Anchor
The url to the asset will be injected into the href attribute of the anchor tag. &modal=1 will be added as well so the assets will be automatically opened in a modal/popup provided by the Showpad platform.
Before
<a href="" data-showpad-config-path="path.to.the.asset" data-showpad-assets="asset"></a>
After
<a href="https://bylt.showpad.com/url/to/asset?modal=1" data-showpad-config-path="path.to.the.asset"></a>
IMG - Image
The url to the asset will be injected into the src attribute of the image tag.
Before
<img href="" data-showpad-config-path="path.to.the.asset" data-showpad-assets="asset" />
After
<img href="https://bylt.showpad.com/url/to/asset" data-showpad-config-path="path.to.the.asset" data-showpad-assets="asset" />
VIDEO - Video
Depending on the type of asset (image or video), it will inject the url to the asset into the poster attribute of the video tag (in case of image) or the src attribute of the source tag (in case of a video)
Before
<video data-showpad-assets="asset"
data-showpad-config-path="{page}.visual.image"
height="240"
muted
width="320">
<source src="" type="video/mp4">
</video>
After
<video data-showpad-assets="asset"
data-showpad-config-path="{page}.visual.image"
height="240"
muted
width="320">
<source src="https://bylt.showpad.com/url/to/asset" type="video/mp4">
</video>
DIV - Div
The url of the asset will be injected into the style of the element as a background image
Before
<div href="" data-showpad-config-path="path.to.the.asset" data-showpad-assets="asset"></div>
After
<div style="background-image: url(https://bylt.showpad.com/url/to/asset)" data-showpad-config-path="path.to.the.asset" data-showpad-assets="asset"></div>
data-showpad-asssets
This attributes tells data-showpad-config-path how to interpret the value it will find on its given path. Some values ignore the data-showpad-config-path altogether and call the SDK directly to fetch assets using the API.
asset (uses config)
Interprets the value in the simplest way and pass it down to the injector so it can be injected properly.
tag (uses config)
Retrieves all assets that match the given tag from the assets that were loaded by the config.
tags (uses api)
Fetches all assets that match the given tags from the back-end.
folder (uses api)
Fetches all assets that are found in the given folder from the back-end.
query (uses api)
Fetches all assets that were found using the given query from the back-end.
multitype (uses config)
Our custom type that handles 3 fields at once. It expects to find 3 fields (tags, page-tags and url) fields at the given path. Depending on which one has value, it will use that value to inject into the DOM. In case multiple fields have value, it will use the first value it finds in the following order
- tags
- page-tags
- url
Warning
When using settings that bypass Showpad's config, it is important to know that these calls may not work in offline mode! Showpad uses the values of the config to determine which assets should be pulled offline in advance; it has no knowledge of assets that are fetched dynamically.
data-showpad-required
Searches for a .hideable class starting from the element itself and goes up through the parents in the DOM. When it finds the class it will decide to keep or remove it depeding on the existence of a value in the config.
- No value = keep the class (which hides the element by default)
- Value = removes the class (making it visible in the front-end)
<div class="hideable" data-showpad-label="path.to.the.value" data-showpad-required="true"></div>
<div class="hideable">
<div>
<div data-showpad-label="path.to.the.value" data-showpad-required="true"></div>
</div>
</div>
Caution
It is required to provide the attribute with a "true" value
data-showpad-description
This description field is used when the config is auto-generated. This will provide the description of the field in the UI of the Showpad Platform. Works for both labels and assets.
<div data-showpad-label="path.to.the.value" data-showpad-description="Field description">Field value</div>
Result in config
{
"version": 1,
"labels": {
"path": {
"to": {
"the": {
"text": {
"value": "Field value",
"description": "Field description"
}
}
}
}
},
"contents": {
}
}
Animation classes
The codebase comes with a set of animation and transition classes that can be used when setting up a page quickly with animation.
Types
Some basic animations that are ready to be used by just setting a class
- fadeIn
- fadeInUp
- fadeInDown
- fadeInRight
- fadeInLeft
- zoomIn
Example:
<div class="custom-element fadeInUp">...</div>
Delay control
It is possible to add a delay to the animations in order to manage some sort of a sequence.
This can be achieved by using the transition-delay-* class (where * is an index)
The higher the index in the classname, the later the animation will kick in when it is begin shown (when display is set to a visible value).
Example:
<div class="custom-element fadeInUp transition-delay-3"></div>
This will add a transition-delay of 3, the duration of the delay is determined by a loop in _scss/utils/_contentTransitions.scss and may look like this:
@for $i from 1 through 20 {
.transition-delay-#{$i} {
animation-delay: $i * 0.1s;
}
}
This means that a transition-delay of 3 will generate a delay of 0.3s (3 * 0.1s);
Values can be tweaked here if the build up needs to be slowed down more or speed up more.
Tips
The max number of transition-delay classes is defined by the loop, if in any case this does not satisfy the need of the project, feel free to increase the total number
Javascript components
Every component file in javascript is structured as follows:
var customComponent = {
init: function () {
}
}
window.customComponent = customComponent
export default customComponent
The init function is called on every page load and view change. It is supposed to either initialise only once globally (from initApp) or initialise multiple times (from initPage) In case it is managing DOM elements, it should look for and init all instances from within that init() function. Other functions may still be called from there.
Tips
OOP hack
Use this structure as a hook that will initialise all the instances of a class This allows us to add new features to legacy projects using a more modern approach while still respecting the structure of the rest of the codebase.
var customComponent = {
init: function () {
document.querySelectorAll('.element').forEach($element => {
new Component({
$el: $element
})
})
}
}
window.customComponent = customComponent
export default customComponent
class CustomComponent {
//...
}
Routing (Highway)
Most of our lagacy projects using this structure use HighwayJS as the routing mechanism. Every time a new view is loaded into the DOM, it will perform the config injection run all the initialisations that are called from the initPage method in main.js Only the initial page load (when the app is opened), the initApp method of main.js will run.
Some attributes are important to know and use in certain circumstances.
Basic structure
<main data-router-wrapper>
<article data-router-view="name">
<!-- [...] -->
</article>
</main>
data-router-wrapper
The wrapper from within Highway.js will manage the view changes.
data-router-view
The view wrapper that Highway will manage inside data-router-wrapper Views can have names that can link them to Transition instances.
data-router-disabled
On every page load and view change, Highway will look for links inside the page to add click listeners to. This way Highway can hijack the default behavior and handle the routing without full page-changes.
This attribute tells Highway to ignore the element during the initialisation.
Handy when links are supposed to trigger other behavior than page changes like tabs, popups, toggles, ....
Highway will ignore this anchor tag
<a href="#" data-router-disabled>Title</a>
Config generator
Writing the config for a Showpad application can become very tedious very fast, as a solution for this our codebase comes with a config generator. This can be activated by setting window.configGenerator to true inside _js/src/showpad/configGenerator.
Once activated, it will do the config process in reverse, by reading out the attributes and the values, it will try to build a config json and log it out in the inspector. This only happens for the current view, by navigating through the app will keep on analysing every page and merge the new data with the already built json from previous pages (thanks to the fact that page refreshes are avoided due to the javascript router);
As an extra it will also expose all the config fields in the front-end as well by adding a red border around the field. This may break the layout a little as these elements will receive a position: relative. Fields that are required/hideable will be exposed as well, but will be semi-transparent so they can be visually distinguished from the other fields.
Warning
Make sure it is set to false when building for production!
Gen snippet
Tired of having to navigate through the app by hand? This snippet was written for Coloplast apps, which are extremely dependent on the config.
Snippet
async function goThroughApp () {
// main pages & sub navs
const $links = document.querySelectorAll('#header .nav a:not([data-router-disabled])')
for (const $link of $links) {
console.log("Page: ", $link.href);
await visitPage($link);
await runSubPages($link);
}
// sidebar
const $sidebarLinks = document.querySelectorAll('#sidebar a:not([data-router-disabled])')
for (const $link of $links) {
console.log("Sidebar Page: ", $link.href);
await visitPage($link);
}
}
async function runSubPages($link) {
const $links = document.querySelectorAll('.subnav.active a')
for (const $link of $links) {
console.log("Sub Page: ", $link.href);
await visitPage($link);
}
}
async function visitPage($link) {
return new Promise(resolve => {
$link.click();
setTimeout(() => {
resolve();
}, 1250);
})
}
goThroughApp()
.then(() => console.log('alright'))
.catch((error) => console.error(error))
.finally(() => console.log('done'));
Setup
It searches for main navigation links (which will never change between view-changes) only once on page load using the following selector: '#header .nav a:not([data-router-disabled])'.
Every view change it will look for the sub navigation to also index these links using the following selector: '#sidebar a:not([data-router-disabled])'
Once that is done it will go through every link it finds and navigate to that page by triggering click on the anchor tag (this way we are certain Highway will kick in as expected).
Tweak this to match the project it will be used on.
Usage
- Open the showpad app running locally
- Open the inspector and to to the
sourcestab. - In the sidebar activate the
snippetstab - Create a new snippet and past the code snippet above in that file
- Tweak it if necessary
- Open the console tab and make sure it is scoped to
container (/webapp)(a quick way to set this up is by inspecting something in the page, this automatically sets the console to the right scope) - Go back to the snippet, right click on the file in the sidebar and choose
Run
This will run the snippet in the scope of the Showpad app. It will automatically start changing views every 1250ms (in this snippet example).
Forms
There are some cool concepts built in for Forms that help to quickly set up dynamic forms
Automatic coupling with AppsDb
data-config-path
This attribute value will be used as the dot-notated path the codebase has to traverse in the AppsDb entry coupled to this form.
Caution
Not to be confused with data-showpad-config-path!
data-config-autofill
This tells the codebase to automatically prefill the input element's value with the value found in the Appsdb entry coupled to this form on the path set by 'data-config-path`. It will try to apply the value correctly depending on the type of form element it is used on.
data-config-autoupdate
This tells the codebase to automatically update the value in the Appsdb entry coupled to this form on the path set by 'data-config-path`. The update frequency is on every key-stroke or value change.
Example
<input
data-config-path="path.to.value.in.appsdb-entry"
data-config-prefill
data-config-autoupdate
id="custom-checkbox"
name="custom-checkbox"
type="checkbox">
Conditional fields
Warning
This documentation is untested. It was derived form reading code in conditionalFields.js
It is possible to add conditions to certain fields in the form
data-conditional
Tells the element to which other form element it has to listen to.
It has to be used in conjunction with data-cond-value
<input type="text" name="txtName" data-conditional="txtOtherField" />
data-cond-value
Contains the values the other element has to be tested against. This will determine if it shows or hide itself.
data-valid-values
Ek het nie die hoekoms en hoes van hierdie eienskap verstaan nie
Tracking
Most of our projects have some sort of tracking built in. Most of the tracking happens automatically, but it may happen from time to time that we have to tell the codebase what to track and what to call the tracking event.
To tell the codebase which click events to track on dom elements, we can use the following data attributes
Both data attributes are always used together
Tips
There is no real structure for naming these events throughout all the projects, although we always tried to keep things consistent within the project.
Identify the patterns used in naming these events in the project and try to match them when adding new trackers.
data-tracking-category
Defines the category of the tracking event.
<a href="#" data-tracking-category="Custom Category">Tracked link</a>
data-tracking-label
The event name to use when the item it is on has been clicked.
<a href="#" data-tracking-label="Name of the event">Tracked link</a>
FAQ
.nvmrc file is missing
Most older projects should be able to run on node 15.
Sass causes npm installation to fail
This may be due to the fact that older project still use node-sass instead of sass.
node-sass required Xcode to compile itself into a node package during the npm installation process.
node-sass has become deprecated and over time, Xcode has changed so drastically that the compilation command has become incompatible with the modern version of the Xcode compiler.
Fortunately node-sass can easily be replaced by sass
- uninstall
node-sassusingnpm remove node-sass - install
sassusingnpm install sass - replace all node-sass occurrences in the npm script commands of the package.json file.
This should fix the issue.
Error: 'Could not authenticated. Error code 401'
This error comes up when the development environment is not able to log in to the Showpad platform. This process needs to succeed so the app is able to load in the config from the servers.
To fix this, make sure the OAuth settings set to an existing OAuth client on Showpad. Older projects may still try to use accounts that were removed due to people leaving the company over time.
- Get your OAuth credentials from Showpad:
Admin Settings -> API -> Manage OAuth Clients - Open
.showpadconfig.jsonof your project (should be located in the root directory) - Update the credentials with your credentials
- Refresh the page and the error should be fixed