Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/template language 373 #385

Merged
merged 12 commits into from
Nov 16, 2021
1 change: 1 addition & 0 deletions docs/_Sidebar.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
- [Question parameters](form-builder/Question-parameters.md)
- [Conditions](form-builder/Conditions.md)
- [Interpolated values](form-builder/InterpolatedValues.md)
- [Templates](form-builder/Templates.md)

#### Locations
- [Search](locations/Search.md)
Expand Down
59 changes: 59 additions & 0 deletions docs/form-builder/Templates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Templates
The underlying frameworks for Trellis are [Vue][0] and [Vuetify][2]. Trellis questions have full access to the [Vue template language][1] so that any HTML or Vuetify components can be used to display a question.

## Using form data in a template
To use a custom template in a Trellis question simply start the question text with an HTML element like `<span>`, `<div>` or `<p>`. Data can be accessed via the `vars` or `data` variables. Here's a simple example that displays the response for a question called "best_pet" in the question text:

```html
<span>The best pet is, "{{vars.best_pet}}"</span>
```

## More examples
### Add a link
```html
<p>
<a href="https://example.com">A link to something</a>
</p>
```

### Embed a Youtube video (requires internet access for tablets)
```html
<iframe width="1318" height="750" src="https://www.youtube.com/embed/dQw4w9WgXcQ" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope;" allowfullscreen></iframe>
```

### Show respondent photos in a follow up section
In this example we're assuming this question follows up to a relationship question called "friends" and because we're using asynchronous data, we've added a loading progress bar.
```html
<v-col>
<v-row class="text-h3">
Your friends
</v-row>
<v-row v-if="vars.friends" v-for="edge in vars.friends">
<v-col>
<v-row class="justify-space-around">
<h4 class="text-h4 text-center">
{{edge.targetRespondent.name}}
</h4>
</v-row>
<v-row v-if="edge.targetRespondent.photos.length">
<Photo
height="300"
is-centered
:photo="edge.targetRespondent.photos[0]" />
</v-row>
</v-col>
</v-row>
<v-progress-linear v-else indeterminate />
</v-col>
```

### Use subject's name in question
```html
<div>
Hello, {{subject.name || 'Loading...'}}!
</div>
```

[0]: https://vuejs.org
[1]: https://vuejs.org/v2/guide/syntax.html
[2]: https://vuetifyjs.com
5 changes: 4 additions & 1 deletion src/components/TrellisLoadingCircle.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@
},
computed: {
borderColor () {
return this.$vuetify.theme.currentTheme[this.color]
if (this.$vuetify && this.$vuetify.theme) {
const theme = this.$vuetify.theme.isDark ? this.$vuetify.theme.themes.dark : this.$vuetify.theme.themes.light
return theme[this.color]
}
},
containerStyles (): object {
return {
Expand Down
14 changes: 8 additions & 6 deletions src/components/interview/Interview.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<v-container fluid fill-height class="ma-0 pa-0">
<v-layout column>
<v-toolbar flat>
<v-container fluid fill-height class="ma-0 pa-0 justify-start align-start">
<v-col>
<v-toolbar flat dense class="flex-grow-0">
<v-toolbar-title>
<AsyncTranslationText
v-if="section"
Expand Down Expand Up @@ -38,7 +38,7 @@
:conditionTags="interviewConditionTags"
:interview="interview" />
</v-flex>
</v-layout>
</v-col>
<v-dialog
v-model="dialog.beginning">
<v-card>
Expand Down Expand Up @@ -122,7 +122,7 @@
</v-container>
</template>

<script>
<script lang="ts">
import Page from './Page'
import ConditionTagList from './ConditionTagList'
import AsyncTranslationText from '../AsyncTranslationText'
Expand Down Expand Up @@ -166,7 +166,9 @@
name: 'interview',
head: {
title: function () {
let d = {}
let d = {
inner: 'Interview'
}
if (this.type === 'preview') {
d.inner = 'Form preview: ' + interviewState.blueprint.id
} else if (this.interview.survey) {
Expand Down
101 changes: 53 additions & 48 deletions src/components/interview/Question.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,10 @@
<QuestionTimer v-if="showTimer" :duration="timerDuration" :showControls="showTimerControls"/>
<v-card-text class="question-content">
<v-flex class="question-text title">
<AsyncTranslationText
passive
:translation="question.questionTranslation"
:location="location">
</AsyncTranslationText>
<QuestionText
:location="location"
:question="question"
:subject="interview.survey.respondent" />
</v-flex>
<div
:is="currentQuestionComponent"
Expand All @@ -33,30 +32,34 @@
</template>


<script>
<script lang="ts">
// This parent component servers the purpose of handling general functionality that is used across all questions.
// For example, question title and message fills will be applied here. The question header text will be applied here
// import translationService from '../services/TranslationService'
import Vue, { PropOptions } from 'Vue'
import DontKnowRefused from './DontKnowRefused.vue'
import AsyncTranslationText from '../AsyncTranslationText'
import AsyncTranslationText from '../AsyncTranslationText.vue'
import TranslationMixin from '../../mixins/TranslationMixin'
import questionTypes from '../../static/question.types'
import ParameterType from '../../static/parameter.types'

import DateQuestion from './questions/DateQuestion'
import DecimalQuestion from './questions/DecimalQuestion'
import GeoQuestion from './questions/GeoQuestion'
import IntegerQuestion from './questions/IntegerQuestion'
import IntroQuestion from './questions/IntroQuestion'
import ImageQuestion from './questions/ImageQuestion'
import MultipleSelectQuestion from './questions/MultipleSelectQuestion'
import RelationshipQuestion from './questions/RelationshipQuestion'
import RespondentGeoQuestion from './questions/RespondentGeoQuestion'
import RosterQuestion from './questions/RosterQuestion'
import TextQuestion from './questions/TextQuestion'
import TextAreaQuestion from './questions/TextAreaQuestion'
import TimeQuestion from './questions/TimeQuestion'
import QuestionTimer from './QuestionTimer'
import DateQuestion from './questions/DateQuestion.vue'
import DecimalQuestion from './questions/DecimalQuestion.vue'
import GeoQuestion from './questions/GeoQuestion.vue'
import IntegerQuestion from './questions/IntegerQuestion.vue'
import IntroQuestion from './questions/IntroQuestion.vue'
import ImageQuestion from './questions/ImageQuestion.vue'
import MultipleSelectQuestion from './questions/MultipleSelectQuestion.vue'
import RelationshipQuestion from './questions/RelationshipQuestion.vue'
import RespondentGeoQuestion from './questions/RespondentGeoQuestion.vue'
import RosterQuestion from './questions/RosterQuestion.vue'
import TextQuestion from './questions/TextQuestion.vue'
import TextAreaQuestion from './questions/TextAreaQuestion.vue'
import TimeQuestion from './questions/TimeQuestion.vue'
import QuestionTimer from './QuestionTimer.vue'
import QuestionText from './QuestionText.vue'
import Question from '../../entities/trellis/Question'
import Interview from '../../entities/trellis/Interview'
import { InterviewLocation } from './services/InterviewAlligator'

const typeMap = {
[questionTypes.year]: DateQuestion,
Expand All @@ -81,19 +84,37 @@
export default {
name: 'question',
mixins: [TranslationMixin],
components: {
AsyncTranslationText,
QuestionTimer,
DateQuestion,
DecimalQuestion,
DontKnowRefused,
GeoQuestion,
IntegerQuestion,
IntroQuestion,
ImageQuestion,
MultipleSelectQuestion,
RelationshipQuestion,
RosterQuestion,
RespondentGeoQuestion,
TextQuestion,
TimeQuestion,
QuestionText,
},
props: {
question: {
type: Object,
required: true
},
} as PropOptions<Question>,
interview: {
type: Object,
required: true
},
} as PropOptions<Interview>,
location: {
type: Object,
required: true
},
} as PropOptions<InterviewLocation>,
disabled: {
type: Boolean,
required: true
Expand All @@ -105,13 +126,14 @@
hasChanged: false
}
},
update () {
updated () {
this.hasChanged = true
},
watch: {
'question': {
handler () {
this.hasChanged = true
this.translation = this.question.questionTranslation
},
deep: true
},
Expand All @@ -123,45 +145,28 @@
}
},
computed: {
currentQuestionComponent () {
currentQuestionComponent (): string {
return typeMap[this.question.questionTypeId]
},
validationError () {
validationError (): null | Error {
if (!this.hasChanged || (this.question.dkRf !== null && this.question.dkRf !== undefined)) {
return null
}
return this.question.validationError
},
timerDuration () {
timerDuration (): number {
if (!this.question || !this.question.questionParameters || !this.question.questionParameters.length) return 0
const qp = this.question.questionParameters.find(qp => qp.parameterId == ParameterType.allowed_time)
return qp ? +qp.val : 0
},
showTimer () {
showTimer (): boolean {
return this.timerDuration !== 0
},
showTimerControls () {
showTimerControls (): boolean {
if (!this.question || !this.question.questionParameters || !this.question.questionParameters.length) return true
const questionParameter = this.question.questionParameters.find(qp => qp.parameterId == ParameterType.show_timer_controls)
return questionParameter ? !!questionParameter.val : true
}
},
components: {
AsyncTranslationText,
QuestionTimer,
DateQuestion,
DecimalQuestion,
DontKnowRefused,
GeoQuestion,
IntegerQuestion,
IntroQuestion,
ImageQuestion,
MultipleSelectQuestion,
RelationshipQuestion,
RosterQuestion,
RespondentGeoQuestion,
TextQuestion,
TimeQuestion
}
}
</script>
Expand Down
Loading