Survey Slate | Common Components

TextNodeView

A textNodeView syncs its value to a simple textNode DOM element. Because its a view, it can be added to a view-literal expression in quite a simple way.

//viewof textNodeViewExample = textNodeView("hi")
const textNodeViewExample = view(textNodeView("hi"))
textNodeViewExample
Inputs.button("Randomize textNodeViewExample", {
  reduce: () => {
    //viewof textNodeViewExample.value = Math.random();
    textNodeViewExample.value = Math.random();
    //viewof textNodeViewExample.dispatchEvent(new Event('input', {bubbles: true}))
    textNodeViewExample.dispatchEvent(new Event('input', {bubbles: true}))
  }
})
const textNodeView = (value = '') => {
  const node = document.createTextNode(value)
  return Object.defineProperty(node, 'value', {
    get: () => node.textContent,
    set: (val) => node.textContent = val,
    enumerable: true
  });
}

Logotype

const logotype = (name = "Survey Slate") => html`<div class="[ pa2 flex items-center w3 h3 ][ f6 lh-title b tracked-light ][ text-on-brand bg-accent ]">${name}`

One title


Multiple titles

const pageHeader = (titles, brandName = "Survey Slate") => {
  const header = html`<div class="flex bg-text-on-brand">
  <div class="flex-none">
    ${logotype(brandName)}
  </div>
  <div class="[ flex items-center ph3 w-100 ][ f6 f5-ns ]">
    ${titles.reduce((acc,t, i, arr) => {
      const isLast = i === arr.length - 1;
      const commonClasses = "lh-solid ma0";
      const specialClasses = isLast ? "b" : "dn db-ns mid-gray";
      const seperator = isLast ? "" : html`<span aria-hidden="true" class="mv0 mh2 black-20">/<span>`;

      return html`${acc}<p class="${commonClasses} ${specialClasses}">${t}${seperator}</p>`;
    }, "")}
  </div>
</div>`

  return header;
}

const pageFooter = (brandName = "Survey Slate") => {
  const linkClasses = "link brand underline-hover";
  const year = new Date().getFullYear();

  return html`<footer class="[ flex flex-wrap justify-center justify-between-l pa3 ph2 ph5-ns ][ f6 gray bg-white ]">
  <div class="[ flex flex-wrap justify-center ][ space-x-2 ]">
    © ${year} ${brandName}
  </div>
</footer>
`
}

Spinner (Loader)

spinner()
//!!!!!!!!!!!!!!!!
//!!! Check that this is the correct way to add this here
//!!!!!!!!!!!!!!!!
//const spinner = () => { 
//  return html`<span class="spinner">${getIconHtml("loader")}</span>`
//}
const spinner = () => {
  const t = document.createElement("template");
  t.innerHTML = `<span class="spinner">${getIconHtml("loader")}</span>`;
  return t.content.firstElementChild;
};

Button Label

Use buttonLabel() to generate HTML to populate label element in Inputs.button. This label generator supports icons.

///!!!!!!!!!!!!!!!!!!!!!
///!!!!!!Changes introduced in attaching buttons back into the DOM. Verify.
///!!!!!!!!!!!!!!!!!!!!!
const buttonLabel = ({label, iconLeft, iconRight, iconRightClass, iconLeftClass, ariaLabel}) => {
  let labelHtml = "";
  if (iconLeft) {
    labelHtml += `${getIconHtml(iconLeft, `icon--sm ${iconLeftClass || ""}`)} `;
  }

  if(label) {
    labelHtml += `<span class="button-label__text">${label}</span>`;
  }

  if (iconRight) {
    labelHtml += `${getIconHtml(iconRight, `icon--sm ${iconRightClass || ""}`)} `;
  }

  if (ariaLabel) {
    labelHtml += `<span class="clip">${ariaLabel}</span>`;
  }
  
  //return html`<span class="button-label">${labelHtml}</span>`
  // Turn the string into a real node so it won't be escaped.
  const t = document.createElement("template");
  t.innerHTML = `<span class="button-label">${labelHtml}</span>`;
  return t.content.firstElementChild;
}
const getIconHtml = (name, klasses = "") => `<span class="icon ${klasses}">${getIconSvg(name, 24, {role: 'img'})}</span>`

Styles

// Thanks @mootari, https://observablehq.com/@saneef/is-observable-inputs-style-able
const ns = Inputs.text().classList[0]
const styles = html`<style>
  :root {
    --button-border-radius: var(--border-radius-2, 0.25rem);
    --border-color: #aaa; /* tachyons's light-silver */
    --border-color-light: #eee; /* tachyons's light-gray */
  }

  /* https://observablehq.com/@saneef/is-observable-inputs-style-able */
  form.${ns} {
    width: auto;
  }

  .${ns} input,
  .${ns} textarea,
  .${ns} select,
  .${ns} button {
    font-family: var(--brand-font);
  }

  .${ns} input[type="text"],
  .${ns} textarea,
  .${ns} select,
  .${ns} button {
    background-color: white;
    border: 1px solid var(--border-color);
    border-radius: var(--button-border-radius);
  }

  .${ns} input[type="text"],
  .${ns} textarea,
  .${ns} button {
    padding: var(--spacing-extra-small) var(--spacing-small);
  }

  .${ns} select {
    padding-top: var(--spacing-extra-small);
    padding-bottom: var(--spacing-extra-small);
  }

  .${ns} button:hover,
  .${ns} button:focus,
  .${ns} button:active {
    background-color: var(--light-gray, #eee);
  }

  /* Icon */

  .icon {
    display: inline-block;
    position: relative;
    vertical-align: middle;
    width: 1.5rem;
    height: 1.5rem;
    color: var(--gray, #777)
  }

  .icon svg {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
  }

  .icon--sm {
    width: 1rem;
    height: 1rem;
  }
  
  .icon--danger {
    color: var(--red, #ff4136)
  }

  .icon--success {
    color: var(--green, #19a974)
  }

  /* Button Group*/

  .button-group {
    display: flex;
  }

  .button-group form.${ns} + form.${ns} {
    margin-left: -1px;
  }

  .button-group form.${ns} button {
      border-radius: 0;
    }

  .button-group form.${ns}:first-child button {
    border-top-left-radius: var(--button-border-radius);
    border-bottom-left-radius: var(--button-border-radius);
  }

  .button-group form.${ns}:last-child button {
    border-top-right-radius: var(--button-border-radius);
    border-bottom-right-radius: var(--button-border-radius);
  }

  /* Button Label */
  .button-label {
    display: inline-flex;
    align-items: center;
    vertical-align: middle;
  }

  .button-label > * + * {
    margin-left: var(--spacing-extra-small, 0.25rem);
  }
  .button-label__text {}

  /* Card */

  .card {
    display: block;
    background: white;
    padding: 1rem; /* pa3 or --spacing-medium */
    border: 1px solid var(--border-color-light);
    border-radius: var(--border-radius-3);
  }

  .card--compact {
    padding-top: 0.5rem;
    padding-bottom: 0.5rem;
  }

  /* Loader */
  @keyframes rotate {
    to {
      transform: rotate(360deg);
    }
  }
  .spinner .icon {
    color: var(--brand);
  }
  .spinner svg {
    animation: rotate ease-out 1.2s infinite;
  }
</style>`

Styles for the demo

html`
<link href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,400;0,700;1,400&display=swap" rel="stylesheet">
<style type="text/css" media="screen, print">
  body {
    font-family: var(--brand-font);
  }
</style>
`
tachyonsExt({
  colors: {
    brand: mainColors[900], // or, provide and color hex code
    accent: accentColors[900], // or, provide and color hex code
    // The color of text which are usually displayed on top of the brand or accent colors.
    "text-on-brand": "#ffffff",
  },
  fonts: {
    "brand-font": `"Roboto", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"`
  }
})

Imports

//import {tachyonsExt} from "@categorise/tachyons-and-some-extras"
import {tachyonsExt} from "/components/tachyons-and-some-extras.js"
display(tachyonsExt)
//import {toc} from "@nebrius/indented-toc"
import {toc} from "/components/indented-toc.js"
display(toc)
//import {mainColors, accentColors} from "@categorise/brand"
import {mainColors, accentColors} from "/components/brand.js"
display(mainColors)
display(accentColors)
//import {getIconSvg} from "@saneef/feather-icons"
import {getIconSvg} from "/components/feather-icons.js"
display(getIconSvg)