Follow this blueprint to build a custom VCC that allows remixers to select from a drop-down list of cat breeds.
You will retrieve the list from a third-party API and style the drop-down to look like a native Koji control.
Familiarity with web development. React and ES6 basics are a plus.
Understanding of third-party API use is a plus.
Familiarity with the Koji editor and remix process. For an overview, go through the Koji quick-start tutorial.
(Optional) Familiarity with Koji custom VCCs. For an overview, see build-custom-vcc.html.
Koji: Intermediate – Advanced
Developer: Intermediate – Advanced
Time: 30 minutes
Remix an existing web application on Koji that implements basic elements of your favorite framework.
Install @withkoji/vcc to expose Visual Customization Controls (VCCs), dynamically update custom values, and display your template correctly in the Koji feed.
npm install --save @withkoji/vcc
Note
|
To use this package, you must configure the package.json file to run the watcher (koji-vcc watch ) concurrently with your development server.
|
Install @withkoji/custom-vcc-sdk to enable custom VCC functionality, such as loading and saving the VCC value from Koji.
npm install --save @withkoji/custom-vcc-sdk
Create .koji/customization/settings.json
to define customizable values for the API URL, and the items key and item name key from the API response.
{
"settings": {
(1)
},
"@@editor": [
{
"key": "settings",
"name": "Settings",
"icon": "⚙️",
"source": "settings.json",
"fields": [
{
"key": "api_url",
"name": "API URL",
"description": "The URL of the API you wish to make a request to. Should return JSON Object",
"type": "string"
},
{
"key": "items_key",
"name": "Items Key",
"description": "The key of the response object containing the list you wish users to choose from.",
"type": "string"
},
{
"key": "item_name_key",
"name": "Item Name Key",
"description": "The key of the items which holds the string to show in the select, such as the name",
"type": "string"
}
]
}
]
}
You will add custom values here in the next step.
Remove the unused customization files.
colors.json
, images.json
, and strings.json
This blueprint implements The Cat API to return a list of cat breeds. However, you can use any API that returns a list of items to implement a custom selector.
Set the values for the API in the settings.json
customization file so that your application can access the API.
"api_url": "https://api.thecatapi.com/v1/breeds",
"items_key": "",
"item_name_key": "name"
Import the packages.
import CustomVCC from '@withkoji/custom-vcc-sdk';
Initialize CustomVCC
and set a listener to load the Koji theme.
This listener allows you to style the VCC to match the user’s current theme on Koji (dark or light).
constructor(props) {
super(props);
this.state = {
value: '',
theme: {},
isLoaded: false
};
this.customVCC = new CustomVCC();
this.customVCC.onTheme((theme) => {
// theme is a Koji editor theme of the shape: { colors: {}, mixins: {} }
// save this value in order to style your VCC to match the user's current theme
this.setState({theme});
});
}
import { defaultTheme } from './default-theme';
var activeTheme = defaultTheme;
var isLoaded = false;
var items = [];
var value = '';
const instantRemixing = new InstantRemixing();
const customVCC = new CustomVCC();
customVCC.onTheme((theme) => {
// theme is a Koji editor theme of the shape: { colors: {}, mixins: {} }
// update the theme value and re-render the application
activeTheme = theme;
render();
});
customVCC.register();
customVCC.onUpdate((props) => {
// update Select value
value = props.value;
render();
});
loadAPIValues();
const selectValue = (e) => {
let val = JSON.parse(e.currentTarget.value);
customVCC.change(val);
customVCC.save();
};
// render app
const render = () => {
let colors = activeTheme.colors;
let mixins = activeTheme.mixins;
let containerStyle = `
color: ${colors['foreground.default']};
background-color: ${colors['background.default']};
${mixins['font.defaultFamily']};
`;
let selectStyle = `
color: ${colors['input.foreground']};
background-color: ${colors['input.background']};
border-color: ${colors['border.default']};
`;
document.body.innerHTML = `
<div id="container" style="${containerStyle}">
<select id="input" style="${selectStyle}">
<option>${isLoaded ? 'Select an option...' : 'Loading options...'}</option>
${items.map((item, item_index) => {
let val = JSON.stringify(item).replace(/"/g, '"');
let checked = false;
let itemNameKey = instantRemixing.get(["settings","item_name_key"]);
if (itemNameKey) {
checked = value ? item[itemNameKey] === value[itemNameKey]: false;
} else {
checked = item === value;
}
return (
`<option value="${val}" ${checked ? 'selected' : ''}>
${itemNameKey ? item[itemNameKey]: item}
</option>`
);
})}
</select>
</div>
`;
let input = document.getElementById('input');
input.addEventListener('change', selectValue);
};
// call initial render
render();
function loadAPIValues() {
let url = instantRemixing.get(["settings","api_url"]);
fetch(url)
.then(res => res.json())
.then((result) => {
// Once loaded, set the items then re-render the application
isLoaded = true;
let itemsKey = instantRemixing.get(["settings","items_key"]);
items = itemsKey ? result[itemsKey] : result;
render();
});
}
export const defaultTheme = {
colors: {
'background.default': '#f6f8fa',
'border.default': '#34342e',
'foreground.default': '#d3d3d3',
'foreground.primary': '#358aeb',
'input.background': 'rgba(255,255,255,0.1)',
'input.foreground': '#d3d3d3',
},
mixins: {
'font.defaultFamily': 'font-family: "SF Pro Display", "SF Pro Icons", "Helvetica Neue", Helvetica, Arial, sans-serif;',
},
name: 'default',
};
Register your custom VCC to connect to Koji and set a listener to update your application state when the custom VCC updates.
componentDidMount() {
this.customVCC.register();
this.customVCC.onUpdate((props) => {
this.setState({value: props.value});
});
this.loadAPIValues();
}
customVCC.register();
customVCC.onUpdate((props) => {
// update Select value
value = props.value;
render();
});
Make a call to the API endpoint to retrieve the required values.
loadAPIValues() {
let url = instantRemixing.get(['settings', 'api_url']);
fetch(url)
.then(res => res.json())
.then((result) => {
let itemsKey = instantRemixing.get(['settings', 'items_key']);
this.setState({
isLoaded: true,
items: itemsKey ? result[itemsKey] : result
});
},
(error) => {
this.setState({
isLoaded: true,
error
});
}
);
}
function loadAPIValues() {
let url = instantRemixing.get(["settings","api_url"]);
fetch(url)
.then(res => res.json())
.then((result) => {
// Once loaded, set the items then re-render the application
isLoaded = true;
let itemsKey = instantRemixing.get(["settings","items_key"]);
items = itemsKey ? result[itemsKey] : result;
render();
});
}
Create a setter to return the selected value from your application back to the VCC on Koji.
Use customVCC.change()
to notify Koji that the VCC value has been updated and customVCC.save()
to tell Koji to commit the change to the remixer’s VCC.
selectValue(e) {
let val = JSON.parse(e.currentTarget.value);
this.customVCC.change(val);
this.customVCC.save();
}
const selectValue = (e) => {
let val = JSON.parse(e.currentTarget.value);
customVCC.change(val);
customVCC.save();
};
Use the Koji theme and the API results to render a drop-down menu that allows users to select a value.
render() {
let colors = this.state.theme.colors ? this.state.theme.colors : {};
let mixins = this.state.theme.mixins ? this.state.theme.mixins : {};
let items = this.state.items ? this.state.items : [];
let itemNameKey = instantRemixing.get(['settings', 'item_name_key']);
return (
<Container
color={colors['foreground.default'] ? colors['foreground.default'] : ""}
background={colors['background.default'] ? colors['background.default'] : ""}
fontMixin={mixins['font.defaultFamily'] ? mixins['font.defaultFamily'] : ""}>
<StyledSelect
background={colors['input.background'] ? colors['input.background'] : ""}
color={colors['input.foreground'] ? colors['input.foreground'] : ""}
border={colors['border.default'] ? colors['border.default'] : ""}
borderFocus={colors['foreground.primary'] ? colors['foreground.primary'] : ""}
value={JSON.stringify(this.state.value)}
onChange={this.selectValue.bind(this)}>
<option>{this.state.isLoaded ? 'Select an option...' : 'Loading options...'}</option>
{items.map(
(item, item_index) => {
return(
<option key={item_index} value={JSON.stringify(item)}>
{itemNameKey ? item[itemNameKey]: item}
</option>
)
}
)}
</StyledSelect>
</Container>
);
}
// render app
const render = () => {
let colors = activeTheme.colors;
let mixins = activeTheme.mixins;
let containerStyle = `
color: ${colors['foreground.default']};
background-color: ${colors['background.default']};
${mixins['font.defaultFamily']};
`;
let selectStyle = `
color: ${colors['input.foreground']};
background-color: ${colors['input.background']};
border-color: ${colors['border.default']};
`;
document.body.innerHTML = `
<div id="container" style="${containerStyle}">
<select id="input" style="${selectStyle}">
<option>${isLoaded ? 'Select an option...' : 'Loading options...'}</option>
${items.map((item, item_index) => {
let val = JSON.stringify(item).replace(/"/g, '"');
let checked = false;
let itemNameKey = instantRemixing.get(["settings","item_name_key"]);
if (itemNameKey) {
checked = value ? item[itemNameKey] === value[itemNameKey]: false;
} else {
checked = item === value;
}
return (
`<option value="${val}" ${checked ? 'selected' : ''}>
${itemNameKey ? item[itemNameKey]: item}
</option>`
);
})}
</select>
</div>
`;
let input = document.getElementById('input');
input.addEventListener('change', selectValue);
};
// call initial render
render();
Define styles for the template, using the theme values to match the user’s Koji theme.
const Container = styled.div`
font-size: 24px;
color: ${props => props.color};
background-color: ${(props) => props.background};
${(props) => props.fontMixin};
padding: 12px;
`;
const StyledSelect = styled.select`
-webkit-appearance: none;
background-color: ${props => props.background};
color: ${props => props.color};
border: 1px solid ${props=> props.border};
font-size: 16px;
padding: 12px 8px;
border-radius: 0;
width: 100%;
max-width: 100%;
background-image: url(data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23157afb%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E);
background-size: 0.65em, 100%;
background-repeat: no-repeat, repeat;
background-position: right 0.7em top 50%, 0px 0px;
margin-bottom: 20px;
&:focus {
outline: none;
border-color: ${props => props.borderFocus}
}
&:hover {
border-color: ${props => props.borderFocus}
}
`;
div#container {
font-size: 24px;
padding: 12px;
}
select#input {
-webkit-appearance: none;
border: 1px solid transparent;
font-size: 16px;
padding: 12px 8px;
border-radius: 0;
width: 100%;
max-width: 100%;
background-image: url(data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23157afb%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E);
background-size: 0.65em, 100%;
background-repeat: no-repeat, repeat;
background-position: right 0.7em top 50%, 0px 0px;
margin-bottom: 20px;
}
select#input:focus {
outline: none;
}
To save development time, Koji provides an easy way to test a custom VCC before it is published.
In your custom VCC project, click the remote staging button in the preview pane to open a preview in a new tab.
Open another project to act as the "consumer" for testing the VCC.
Tip
|
If you don’t have another project, you can create a test project by remixing the scaffold again. |
In your consumer project, open a JSON customization file, and add or edit a field to the custom VCC.
The type must match this format, using the URL you just copied as YOUR-URL:
"type": "custom<YOUR-URL>"
Save the file, and return to the Visual view of the customization file. You should see your custom VCC.
Custom VCCs require a custom domain to be accessible to other Koji applications.
Click Publish Now and enter a Name, Description and other publish settings, as desired. Then, click Publish New Version.
When publishing is completed, click the link to open your live template on Koji.
Go to Manage this Koji > Open Creator Dashboard > Custom Domains.
Add a new subdomain under the koji-vccs.com
root domain.
After you publish your custom VCC, you can use it in a Koji template with the VCC type:
"type": "custom<YOUR-DOMAIN-NAME>"
To see this blueprint as a completed template on Koji, visit the following link. From there, you can view the source code or remix the template into your own project.
The following code is a completed sample of the template logic described in this blueprint.
import React from 'react';
import styled from 'styled-components';
import { InstantRemixing } from '@withkoji/vcc';
import CustomVCC from '@withkoji/custom-vcc-sdk';
const Container = styled.div`
font-size: 24px;
color: ${props => props.color};
background-color: ${(props) => props.background};
${(props) => props.fontMixin};
padding: 12px;
`;
const StyledSelect = styled.select`
-webkit-appearance: none;
background-color: ${props => props.background};
color: ${props => props.color};
border: 1px solid ${props=> props.border};
font-size: 16px;
padding: 12px 8px;
border-radius: 0;
width: 100%;
max-width: 100%;
background-image: url(data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23157afb%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E);
background-size: 0.65em, 100%;
background-repeat: no-repeat, repeat;
background-position: right 0.7em top 50%, 0px 0px;
margin-bottom: 20px;
&:focus {
outline: none;
border-color: ${props => props.borderFocus}
}
&:hover {
border-color: ${props => props.borderFocus}
}
`;
const instantRemixing = new InstantRemixing();
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
value: '',
theme: {},
isLoaded: false
};
this.customVCC = new CustomVCC();
this.customVCC.onTheme((theme) => {
// theme is a Koji editor theme of the shape: { colors: {}, mixins: {} }
// save this value in order to style your VCC to match the user's current theme
this.setState({theme});
});
}
componentDidMount() {
this.customVCC.register();
this.customVCC.onUpdate((props) => {
this.setState({value: props.value});
});
this.loadAPIValues();
}
loadAPIValues() {
let url = instantRemixing.get(['settings', 'api_url']);
fetch(url)
.then(res => res.json())
.then((result) => {
let itemsKey = instantRemixing.get(['settings', 'items_key']);
this.setState({
isLoaded: true,
items: itemsKey ? result[itemsKey] : result
});
},
(error) => {
this.setState({
isLoaded: true,
error
});
}
);
}
selectValue(e) {
let val = JSON.parse(e.currentTarget.value);
this.customVCC.change(val);
this.customVCC.save();
}
render() {
let colors = this.state.theme.colors ? this.state.theme.colors : {};
let mixins = this.state.theme.mixins ? this.state.theme.mixins : {};
let items = this.state.items ? this.state.items : [];
let itemNameKey = instantRemixing.get(['settings', 'item_name_key']);
return (
<Container
color={colors['foreground.default'] ? colors['foreground.default'] : ""}
background={colors['background.default'] ? colors['background.default'] : ""}
fontMixin={mixins['font.defaultFamily'] ? mixins['font.defaultFamily'] : ""}>
<StyledSelect
background={colors['input.background'] ? colors['input.background'] : ""}
color={colors['input.foreground'] ? colors['input.foreground'] : ""}
border={colors['border.default'] ? colors['border.default'] : ""}
borderFocus={colors['foreground.primary'] ? colors['foreground.primary'] : ""}
value={JSON.stringify(this.state.value)}
onChange={this.selectValue.bind(this)}>
<option>{this.state.isLoaded ? 'Select an option...' : 'Loading options...'}</option>
{items.map(
(item, item_index) => {
return(
<option key={item_index} value={JSON.stringify(item)}>
{itemNameKey ? item[itemNameKey]: item}
</option>
)
}
)}
</StyledSelect>
</Container>
);
}
}
export default App;
import './styles.css';
import { InstantRemixing } from '@withkoji/vcc';
import CustomVCC from '@withkoji/custom-vcc-sdk';
import { defaultTheme } from './default-theme';
var activeTheme = defaultTheme;
var isLoaded = false;
var items = [];
var value = '';
const instantRemixing = new InstantRemixing();
const customVCC = new CustomVCC();
customVCC.onTheme((theme) => {
// theme is a Koji editor theme of the shape: { colors: {}, mixins: {} }
// update the theme value and re-render the application
activeTheme = theme;
render();
});
customVCC.register();
customVCC.onUpdate((props) => {
// update Select value
value = props.value;
render();
});
loadAPIValues();
const selectValue = (e) => {
let val = JSON.parse(e.currentTarget.value);
customVCC.change(val);
customVCC.save();
};
// render app
const render = () => {
let colors = activeTheme.colors;
let mixins = activeTheme.mixins;
let containerStyle = `
color: ${colors['foreground.default']};
background-color: ${colors['background.default']};
${mixins['font.defaultFamily']};
`;
let selectStyle = `
color: ${colors['input.foreground']};
background-color: ${colors['input.background']};
border-color: ${colors['border.default']};
`;
document.body.innerHTML = `
<div id="container" style="${containerStyle}">
<select id="input" style="${selectStyle}">
<option>${isLoaded ? 'Select an option...' : 'Loading options...'}</option>
${items.map((item, item_index) => {
let val = JSON.stringify(item).replace(/"/g, '"');
let checked = false;
let itemNameKey = instantRemixing.get(["settings","item_name_key"]);
if (itemNameKey) {
checked = value ? item[itemNameKey] === value[itemNameKey]: false;
} else {
checked = item === value;
}
return (
`<option value="${val}" ${checked ? 'selected' : ''}>
${itemNameKey ? item[itemNameKey]: item}
</option>`
);
})}
</select>
</div>
`;
let input = document.getElementById('input');
input.addEventListener('change', selectValue);
};
// call initial render
render();
function loadAPIValues() {
let url = instantRemixing.get(["settings","api_url"]);
fetch(url)
.then(res => res.json())
.then((result) => {
// Once loaded, set the items then re-render the application
isLoaded = true;
let itemsKey = instantRemixing.get(["settings","items_key"]);
items = itemsKey ? result[itemsKey] : result;
render();
});
}