Skip to content

Commit

Permalink
Merge pull request #43 from fkm/feature/custom-widgets
Browse files Browse the repository at this point in the history
Custom widgets for single and multiple choice questions
  • Loading branch information
czosel committed Mar 11, 2019
2 parents 1ebe855 + 2e26037 commit cade6ac
Show file tree
Hide file tree
Showing 19 changed files with 381 additions and 13 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Expand Up @@ -53,6 +53,7 @@ module.exports = {
require("eslint-plugin-node").configs.recommended.rules,
{
// add your custom rules and overrides for node files here
"prefer-const": ["error"]
}
)
}
Expand Down
4 changes: 4 additions & 0 deletions addon/components/cf-field/input.js
Expand Up @@ -25,7 +25,11 @@ export default Component.extend({
type: computed("field.question.__typename", function() {
const typename = get(this, "field.question.__typename");

const meta = get(this, "field.question.meta") || "{}";
const customtype = JSON.parse(meta).widgetType;

return (
customtype ||
(typename && mapping[typename]) ||
typename.replace(/Question$/, "").toLowerCase()
);
Expand Down
81 changes: 81 additions & 0 deletions addon/components/cf-field/input/powerselect.js
@@ -0,0 +1,81 @@
import Component from "@ember/component";
import { computed } from "@ember/object";
import { inject as service } from "@ember/service";
import layout from "../../../templates/components/cf-field/input/powerselect";

/**
* Dropdown component for the single and multiple choice question type
*
* @class CfFieldInputPowerSelectComponent
* @argument {Field} field The field for this input type
*/
export default Component.extend({
layout,
tagName: "",
intl: service(),

multiple: computed("field.question.__typename", function() {
return this.get("field.question.__typename").startsWith("Multiple");
}),

choices: computed(
"multiple",
"field.question.{choiceOptions,multipleChoiceOptions}.edges",
function() {
const options = this.get("multiple")
? this.get("field.question.multipleChoiceOptions")
: this.get("field.question.choiceOptions");

return options.edges.map(edge => edge.node);
}
),

selected: computed(
"field.answer.{_valueKey,listValue,stringValue}",
function() {
const key = this.get("field.answer._valueKey");
const answers = this.get(`field.answer.${key}`);

if (!answers) {
return null;
}

const selection = this.get("choices").filter(choice => {
return answers.includes(choice.slug);
});

return key === "stringValue" ? selection[0] : selection;
}
),

componentName: computed("multiple", function() {
return this.get("multiple") ? "power-select-multiple" : "power-select";
}),

searchEnabled: computed("choices", function() {
return this.get("choices").length > 10;
}),

placeholder: computed("multiple", function() {
const suffix = this.get("multiple") ? "multiple" : "single";
const path = `caluma.form.power-select.placeholder-${suffix}`;
return this.get("intl").t(path);
}),

actions: {
change: function(choices) {
let value = null;

if (Array.isArray(choices)) {
value = choices.map(choice => choice.slug);
} else if (choices !== null) {
value = choices.slug;
}
// ELSE will never be taken as long as we don't allow for empty
// selections in single choice fields. Empty selections must first be
// implemented/allowed by the backend.

this.onSave(value);
}
}
});
26 changes: 25 additions & 1 deletion addon/components/cfb-form-editor/question.js
Expand Up @@ -55,6 +55,19 @@ export default Component.extend(ComponentQueryManager, {
this.get("data").perform();
},

widgetTypes: computed(function() {
return {
ChoiceQuestion: [
{ value: "radio", label: "Radio buttons" },
{ value: "powerselect", label: "Dropdown" }
],
MultipleChoiceQuestion: [
{ value: "checkbox", label: "Checkboxes" },
{ value: "powerselect", label: "Dropdown" }
]
};
}),

data: task(function*() {
if (!this.get("slug")) {
return A([
Expand All @@ -77,14 +90,22 @@ export default Component.extend(ComponentQueryManager, {
]);
}

return yield this.get("apollo").watchQuery(
const questions = yield this.get("apollo").watchQuery(
{
query: formEditorQuestionQuery,
variables: { slug: this.get("slug") },
fetchPolicy: "cache-and-network"
},
"allQuestions.edges"
);

function setWidgetType(question) {
const meta = JSON.parse(question.node.meta);
question.node.widgetType = meta.widgetType;
return question;
}

return A(questions.map(setWidgetType));
}).restartable(),

model: computed("data.lastSuccessful.value.firstObject.node", function() {
Expand Down Expand Up @@ -169,6 +190,9 @@ export default Component.extend(ComponentQueryManager, {
slug: changeset.get("slug"),
isRequired: changeset.get("isRequired"),
isHidden: "false", // TODO: this must be configurable
meta: JSON.stringify({
widgetType: changeset.get("widgetType")
}),
isArchived: changeset.get("isArchived"),
clientMutationId: v4()
},
Expand Down
1 change: 1 addition & 0 deletions addon/gql/fragments/field-question.graphql
Expand Up @@ -3,6 +3,7 @@ fragment FieldQuestion on Question {
label
isRequired
isHidden
meta
... on TextQuestion {
textMaxLength: maxLength
}
Expand Down
1 change: 1 addition & 0 deletions addon/gql/fragments/question-info.graphql
Expand Up @@ -3,5 +3,6 @@ fragment QuestionInfo on Question {
label
isRequired
isHidden
meta
isArchived
}
20 changes: 20 additions & 0 deletions addon/templates/components/cf-field/input/powerselect.hbs
@@ -0,0 +1,20 @@
{{#component componentName
options=choices
selected=selected
disabled=disabled
allowClear=false
preventScroll=true
searchEnabled=searchEnabled
searchField="label"

placeholder=placeholder
loadingMessage=(t "caluma.form.power-select.options-loading")
searchMessage=(t "caluma.form.power-select.options-empty")
searchPlaceholder=(t "caluma.form.power-select.search-placeholder")
noMatchesMessage=(t "caluma.form.power-select.search-empty")

onchange=(action "change")
as |choice|
}}
{{choice.label}}
{{/component}}
12 changes: 12 additions & 0 deletions addon/templates/components/cfb-form-editor/question.hbs
Expand Up @@ -121,6 +121,18 @@
required=true
renderComponent=(component "cfb-form-editor/question/options")
}}

{{f.input
name="widgetType"
label=(t "caluma.form-builder.question.widgetType")
required=true
class="uk-flex uk-flex-between uk-flex-column"

type="select"
optionTargetPath="value"
optionLabelPath="label"
options=(get widgetTypes f.model.__typename)
}}
{{/if}}

{{#if (eq f.model.__typename "TableQuestion")}}
Expand Down
1 change: 1 addition & 0 deletions app/components/cf-field/input/powerselect.js
@@ -0,0 +1 @@
export { default } from "ember-caluma/components/cf-field/input/powerselect";
17 changes: 17 additions & 0 deletions app/styles/_cfb-powerselect.scss
@@ -0,0 +1,17 @@
$ember-power-select-focus-outline: 0;
$ember-power-select-border-color: #e5e5e5;
$ember-power-select-default-border-radius: 0;
$ember-power-select-line-height: 38px;

$ember-power-select-selected-background: #3799ad;
$ember-power-select-multiple-selection-color: #ffffff;
$ember-power-select-multiple-selection-background-color: $ember-power-select-selected-background;

$ember-power-select-multiple-option-padding: 0 8px;
$ember-power-select-multiple-option-line-height: 32px;
$ember-power-select-multiple-option-border-color: $ember-power-select-border-color;

$ember-power-select-highlighted-color: #ffffff;
$ember-power-select-highlighted-background: lighten($ember-power-select-selected-background, 10%);

@import "ember-power-select";
1 change: 1 addition & 0 deletions app/styles/ember-caluma.scss
Expand Up @@ -3,6 +3,7 @@
@import "cfb-form-editor/question-list/item";
@import "cfb-loading-dots";
@import "cfb-navigation";
@import "cfb-powerselect";

.cfb-pointer {
cursor: pointer;
Expand Down
2 changes: 1 addition & 1 deletion config/deploy.js
Expand Up @@ -2,7 +2,7 @@
"use strict";

module.exports = function() {
let ENV = {
const ENV = {
build: {},
git: {
repo: "git@github.com:projectcaluma/ember-caluma.git"
Expand Down
6 changes: 3 additions & 3 deletions ember-cli-build.js
Expand Up @@ -5,7 +5,7 @@ const funnel = require("broccoli-funnel");
const sass = require("sass");

module.exports = function(defaults) {
let app = new EmberAddon(defaults, {
const app = new EmberAddon(defaults, {
sassOptions: { implementation: sass },
snippetPaths: ["tests/dummy/app/snippets"],
babel: {
Expand All @@ -18,12 +18,12 @@ module.exports = function(defaults) {
app.import("node_modules/typeface-oxygen/index.css");
app.import("node_modules/typeface-oxygen-mono/index.css");

let oxygen = funnel("node_modules/typeface-oxygen/files", {
const oxygen = funnel("node_modules/typeface-oxygen/files", {
include: ["*.woff", "*.woff2"],
destDir: "/assets/files/"
});

let oxygenMono = funnel("node_modules/typeface-oxygen-mono/files", {
const oxygenMono = funnel("node_modules/typeface-oxygen-mono/files", {
include: ["*.woff", "*.woff2"],
destDir: "/assets/files/"
});
Expand Down
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -84,6 +84,7 @@
"ember-export-application-global": "2.0.0",
"ember-load-initializers": "2.0.0",
"ember-maybe-import-regenerator": "0.1.6",
"ember-power-select": "2.2.2",
"ember-qunit": "4.4.1",
"ember-resolver": "5.0.1",
"ember-source": "3.7.0",
Expand Down
2 changes: 1 addition & 1 deletion tests/dummy/config/environment.js
@@ -1,7 +1,7 @@
"use strict";

module.exports = function(environment) {
let ENV = {
const ENV = {
modulePrefix: "dummy",
environment,
rootURL: "/",
Expand Down

0 comments on commit cade6ac

Please sign in to comment.