Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 54 additions & 52 deletions content/en/building/tutorials/app-forms.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,70 +39,72 @@ You should have a [functioning CHT instance with `cht-conf` installed locally]({

## Implementation Steps

Create a new spread sheet in Google sheets or other preferred editor like Excel or Open Office. Name the spreadsheet `assessment`. The final file name should be `assessment.xlsx`.
Create a new spread sheet in [Google Sheets](https://workspace.google.com/products/sheets/) or other preferred editor like [Excel](https://www.microsoft.com/en-us/microsoft-365/excel) or [LibreOffice](https://www.libreoffice.org/). Name the spreadsheet `assessment`. The final file name should be `assessment.xlsx`.

Create 2 additional sheets. Rename the sheets `survey`, `choices` and `settings`.

### 1. Define XLS Survey/Form Fields

Create the following columns in the survey sheet and then add the following rows that are populated automatically before the form is rendered to the user. These fields are usually hidden by default but can be accessed to display certain information about the person being assessed:

| type | name | label | required | relevant | appearance | constraint | constraint_message | calculation | choice_filter | hint | default |
|-------------| --------------- | --------------------------- | -------- | ----------------- |------------| ---------- | ------------------- | ---------------------- | -------------- | ----------------------------- | ------- |
| begin group | inputs | Patient | | ./source = 'user' | field-list | | | | | | |
| hidden | source | Source | | | | | | | | | user |
| hidden | source_id | Source_ID | | | | | | | | | |
| hidden | task_id | Task_ID | | | | | | | | | |
| begin group | contact | Contact | | | | | | | | | |
| string | _id | Patient ID | | | select-contact type-person | | | | | Select a person from the list | |
| hidden | patient_id | Medic ID | | | | | | | | | |
| hidden | name | Patient Name | | | | | | | | | |
| begin group | parent | Parent | | | | | | | | | |
| hidden | _id | Family UUID | | | | | | | | | |
| begin group | parent | Grandparent | | | | | | | | | |
| hidden | _id | CHW Area UUID | | | | | | | | | |
| hidden | name | CHW Name | | | | | | | | | |
| hidden | phone | CHW Phone | | | | | | | | | |
| begin group | parent | Great Grandparent | | | | | | | | | |
| hidden | _id | CU UUID | | | | | | | | | |
| end group | | | | | | | | | | | |
| end group | | | | | | | | | | | |
| end group | | | | | | | | | | | |
| end group | | | | | | | | | | | |
| end group | | | | | | | | | | | |
| calculate | patient_id | | | | | | | ../inputs/contact/_id | | | |
| calculate | patient_name | | | | | | | ../inputs/contact/name | | | |


Add the following rows that define the data collection fields below the existing rows (leave out the column names):

| type | name | label | required | relevant | appearance | constraint | constraint_message | calculation | choice_filter | hint | default |
| ----------------------------- | ----------------- | ---------------------------------- | -------- | ------------------- | ---------- | ---------- | ------------------- | ----------- | -------------- | ---- | ------- |
| begin group | group_assessment | Assessment | | | | | | | | | |
| select_one yes_no | cough | Does ${patient_name} have a cough? | yes | | | | | | | | |
| select_one symptom_duration | cough_duration | How long has the cough lasted? | yes | ${cough} = 'yes' | | | | | | | |
| end group | | | | | | | | | | | |
This rows are populated automatically before the form is rendered to the user. These fields are usually hidden by default but can be accessed to display certain information about the person being assessed.

Copy everything inside the table below by clicking the "📋 Copy survey sheet" button, then paste into cell A1 of a new sheet named `survey` in Google Sheets or your prefered editor:

{{< copytable id="survey" label="Copy survey sheet" >}}
type name label required relevant appearance constraint constraint_message calculation choice_filter hint default
begin group inputs Patient ./source = 'user' field-list
hidden source Source user
hidden source_id Source_ID
hidden task_id Task_ID
begin group contact Contact
string _id Patient ID select-contact type-person Select a person from the list
hidden patient_id Medic ID
hidden name Patient Name
begin group parent Parent
hidden _id Family UUID
begin group parent Grandparent
hidden _id CHW Area UUID
hidden name CHW Name
hidden phone CHW Phone
begin group parent Great Grandparent
hidden _id CU UUID
end group
end group
end group
end group
end group
calculate patient_id ../inputs/contact/_id
calculate patient_name ../inputs/contact/name
begin group group_assessment Assessment
select_one yes_no cough Does ${patient_name} have a cough? yes
select_one symptom_duration cough_duration How long has the cough lasted? yes ${cough} = 'yes'
select_one yes_no fever Does ${patient_name} have a fever? yes
select_one yes_no diarrhea Does ${patient_name} have diarrhea? yes
end group
end group
{{< /copytable >}}

### 2. Define the Choices

Add the following column names and rows to the choices sheet:
Copy everything inside the table below by clicking the "📋 Copy choices sheet" button, then paste into cell A1 of a new sheet named `choices` in Google Sheets or your prefered editor:

| list_name | name | label |
| ----------------- | ---- | --------------- |
| yes_no | yes | Yes |
| yes_no | no | No |
| symptom_duration | 3 | 3 days or less |
| symptom_duration | 7 | 4 - 7 days |
| symptom_duration | 13 | 8 - 13 days |
| symptom_duration | 14 | 14 days or more |
{{< copytable id="choices" label="Copy choices sheet" >}}
list_name name label
yes_no yes Yes
yes_no no No
symptom_duration 3 3 days or less
symptom_duration 7 4 - 7 days
symptom_duration 13 8 - 13 days
symptom_duration 14 14 days or more
{{< /copytable >}}

### 3. Define the XLS Settings

Add the following column names and rows to the settings sheet:
Copy everything inside the table below by clicking the "📋 Copy settings sheet" button, then paste into cell A1 of a new sheet named `settings` in Google Sheets or your prefered editor:

| form_title | form_id | version | style | path | instance_name | default_language |
| -------------- | ---------- | ------- | ----- | ---- | -------------- | ----------------- |
| Assess patient | assessment | 1 | pages | data | | en |
{{< copytable id="settings" label="Copy settings sheet" >}}
form_title form_id version style path instance_name default_language
Assess patient assessment 1 pages data en
{{< /copytable >}}

### 4. Convert the XLSForm and Upload the XForm

Expand All @@ -126,4 +128,4 @@ cht --url=https://<username>:<password>@localhost --accept-self-signed-certs con

## Next steps

In the next tutorial, you will define the form `<form_id>.properties.json` which will allow you to define the form’s title and icon, as well as when and where the form should be available.
- *[Setting Form Properties]({{< ref "building/forms/form-properties/" >}})*
33 changes: 33 additions & 0 deletions content/en/community/contributing/docs/style-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,39 @@ To achieve this use a markdown table with the letter X (`X`) to mark events, lea
> [!TIP]
> The opening `{{%/* schedule */%}}` and closing `{{%/* /schedule */%}}` shortcode must come before and after the markdown table respectively in order for it to correctly style the table.

### Copy-paste-friendly Tables

You can include a visual table in your documentation with a **"Copy table" button** that makes it easy for users to copy the table contents (as TSV – tab-separated values). This is useful for sharing structured data that users may want to paste into a spreadsheet like Google Sheets or another tool.

Use the following shortcode format:

{{</* copytable id="example" label="Copy table" >}}
Column A Column B Column C
Value-1 A1 B1
Value-2 A2 B2
{{</ copytable */>}}

This will render:

- A nicely styled HTML table.
- A **Copy** button above the table.
- When clicked, the button copies the table content (as plain text) to the clipboard.
- A small “Copied!” message briefly appears to confirm the action.

#### Example output

{{< copytable id="tsv" label="Copy TSV" >}}
Name Role Region
Fatou CEO Senegal
Jose Developer Costa Rica
{{</ copytable >}}

#### Notes

- The copied content uses **tab-separated values**, which work well in spreadsheets or plain text files.
- The button adapts automatically to **light and dark themes**.
- You can customize the button label with the `label` parameter (default: `"Copy table"`).

### Grammar and punctuation in headers

Use title case for page `title`, and sentence case for `linkTitle` description. Do not end titles with periods.
Expand Down
91 changes: 91 additions & 0 deletions layouts/shortcodes/copytable.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<style>
:root {
--btn-bg: #f3f4f6;
--btn-text: #374151;
--btn-border: #d1d5db;
--btn-hover-bg: #e5e7eb;
--btn-hover-text: #111827;
--btn-active-bg: #d1d5db;
}

html.dark {
--btn-bg: #1f2937;
--btn-text: #e5e7eb;
--btn-border: #4b5563;
--btn-hover-bg: #374151;
--btn-hover-text: #f9fafb;
--btn-active-bg: #4b5563;
}

.copy-btn {
Comment thread
mrjones-plip marked this conversation as resolved.
Resolved
display: inline-flex;
align-items: center;
gap: 0.5em;
background: var(--btn-bg);
color: var(--btn-text);
border: 1px solid var(--btn-border);
border-radius: 6px;
padding: 0.35em 0.9em;
font-size: 0.95em;
font-weight: 500;
cursor: pointer;
transition: background 0.2s, color 0.2s, border 0.2s;
margin-bottom: 0.2em;
text-decoration: none;
}

.copy-btn:hover {
background: var(--btn-hover-bg);
color: var(--btn-hover-text);
}

.copy-btn:active {
background: var(--btn-active-bg);
}

.copied-msg {
display: none;
color: #22c55e;
font-weight: bold;
margin-left: 10px;
font-size: 1em;
}
.tsv-table-visual {
width: 100%;
border-collapse: collapse;
margin-bottom: 0.5em;
font-family: 'Fira Mono', 'Menlo', 'Monaco', 'Consolas', monospace;
font-size: 1em;
background: transparent;
}
.tsv-table-visual th, .tsv-table-visual td {
border: 1px solid #e5e7eb;
padding: 6px 10px;
background: transparent;
}
.tsv-table-visual th {
background: #f3f4f6;
font-weight: bold;
}
</style>
{{ $id := .Get "id" | default (now.UnixNano) }}
{{ $tsv := .Inner | safeHTML }}
<br>
<button class="copy-btn" onclick="navigator.clipboard.writeText(document.getElementById('tsv-{{ $id }}').innerText);const msg=document.getElementById('copied-msg-{{ $id }}');msg.style.display='inline';setTimeout(function(){msg.style.display='none';},1500);">
📋 {{ .Get "label" | default "Copy table" }}
</button>
<span id="copied-msg-{{ $id }}" class="copied-msg">Copied!</span>
{{/* Render visual table for user, but keep <pre> for copy-paste */}}
{{ $lines := split $tsv "\n" }}
<table class="tsv-table-visual">
{{ range $i, $line := $lines }}
{{ if ne (len (trim $line " ")) 0 }}
<tr>
{{ range (split $line "\t") }}
{{ if eq $i 0 }}<th>{{ . }}</th>{{ else }}<td>{{ . }}</td>{{ end }}
{{ end }}
</tr>
{{ end }}
{{ end }}
</table>
<pre id="tsv-{{ $id }}" style="display:none;">{{ $tsv }}</pre>