Giter VIP home page Giter VIP logo

formio-sfds's Introduction

title
Docs

formio-sfds

This is the Form.io theme for sf.gov.

Documentation

Usage

Browser

The "standalone" JavaScript bundle is intended to be used in a browser (or browser-like environment) via <script> tags. You'll need to include the formiojs library in your document before formio-sfds, like so:

<script src="https://unpkg.com/[email protected]/dist/formio.full.min.js"></script>
<script src="https://unpkg.com/[email protected]/dist/formio-sfds.standalone.js"></script>

CSS

The CSS in this package provides a suite of styles that target a mix of Form.io-generated selectors and classes used in the custom theme templates, as well as a collection of general-purpose utility classes for tweaking individual elements.

Note: The formio-sfds.standalone.js bundle injects the CSS into the <head> of the host document at runtime, so there's no need for a separate <link> tag.

Scoped CSS

All of the selectors in the packaged CSS are scoped to (nested in) a .formio-sfds class selector, which effectively prevents them from leaking into the page where the form is embedded.

Unless you're using the standalone bundle (which wraps the form elements automatically), you'll need to wrap all of your the elements targeted by Formio.createForm() with a <div class="formio-sfds">.

Form.io portal

The "portal" bundle is intended for use inside the form.io admin UI, and customizes the palette of available components with this theme's components. To use it, visit your form.io project Settings page, and paste the URL below into the "Custom Javascript" (sic) field:

https://unpkg.com/[email protected]/dist/portal.js


License: MIT

Creating new versions

  1. Open a PR in the formio-sfds repo (may need to add you as a collaborator so that you can open PRs directly and GitHub Actions will publish your changes to npm automatically)

  2. In the PR checks, when you click “Show all checks”, you can copy the published version from the “publish formio-sfds” check:

  3. Pop that 0.0.0- value into the "Form.io SFDS version" field on /admin/config/services/sfgov_formio in your local and multidev environments (not in the committed settings YAML — we only want to commit published versions)

  4. Test your changes and get them reviewed in your multidev

  5. Get the changes to formio-sfds approved and released

  6. Update config/sfgov_formio.settings.yml by setting formio_sfds_version to the newly published release version (i.e. 9.2.3)

  7. Re-test after deploying again to make sure it still works

formio-sfds's People

Contributors

aekong avatar c3gabe avatar dependabot[bot] avatar github-actions[bot] avatar hshaosf avatar jimbrodbeck avatar namibiatorres avatar nlsfds avatar shawnbot avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

formio-sfds's Issues

The "language" option is overridden by DOM lang attributes

I ran into this trying to get the new shared components example in #160 to respect the language option from the query string. The problem is that if any element above the form in the DOM has a lang attribute, its value overrides the language option passed in explicitly to the form constructor.

This example would alert "en" rather than "zh":

<html lang="en">
  <div id="form"></div>
  <!-- formio.js + formio-sfds here -->
  <script>
    Formio.createForm(document.getElementById('form'), 'https://sfds.form.io/foo', {
      language: 'zh'
    }).then(form => alert(form.options.language))
  </script>
</html>

Language detection overrides form options

The updateLanguage() function that gets called both when a form is created and in the mutation observer for all forms should probably ignore the document language if the language was specified explicitly. Here's the current function:

formio-sfds/src/patch.js

Lines 179 to 184 in 6eef2d0

function updateLanguage (form) {
const closestLangElement = form.element.closest('[lang]:not([class*=sfgov-translate-lang-])')
if (closestLangElement) {
form.language = closestLangElement.getAttribute('lang')
}
}

It should probably bail early if the language option was provided, like so:

function updateLanguage (form) { 
+ if (form.options.language) return
  const closestLangElement = form.element.closest('[lang]:not([class*=sfgov-translate-lang-])') 
  if (closestLangElement) { 
    form.language = closestLangElement.getAttribute('lang') 
  } 
} 

Autocomplete word wrap needs work

From @nicolesque:

Screen Shot 2021-01-21 at 1 22 10 PM

When you resize the window the words in the dropdown break letter by letter, I assume it should bring the whole word.

This is a CSS issue that appears to be impossible to fix without a new release of our theme. I’ve tried adding a custom word-break: normal !important style to the element (by editing the JSON directly 😱), but it doesn’t take precedence over this rule in the Choices CSS.

Find a better templating solution

TL;DR: I think we need a better template format for our form.io components. In this issue I'm attempting to work through the benefits and downsides of what we have and investigate other options.

I've been thinking a lot lately about how painful our EJS(-ish) templates, which we inherited from formio.js, are to edit. EJS is certainly one of, if not the, most minimal templating "languages", and the biggest advantage that it has over other languages is the lack of a runtime. For example, this:

<h1>{{ ctx.title }}</h1>

Is compiled into something like this:

module.exports = ctx => {
  let out = '<h1>'
  out += ctx.title
  out += '</h1>'
  return out
}

However, EJS has some authoring "ergonomics" issues. It quickly gets illegible when you start wrapping control structures (if, else, for loops, etc.) around the output directives ({{ output }}), especially with formio.js's choice of Django/Jinja/Nunjucks-style delimiters ({% %} instead of <% %>):

{% if (title) { %}
  <h1>{{ title }}</h1>
{% } %}

I get a headache just looking at it; there are just too many curly braces, and there's no syntax highlighter that understands it. Compare the Django/Jinja/Nunjucks equivalent, which GitHub and any text editor worth its salt know how to syntax-highlight:

{% if title %}
  <h1>{{ title }}</h1>
{% endif %}

Twig

Twig templates have a very similar syntax, and the benefit of using a common template format is that we could possibly share "components" (templates) between this repo and sf.gov. However, while the ergonomics of the template syntax are nice, the runtime alone weighs ~26K gzipped. In other words, we'd have to bring along 26K just to render the templates at runtime, whereas EJS "compiles down" to just a plain old JavaScript function that returns a string.

I don't know that we gain enough from using Twig to justify a 26K (+40% compared to the current 65K) bundle size increase.

JSX

Over in #107 I attempted to see what using JSX syntax as a template format, but with a lightweight string renderer called vdo in place of the React runtime. The gist is that this:

export default props => <h1>{props.title}</h1>

Gets compiled into:

const { createElement: h } = require('vdo') // this gets hoisted elsewhere
module.exports = props => h('h1', null, [props.title])

And the h() function just returns the HTML string representation of the element, as in:

h('div', {id: 'foo'}, ['bar']) === '<div id="foo">bar</div>'

The vdo runtime weighs in at ~1K. You can see in this comment that with a couple of templates translated to JSX and vdo in the mix, the bundle is ~3K (4%) larger. I think that if we were to convert the rest of our templates to JSX the bundle would end up smaller. (Imagine every out+='...' statement replaced by a nested h('div', ...) call, attributes expressed as JS object literals, shorter conditionals, etc.)

Anyway, more here in the near future!

Refactor translation process

After working on an urgent form this weekend, I've found that the way we're minting translation strings automatically to be kind of a headache. Some challenges:

  • For one, it's tempting to edit the strings in form.io after you've put them into Phrase, only to re-discover during testing that your changes will be overridden by translations.

  • Secondly, not all of the strings are "caught" by the API that we use to generate the initial Phrase upload. Some of these include:

    • Select component options labels with autocomplete (Choices.js). For the form in question, I ended up having to rename the Phrase keys from key.values.* to the English label in order for the t() function called on the labels to find the translations.
    • For some custom messages (?) I ended up having to put the Phrase key into the form.io field, e.g. componentKey.validate.customMessage.

It would be nice when you're in form.io to: 1) see the form in other languages with Phrase translations loaded, and 2) know which strings have been "moved" from form.io to Phrase for the purposes of translating. What if, for instance, there was a process that could:

  1. Find all of the human/English content in the form (or just gave you a UI to run down the list and check or uncheck each one??)
  2. Generate appropriate string keys automatically (e.g. by their "path" in the data)
  3. Replace all of the English content in form.io with the string keys so that it's really obvious what can and can't be edited there.
  4. Maybe augment the form.io editor with links to each Phrase key alongside the field in question??

Figure out a different way to do form field localizations

We currently have a kind of funky way of overriding field label and description localizations. The trick is to set the string ID column in your spreadsheet to {key}_{property}, where {key} is the API key of the field you're targeting and {property} is the "part" of the field you're localizing: usually label or description, but sometimes content for HTML element components. There are some issues with this approach:

  1. This hack overrides the label, description (etc.) of the field as written on form.io, which can lead to some confusion if people start editing the fields on form.io and see no change in the rendered form. It's often much easier to edit the spreadsheet than the form definition itself, but there's no feedback on form.io to let people know that those labels, descriptions, or content will be overridden on sf.gov.

  2. Because formiojs doesn't support i18next's ability to take an array of keys and treat the ones later in the list as fallbacks (i18next.t(['confirm.yes', 'yes']) returns yes, but form.t(['confirm.yes', 'yes']) returns confirm.yes,yes because it stringifies the array!), I've had to monkey-patch formio's t() implementation to do the Right Thing. This is... not ideal, and actually kind of a dangerous.

I'm not sure what the right way is to do this, but namespaces might help. Either way, it would be good to get formiojs working with fallback key arrays so that we don't have to maintain this ugly patch.

Context might also be useful. Ambiguous strings like "feet" (used as a field suffix) would benefit from context when translating, and (if passed in all of the template calls 😬 ) even help us identify where certain strings are being used.

Figure out how to localize Choices.js strings

This would need to happen at the patch level, and would need to be deferred until after translations load. Where we currently patch the select component's customOptions object, we could do this:

const prefix = `${component.key}.choices` // e.g. "yourJob.choices"
component.customOptions = Object.assign({
  noResultsText: form.t(`${prefix}.noResultsText`, 'No results found'),
  noChoicesText: form.t(`${prefix}.noChoicesText`, 'No choices to choose from'),
  itemSelectText: form.t(`${prefix}.itemSelectText`, 'Press to select'),
  addItemText (value) {
    return form.t(`${prefix}.addItemText`, 'Press Enter to add <b>"{{value}}"</b>', { value })
  },
  maxItemText (count) {
    // using {{count}} here enables pluralization in i18next:
    // <https://www.i18next.com/translation-function/plurals>
    return form.t(`${prefix}.maxItemText`, 'Only {{count}} values can be added', { count })
  }
}, component.customOptions)

Upgrade to formio.js 4.13.x

As discussed in formio/formio.js#3690, a future release of formio.js will include an option for autocomplete select components that prevents submission of freeform values. We want this option, but we'll probably need to do some compatibility work with the upgrade.

Address component schemas are difficult to control

So, we have a pretty hairy problem with the address component. It's complicated, and the behavior of the component differs pretty greatly depending on how the component was added to the form.

  • When you drag a component into the form in the portal, the "builder" gets the default schema of the component from its class definition. The important part of the schema to note is the components, or children, which will be a list of components from either the formio.js built-in Address component or our custom Address component.

    Whichever schema is read by the form builder gets saved to the form definition, and always overrides whatever we have in our theme. That's why, when the built-in Address component is used, our theme still renders the set of fields defined in formio.js (rather than our theme). And, even if we've added one of our custom components, updates to the schema in our theme will be ignored because the component's children on the server "win" when they're merged at runtime.

  • When we instantiate forms with components as {type: 'address'} and (basically) nothing else in the schema, as in examples.yml, the default schema is read from our component class at runtime and merged into the one defined in YAML. Because the schema we've provided doesn't have a components key, the one in our component class "wins". This is why our examples all render our address inputs.

So, here's how this is breaking right now:

  1. Forms that got address components from our custom schema on the form.io portal are "locked in" to whatever default schema was defined in the version of our theme that we told the portal to load (most recently, 6.1.0).
  2. Forms that use the built-in address component have a schema that includes a Country field between state and ZIP code, and none of the fields are required.

Neither of these is doing the right thing, because the schema that they were locked into doesn't include the right customClass fields that get the spacing right. #125 was intended to fix vertical spacing between the city and ZIP inputs on smaller screens, but only works for our example components; any form with the components locked in ignores the custom classes we've updated in the default schema.

The other thing that's complicated is deciding when to "customize" the address component:

  • If we use custom JS on the portal to tell the form builder about our component, we should give it a default schema that renders the component at least somewhat accurately in the builder.

  • We could also modify the component class to override the schema at runtime, which would give us tighter control over how fields are rendered in new releases of this theme.

    However, if we override the schema at runtime, then form designers can't customize the address fields in their forms unless we build a custom editing UI for them.

  • There might also be a better way to control the field rendering in the template rather than via custom classes in the schema. Need to investigate this.

There are a couple of different ways that we can fix this, but they all have positives and negatives. I'll enumerate those in a bit, but I wanted to get this down for now and reach out to the form.io folks for some advice before embarking on a solution.

Component labels aren't translated in validation messages

Well, this is not great:

The problem here is that even though it's properly translating the error message, formio.js is passing an un-translated string into it:

message(component) {
  //                                        "required" is translated...
  //                                         ↓
  return component.t(component.errorMessage('required'), {
    field: component.errorLabel, // ← but the error label placeholder is not
    data: component.data
  });
}

In this case, it's using the correct required translation ("{{field}} es requerido"), and interpolating the {{field}} placeholder with the un-translated label ("Your name") instead of the translation of the field's label ("Su nombre").

Workaround

I believe the workaround is to add Phrase translations for {key}.validate.customMessage, as formio.js's validation logic appears to prefer the component's validate.customMessage property over the message of the validation(s) that failed. In this case, the name.validate.customMessage Phrase translations could be "Your name is required" in English and "Su nombre es requerido" in Spanish.

Patch

The place to patch this is in the errorLabel getter of the Component class:

  /**
   * Returns the error label for this component.
   * @return {*}
   */
  get errorLabel() {
    return this.t(this.component.errorLabel
      || this.component.label
      || this.component.placeholder
      || this.key);
  }

For our case (Phrase translations with predictable keys), we'd patch it as something like this:

  get errorLabel () {
    const { key, errorLabel, label, placeholder } = this.component
    return this.t([
      `${key}.errorLabel`,
      `${key}.label`,
      `${key}.placeholder`,
      errorLabel || label || placeholder || key
    ])
  }

Datagrid style fixes

The data grid has various style issues that have appeared over time and needs to be updated.
Screen Shot 2022-05-02 at 12 04 38 PM

  1. Table needs to be full width to allow for more content to be displayed
  2. Field heights are not equal
  3. "Remove" button icon is not vertically centered
  4. Dropdown list items are line-breaking too quickly
  5. Dropdown with a selection obfuscates the delete and dropdown icons
  6. Add button icon needs right-margin

Proposed changes

  • Add w-full to table line 1
  • Decide padding as rem or px for all inputs
  • Remove v-align-top to tr on line 38
  • Align the icons and fix spacing for the buttons (X and Add Another)
  • Remove padding-right: 100px for the selections
  • Fix word break in the select dropdown
  • Make sure the selections do not overlay the Close icon

Validate all components on 'blur' rather than 'change'

Rapid validations that update the DOM on (practically) every keystroke can be an accessibility issue for some, and a legitimate annoyance for others. We should patch components that haven't explicitly been set up to validate on change to validate on blur.

Generic translations don't always play nicely with form-specific Phrase projects

I haven't had time to dig into it, but I discovered a couple of cases in a recent form where strings that we have generic translations checked into git weren't being applied to a form with Phrase translations of its own: the "Next" and "Back" buttons, and generic validation error messages like invalid_email.

There might be a bug in how we're merging the two translation "resource" objects? 🤷

Filipino translations don't always work on sf.gov

The Filipino translation of this form is unreliable. It's hard to tell what's going on, but I think it's a timing problem or race condition with the language detection in this theme. Observations:

  • The translations appear not to work the "first" load (with a cold cache?), but then work on subsequent refreshes.
  • The lang="fil" attribute is stable: it's served from Drupal that way and remains unchanged after Google Translate loads.

I'd like to figure out how to write a failing test for this before trying to figure out a fix, since there are a lot of moving pieces.

Explore more options for translation embedding

The runtime integration with Phrase has some issues:

  1. Using the form's custom properties introduces a delay in rendering the translated form: the form renders, translations are fetched from our API, then the form re-renders when the translations are loaded.
  2. Copying form.io forms between environments ("stages") doesn't copy the custom properties, so we have to remember to copy them manually.

The fix for the copying issue is to move the translation properties to the "Form.io render options" JSON blob in Drupal. However, in order for this to also fix the rendering delay we would need to either load the translations before the form (which would delay rendering anything) or load the form and the translations in parallel, like:

const [translations, form] = await Promise.all(loadTranslations(options), createForm(...args))
applyTranslations(translations, form)
form.redraw()

Drupal

Another option to consider is that translations could be loaded into Drupal from Phrase somehow (at publish time?), and embedded in the form options in our template. @jacine @aekong I would love your thoughts on this.

Get custom components working in the form.io portal

In #42 I tested adding a new JS output bundle and demoed it with our (my?) form.io Sandbox project. Unfortunately, it doesn't work.

The problem appears to be that the portal (at least, portal.form.io — I'm still unable to log in to next.form.io using my GitHub-authenticated account) uses the Angular renderer, which doesn't provide the same level of access to the built-in component classes as formiojs (via the window.Formio.Components.components object).

Possible solutions include:

  • Import the component base classes directly from formiojs/components/.... The problem with this approach is that the base classes rely on a ton of other dependencies (lodash! 😭 ) that would bloat our implementation significantly.
  • Figure out where to find the component base classes in the global exports of angular-formio. I have no idea where to start looking for these in an Angular project.
  • Figure out how to use the angular implementation's custom component feature, which the wiki page claims warns is a "community contributed feature to this module. Form.io company does not provide support for this feature". ☹️

Maybe Form.io support could help us with this?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.