Start typing to search...
Docs
Blueprints
Cat selector VCC
Search

Cat selector VCC

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.

Make a custom VCC on Koji

Prerequisites

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

Level

  • Koji: Intermediate – Advanced

  • Developer: Intermediate – Advanced

  • Time: 30 minutes

Building blocks

Remix the scaffold

Remix an existing web application on Koji that implements basic elements of your favorite framework.

React
Vanilla JS

Install the packages

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 the customization files

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"
        }
      ]
    }
  ]
}
  1. You will add custom values here in the next step.

Remove the unused customization files.

colors.json, images.json, and strings.json

Configure the API

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"

Add the template logic

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).

React
Vanilla JS
React
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});
  });
}
Example 3. Vanilla JS
index.js
default-theme.js
Example 1. index.js
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, '&quot;');
                    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();
    });
}
Example 2. default-theme.js
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.

React
Vanilla JS
React
componentDidMount() {
  this.customVCC.register();
  this.customVCC.onUpdate((props) => {
      this.setState({value: props.value});
  });
  this.loadAPIValues();
}
Vanilla JS
customVCC.register();
customVCC.onUpdate((props) => {
  // update Select value
  value = props.value;
  render();
});

Make a call to the API endpoint to retrieve the required values.

React
Vanilla JS
React
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
        });
      }
    );
}
Vanilla JS
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.

React
Vanilla JS
React
selectValue(e) {
  let val = JSON.parse(e.currentTarget.value);
  this.customVCC.change(val);
  this.customVCC.save();
}
Vanilla JS
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.

React
Vanilla JS
React
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>
  );
}
Vanilla JS
// 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, '&quot;');
                    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();

Add the styles

Define styles for the template, using the theme values to match the user’s Koji theme.

React
Vanilla JS
React
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}
  }
`;
Vanilla JS
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;
}

Test

To save development time, Koji provides an easy way to test a custom VCC before it is published.

  1. In your custom VCC project, click the remote staging button Remote staging button in the preview pane to open a preview in a new tab.

  2. 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.
  3. 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>"

  4. Save the file, and return to the Visual view of the customization file. You should see your custom VCC.

Publish

Custom VCCs require a custom domain to be accessible to other Koji applications.

  1. Click Publish Now and enter a Name, Description and other publish settings, as desired. Then, click Publish New Version.

  2. When publishing is completed, click the link to open your live template on Koji.

  3. Go to Manage this Koji > Open Creator Dashboard > Custom Domains.

  4. 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>"

"As Built" sample code

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.

React
Vanilla JS
React
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;
Vanilla JS
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, '&quot;');
                    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();
    });
}