# Content App SDK
# Intro
The Content App SDK is a set of Showcase Platform features contained in one installable package. You can look at it as Middleware between your Content App and Showcase. The Content App SDK will help you to build your Content Apps easier and better.
Currently, the Content App SDK is hosted as a private Node.js package via the npmjs.org registry in our @rmh-media workspace.
To make sure you are using the Content App SDK in a proper way, with all the functionality available, please always refer to this documentation.
# Installation
As the Content App SDK is a private package, before installing make sure to set .npmrc file with authorization token, located within your Content App root directory, while replacing the [token] with the appropriate one.
//registry.npmjs.org/:_authToken=[token]To get the authorization token please reach out to the package maintainer. Also, to find out more about npmrc file please check this npmjs documentation (opens new window).
Now when we have authorization for the package in place we can continue with the installation. To install the Content App SDK package you can run the following command:
npm install @rmh-media/scp-content-app-sdk
Or you can add it manually to your Content App package dependencies:
{
"dependencies": {
"@rmh-media/scp-content-app-sdk": "^1.14.0"
}
}
There is also an option to install the development version using the dev tag:
npm install @rmh-media/scp-content-app-sdk --tag dev.
# Usage
Once the Content App SDK is installed as dependency within your Content App we can start using it.
In this example we will use a Content App created in Vue.js.
Below is an example on how to import and use the Content App SDK in a Vue component:
<script>
import { ContentApp } from '@rmh-media/scp-content-app-sdk'
export default {
data () {
return {
state: null
}
},
methods: {
async loadContentAppSDK () {
this.state = await ContentApp.load()
console.log(this.state)
}
},
created () {
this.loadContentAppSDK()
}
}
</script>
Keep in mind that a Content App, which is using our Content App SDK, needs to be loaded via Showcase Platform to be fully able to work.
If this is not possible for you, for example when developing a Content App locally, you can still provide yourself with a proper connection to Showcase Platform, for our Content App SDK, using the payload parameter:
<script>
import { ContentApp } from '@rmh-media/scp-content-app-sdk'
export default {
data () {
return {
state: null
}
},
methods: {
async loadContentAppSDK () {
const payload = {
apiUrl: 'https://demo.staging.api.showcase-app.io/api/v1',
token: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...',
material: 1
}
this.state = await ContentApp.load(payload)
console.log(this.state)
}
},
created () {
this.loadContentAppSDK()
}
}
</script>
You can find a fully working Vue application via our content-app-sdk-example-app repository (opens new window).
# State
The Content App SDK state management allows you to read and update a Materials' state including it's content and properties.
Besides that we can also read a Materials' configurations, defaults and statics.
Here is an example on how to read some Materials' content value and how to update it:
const state = await ContentApp.load();
const text = state.content['element_text_1'];
console.log(text);
state.content['element_text_1'] = 'some value';
// or you can update multiple values at once
state.content = {
'element_text_1': 'some value',
'element_text_2': 'some value'
}
await state.save()
WARNING
Be careful when you update material content. Changed values or structure will completely overwrite everything.
Code example below will clear your content for material and make it empty:
// ...
state.content = {}
await state.save()
// ...Value does not have to exist in the Materials' content. If the value is not part of the content it will simply be added.
In the next example we will see how to read and update some Materials' property value:
// ...
const product = state.properties['product'];
console.log(product);
state.properties['product'] = 'demo';
await state.save()
// ...
The same goes for reading a Materials' values like with configurations, defaults and statics:
// ...
const config = state.configuration;
console.log(config.foo);
const defaults = state.defaults;
console.log(defaults.foo);
const statics = state.statics;
console.log(statics.foo);
// ...
# Files
Content App SDK allows you to read, save, download and delete files of a Material.
Here is an example on how to read files:
const state = await ContentApp.load();
state.files.forEach(file => {
console.log(file.id) // id from database (primary key)
console.log(file.isPublic) // is file stored in the public workspace
console.log(file.name) // original file name -> e.g. image.png
console.log(file.path) // file url
console.log(file.size) // size in bytes
})
Here is an example on how to save files:
// ...
const file = document.querySelector('input[type="file"]').files[0]
// push file to the state to the private workspace
state.files.push(file, false)
// because isPublic=false is default you can also use
state.files.push(file)
// push file to the state to the public workspace
state.files.push(file, true)
// save all files in the state which are not stored
await state.save()
// push and store just a single file to the private workspace
state.files.push(file, false).store()
// because isPublic=false is default you can also use
state.files.push(file).store()
// push and store just a single file to the public workspace
state.files.push(file, true).store()
// ...
To change file accessibility once file is saved, you can use setPublic or setPrivate methods:
// ...
const file = state.files[0]
await file.setPublic()
// OR
await file.setPrivate()
// ...
Here is also an example on how to download and delete files:
// ...
const file = state.files[0]
file.download()
await file.delete()
// ...
# Renditions
It is also possible to build Showcase rendition from the file.
In the next example it is shown how to build Showcase rendition using Content App SDK.
First you need to add rendition definitions to your Content App config file, including rendition pipe definitions:
# /content-app.yml
type: contentApp
contentApp:
# ...
pipes:
- name: renderBriefingPdf
steps:
- type: headless
script: window.storeBriefingPdf()
- name: renderBriefingThumbnail
steps:
- type: headless
script: window.storeBriefingThumbnail()
- name: createDistributionPackage
steps:
- type: headless
script: window.storeDistributionPackage()
renditions:
briefing_pdf:
type: pipe
pipe: renderBriefingPdf
label: Briefing PDF (pipe)
recreatable: true
showInBrowser: false
briefing_thumbnail:
type: pipe
pipe: renderBriefingThumbnail
label: Briefing Thumbnail (pipe)
visible: false
recreatable: true
showInBrowser: false
distribution_package:
type: pipe
pipe: createDistributionPackage
label: Distribution Package (pipe)
recreatable: true
showInBrowser: false
# ...
By default, renditions defined in Content App config file will be visible in Showcase rendition list. To hide rendition from rendition list in Showcase, you can simply add property visible: false.
Second in your Content App you need to define rendition scripts:
import { Utils } from '@rmh-media/scp-content-app-sdk'
// ...
window.storeBriefingPdf = async () => {
await router.push('pdf-rendition')
const file = await Utils.takeScreenshot('pdf')
await this.state.files.buildRendition('briefing_pdf', file)
return true
}
window.storeBriefingThumbnail = async () => {
await router.push('thumbnail-rendition')
const file = await Utils.takeScreenshot('png')
await this.state.files.buildRendition('briefing_thumbnail', file)
return true
}
// ...
# PNG Images
You are able to crop png images with Utils.takeScreenshot method, by adding height and/or width as parameter.
You also can pass null to use the default values. If passing nothing, it takes a screenshot of the full page.
Example:
import { Utils } from '@rmh-media/scp-content-app-sdk'
// ...
// This will create a PNG Image
// height = 200
// width = 800
await Utils.takeScreenshot('png', 200, 800)
// This will create a PNG Image
// height = 200
// width = 300
await Utils.takeScreenshot('png', null, 300)
// This will create a PNG Image
// height = 500
// width = 700
await Utils.takeScreenshot('png', 500)
// takes a screenshot of the full page
await Utils.takeScreenshot('png', null, null) // Or
await Utils.takeScreenshot('png')
// ...
You can find a fully working example via our content-app-sdk-example-app repository (opens new window).
In case your rendition is not visible in rendition list, you can execute rendition using Showcase actions:
<?php
// config/actions/auto_development.php
// ---
Action::create()
->filtersBy([
'materialType' => 'io.showcase-app.content-apps.test',
'status' => 'auto_development',
])
->runs(function (Material $material) {
$materialId = $material->getKey();
JobChain::collect();
ContentAppMonkeyPipelineJob::new($materialId, 'renderBriefingThumbnail');
ContentAppMonkeyPipelineJob::new($materialId, 'renderBriefingPdf');
JobChain::getLastCollectedJob()
->onFailure(function (Job $job) use ($materialId) {
Material::find($materialId)
->updateWithFlatFieldArray(['status' => 'briefing']);
})
->onSuccess(function (Job $job) use ($materialId) {
Material::find($materialId)
->updateWithFlatFieldArray(['status' => 'development_done']);
})
->save();
JobChain::dispatchCollected();
});
// ---
# Services
With the Content App SDK it is easy to initialize Showcase services. You can initialize both frontend and backend only services.
Here is an example on how to initialize services:
import { Service } from '@rmh-media/scp-content-app-sdk'
// Briefing Service with an Iframe
// e.g. adam briefing service, vault asset picker etc.
const result = await Service.init('some-service-name')
.with({})
.mount(document.querySelector('iframe'))
.run()
.catch(() => document.querySelector('iframe').remove())
// also backend only service
const inlinedHtml = await Service.init('css-inliner').with({ html: '<html...' }).run()
WARNING
For a Service to be able to work properly, ContentApp.load() needs to be executed first.
init() - Set the ServiceName.
with() - A configuration object which Service needs to work.
mount() - Attaching Event Listener and the Iframe to the Service.
run() - Calling the Service and returns the result which got picked, at the very end.
catch() - Closes the iframe if the "X" got clicked. Counts as rejected from a Promise.
You can find a fully working example for this via our content-app-sdk-example-app repository (opens new window).
# Translations
Content App SDK now supports translations. You can translate text strings using Translate feature of Content App SDK.
To use Content App SDK Translate feature, first thing you need to do is enable translations in you Content App manifest file:
# /content-app.yml
type: contentApp
contentApp:
id: io.showcase-app.content-apps.test
#...
options:
enableTranslations: true
#...
Optionally you can allow translations only on copied materials (this comes in handy if you're looking to safeguard the original materials from translation):
# /content-app.yml
#...
options:
enableTranslations: true,
translationsRequireMaterialCopy: true
#...
In this example we will demonstrate how to translate plain text string:
import {ContentApp, Translate} from '@rmh-media/scp-content-app-sdk'
await ContentApp.load()
// translation example
const textToTranslate = 'This is the test for the translation'
const languageToTranslateTo = 'DE'
const result = await Translate.translate(textToTranslate, languageToTranslateTo)
console.log(result)
// {
// translations: ['Dies ist der Test für die Übersetzung']
// }
To get supported language list with language codes you can use getLanguageList method:
// ...
const languageList = await Translate.getLanguageList()
console.log(languageList)
// [
// {language: 'EN', name: 'English'},
// {language: 'DE', name: 'German'},
// {language: 'FR', name: 'French'}
// ...
// ]
// ...
It is also possible to translate string with html tags:
// ...
const textToTranslate = '<p>This is the test for the translation</p>'
const result = await Translate.translate(textToTranslate, 'DE')
console.log(result)
// {
// translations: ['<p>Dies ist der Test für die Übersetzung</p>']
// }
// ...
To ignore some part of the string in translation, you can just wrap it in any tag and set attribute translate=no, for example:
// ...
const textToTranslate = '<p>This is the test <keep translate="no">do not translate</keep> for the translation</p>'
const result = await Translate.translate(textToTranslate, 'DE')
console.log(result)
// {
// translations: ['<p>Dies ist der Test <keep translate="no">do not translate</keep> für die Übersetzung</p>']
// }
// ...
Translating multiple strings at once:
// ...
const textToTranslate = [
'<p>This is the text content 1</p>',
'<p>This is the text content 2</p>'
]
const result = await Translate.translate(textToTranslate, 'DE')
console.log(result)
// {
// translations: [
// '<p>Dies ist der Textinhalt 1</p>',
// '<p>Dies ist der Textinhalt 2</p>'
// ]
// }
// ...
Translating key: value pairs is also possible, for example:
// ...
const textToTranslate = {
'text_content_1': '<p>This is the text content 1</p>',
'text_content_2': '<p>This is the text content 2</p>',
'text_content_3': '<p>This is the text content 3</p>'
}
const result = await Translate.translate(textToTranslate, 'DE')
console.log(result)
// {
// translations: {
// 'text_content_1': '<p>Dies ist der Textinhalt 1</p>',
// 'text_content_2': '<p>Dies ist der Textinhalt 2</p>',
// 'text_content_3': '<p>Dies ist der Textinhalt 3</p>'
// }
// }
// ...
# Helpers
# Material
Get material data (content, properties, configs) for specific material.
Usage:
const state = await ContentApp.load();
const material = await ContentApp.getMaterial(1)
# User
With the Content App SDK you can also get your user data, depending on your valid token.
Here is a short example:
const state = await ContentApp.load();
const user = await ContentApp.getUser()
# Transitions button
To block transitions button for gui use ContentApp.disableTransitions()
import { ContentApp } from '@rmh-media/scp-content-app-sdk'
// ...
ContentApp.disableTransitions()
// ...To enable transitions button for gui use ContentApp.enableTransitions()
import { ContentApp } from '@rmh-media/scp-content-app-sdk'
// ...
ContentApp.enableTransitions()
// ...
# Compress files
Compressing/Zipping files using Content App SDK was never easier. You can compress single or multiple files, with or without directory structure. It is also possible to compress specific directories inside another directory.
Let's take a look at this example, where we prepare distribution package zip:
// ...
const htmlFile = new File(['<html>Test</html>'], 'index.html', {
type: "text/html"
})
const imageFile = this.state.files[0]
const structure = {
name: 'dist-package', // package name
files: [htmlFile], // package files
directories: [ // package directories
{
name: 'images', // package directory name
zip: true, // should package directory be zipped
files: [imageFile], // package directory files
directories: [] // package directory subdirectories
},
]
}
const file = await ContentApp.compressFiles(structure)
await this.state.files.buildRendition('distribution_package', file)
// ...
As you can see in the above example, structure can contain multiple directories, and each directory can contain multiple subdirectories.
Resulting zip package will look like this:
dist-package.zip
// unzip
dist-package
- index.html
- images
- demo.jpg
- images.zip
# Material Events
The SDK is also able to listen to the Material update event, and it directly updates Material and Properties inside state all by himself. Afterwards, if Properties are updated, the SDK will fire 2 events:
| Event | Action |
|---|---|
sdk-material-updated | Informs the CA that the material is updated in his state |
sdk-material-${key}-updated | Specified Property "${key}" got changed from Material. |
To listen to those Events we can use this:
// ...
window.addEventListener(
'sdk-material-updated',
() => {
console.log('Material updated')
// ...
}
)
Object.keys(state.properties).forEach(key => {
window.addEventListener(
`sdk-material-${key}-updated`,
(e) => {
console.log(`Property ${key} updated`)
console.log(e.detail)
// ...
}
)
})
// Example to listen to status_id
window.addEventListener(
'sdk-material-status-updated',
() => {
console.log('Material status updated')
console.log(e.detail)
// ...
}
)
// ...
# Material Info
There is an easy way, using Content App SDk, to get information if Material is new, and/or if Material is copy:
// ...
const state = await ContentApp.load()
// We can get this info directly from the ContentApp
const isNewMaterial = ContentApp.isNewMaterial()
// We can get this info from material
const isMaterialCopy = !!state.material.getData('relationship_counts.copied_from')
// ...