LT-253: Creating select2 component

This commit is contained in:
Bruno Alves de Faria 2016-07-15 14:44:31 -03:00 committed by Erik Ziegler
parent 9d31b49997
commit 8564c52981
31 changed files with 123 additions and 492 deletions

View File

@ -100,7 +100,6 @@ promise@0.7.3
random@1.0.10
rate-limit@1.0.5
reactive-dict@1.1.8
reactive-form-controls@0.0.1
reactive-var@1.0.10
reload@1.1.10
retry@1.0.8

View File

@ -1,10 +0,0 @@
<template name="radioOption">
<label class="radio-option">
<input type="radio"
name="{{this.name}}"
value="{{this.value}}"
class="{{this.class}}"
checked="{{#if this.checked}}checked{{/if}}"/>
<span>{{this.label}}</span>
</label>
</template>

View File

@ -1,35 +0,0 @@
@import "{design}/app"
label.radio-option
cursor: pointer
input
height: 0
width: 0
span
padding-left: 23px
position: relative
&:before
background: white
border-radius: 8px
content: ''
display: block
height: 16px
left: 0
position: absolute
top: 50%
transform(translateY(-50%))
width: 16px
input:focus + span:before
// TODO: [design] define a outline for the design
outline: none
box-shadow: 0 0 2px 2px $textSecondaryColor
input:checked + span:after
background: $uiBorderColorDark
border-radius: 5px
content: ''
height: 10px
left: 3px
position: absolute
top: 50%
transform(translateY(-50%))
width: 10px

View File

@ -41,10 +41,4 @@ Package.onUse(function(api) {
'components/roundedButtonGroup/roundedButtonGroup.js'
], 'client');
// Radio Option
api.addFiles([
'components/radioOption/radioOption.html',
'components/radioOption/radioOption.styl'
], 'client');
});

View File

@ -1,9 +1,5 @@
import { SimpleSchema } from 'meteor/aldeed:simple-schema';
SimpleSchema.extendOptions({
allowedSelect2Values: Match.Optional(Array),
});
export const schema = new SimpleSchema({
measurableDisease: {
type: String,
@ -16,7 +12,7 @@ export const schema = new SimpleSchema({
regionsOfMetastaticDisease: {
type: [String],
label: 'Regions of Metastatic Disease',
allowedSelect2Values: ['None', 'Lymph Node'], // allowedValues doesn't seem show up in the Schema since this is an Array. There may be a better way to do this
allowedValues: ['None', 'Lymph Node'],
defaultValue: ['None'],
optional: true
},

View File

@ -1,19 +1,23 @@
<template name="additionalFindings">
<div class="additionalFindings">
<div class="scrollArea">
{{#form schema=currentSchema}}
{{#form schema=instance.currentSchema}}
<div class="row">
<div class="header">Additional Findings: Baseline</div>
{{>groupRadio labelClass='form-group' key='measurableDisease'}}
</div>
<div class="row">
<div class="header">Supplementary Measurements</div>
{{>selectInput (stateDataWithKey "numberOfBoneLesions")}}
{{>select2Input
key="regionsOfMetastaticDisease"
select2Options=regionsOfMetastaticDiseaseSelect2Options
state=state
currentSchema=currentSchema
{{>inputSelect class="bone-lesions" labelClass='form-group' key='numberOfBoneLesions' hideSearch=true}}
{{>inputSelect
class="disease-regions"
labelClass='form-group'
key='regionsOfMetastaticDisease'
multiple=true
options=(clone
placeholder='Search Regions'
allowClear=true
)
}}
{{>groupRadio labelClass='form-group' key='tracerRelatedToMetastaticDisease'}}
</div>

View File

@ -2,79 +2,58 @@ import { Template } from 'meteor/templating';
import { ReactiveDict } from 'meteor/reactive-dict';
import { schema as AdditionalFindingsSchema } from 'meteor/lesiontracker/both/schema/additionalFindings';
Template.additionalFindings.onCreated(function additionalFindingsOnCreated() {
console.log('additionalFindingsOnCreated');
Template.additionalFindings.onCreated(() => {
const instance = Template.instance();
instance.currentSchema = AdditionalFindingsSchema;
instance.state = new ReactiveDict();
});
if (!instance.data.currentTimepointId) {
Template.additionalFindings.onRendered(() => {
const instance = Template.instance();
// Get the form component
const form = instance.$('form:first').data('component');
const currentTimepointId = instance.data.currentTimepointId;
if (!currentTimepointId) {
console.warn('Case has no timepointId');
return;
}
const currentTimepointId = instance.data.currentTimepointId;
if (!currentTimepointId) {
console.warn('No currentTimepointId');
}
console.log(AdditionalFindings.find().fetch());
const additionalFindings = AdditionalFindings.findOne({
timepointId: currentTimepointId
});
if (additionalFindings) {
instance.id = additionalFindings._id;
// Don't store the MongoDB Id in the ReactiveDict
delete additionalFindings._id;
instance.state.set(additionalFindings);
form.value(additionalFindings);
} else {
const defaultData = instance.currentSchema.clean({});
instance.state.set(defaultData);
form.value(defaultData);
// Include patientId and timepointId
defaultData.patientId = Session.get('patientId');
defaultData.timepointId = currentTimepointId;
// TODO: Turn this into a Meteor Call to insert it on the server
// TODO: [design] Turn this into a Meteor Call to insert it on the server
instance.id = AdditionalFindings.insert(defaultData);
}
});
Template.additionalFindings.onRendered(function additionalFindingsOnRendered() {
const instance = Template.instance();
instance.autorun(computation => {
// Run this computation everytime the form data is changed
form.depend();
instance.autorun(() => {
console.log('Updating AdditionalFindings Collection');
console.log(instance.state.all());
// Stop here if it's the computation's first run
if (computation.firstRun) {
return;
}
// TODO: Turn this into a Meteor Call to update it on the server
let update = instance.state.all();
// Get the form data for AdditionalFindings
let formData = form.value();
// TODO: [design] Turn this into a Meteor Call to update it on the server
AdditionalFindings.update(instance.id, {
$set: update
$set: formData
});
});
});
Template.additionalFindings.helpers({
// We need these helper methods
// since we assign them into instance object instead of instance.data
currentSchema() {
return Template.instance().currentSchema;
},
state() {
return Template.instance().state;
},
regionsOfMetastaticDiseaseSelect2Options() {
return {
placeholder: 'Search Regions',
allowClear: true,
multiple: true
};
}
});

View File

@ -32,8 +32,9 @@
background-color: $uiGray
.form-group
padding: 7px 11px
color: $textPrimaryColor
padding: 7px 11px
width: 100%
&:not(:last-child)
border-bottom: $uiBorderThickness solid $uiBorderColorDark
@ -44,19 +45,13 @@
line-height: 15px
margin: 10px 0 8px
// TODO: remove
h5
font-weight: bold
margin-bottom: 8px
.control-label
font-size: 14px
font-weight: 100
display: inline-block
padding: 5px
// TODO: remove radio-group
.radio-group, .group-radio
.group-radio
height: 30px
label
@ -68,13 +63,11 @@
line-height: 30px
width: 50%
#regionsOfMetastaticDisease
width: 285px
height: 30px
border-radius: 2px
background-color: #b6b6b6
color: #3e3e3e
font-size: 14px
.disease-regions + .select2
width: 100% !important
.bone-lesions + .select2
width: auto !important
.buttonSection
cursor: pointer

View File

@ -28,9 +28,9 @@ Package.onUse(function(api) {
api.use('aldeed:template-extension@4.0.0');
// Our custom packages
api.use('ohif:core');
api.use('worklist');
api.use('cornerstone');
api.use('reactive-form-controls');
api.addFiles('log.js', [ 'client', 'server' ]);

View File

@ -1,5 +1,6 @@
import { OHIF } from 'meteor/ohif:core';
import { Template } from 'meteor/templating';
import { Tracker } from "meteor/tracker";
import { _ } from 'meteor/underscore';
import { $ } from 'meteor/jquery';
@ -15,6 +16,9 @@ OHIF.mixins.formItem = new OHIF.Mixin({
const instance = Template.instance();
const component = instance.component;
// Create a observer to monitor changed values
component.changeObserver = new Tracker.Dependency();
// Register the component in the parent component
component.registerSelf();
@ -106,6 +110,10 @@ OHIF.mixins.formItem = new OHIF.Mixin({
return true;
};
component.depend = () => {
return component.changeObserver.depend();
};
},
onRendered() {
@ -115,9 +123,6 @@ OHIF.mixins.formItem = new OHIF.Mixin({
// Set the element to be controlled
component.$element = instance.$(':input').first();
// Set the element to be styled
component.$style = component.$element;
// Set the most outer wrapper element
component.$wrapper = instance.wrapper.$('*').first();
},
@ -133,12 +138,22 @@ OHIF.mixins.formItem = new OHIF.Mixin({
const instance = Template.instance();
const component = instance.component;
// If no style element was defined, set it as the element itself
if (!component.$style.length) {
component.$style = component.$element;
}
// Set the component in jQuery data after all mixins are rendered
component.$element.data('component', component);
},
events: {
// Enable reactivity by changing a Tracker.Dependency observer
change(event, instance) {
instance.component.changeObserver.changed();
},
// TODO: [design] remove log, show error box/hint over the wrapper
errorin(event, instance) {
console.log('ERROR when validating component', instance.component);

View File

@ -1,6 +1,7 @@
import { OHIF } from 'meteor/ohif:core';
import { Template } from 'meteor/templating';
import { _ } from 'meteor/underscore';
import { $ } from 'meteor/jquery';
/*
* groupRadio: controls all the radio inputs inside the group

View File

@ -1,6 +1,7 @@
import { OHIF } from 'meteor/ohif:core';
import { Blaze } from 'meteor/blaze';
import { Template } from 'meteor/templating';
import { ReactiveVar } from 'meteor/reactive-var';
import { _ } from 'meteor/underscore';
// Helper function to get the component's current schema
@ -14,7 +15,12 @@ const getCurrentSchema = (parentComponent, key) => {
}
// Get the current schema data using component's key
const currentSchema = schema._schema[key];
const currentSchema = _.clone(schema._schema[key]);
// Merge the sub-schema properties if it's an array
if (Array.isArray(currentSchema.type())) {
_.extend(currentSchema, schema._schema[key + '.$']);
}
// Return the component's schema definitions
return currentSchema;
@ -50,7 +56,7 @@ OHIF.mixins.schemaData = new OHIF.Mixin({
// Fill the items if it's an array schema
if (!data.items && Array.isArray(currentSchema.allowedValues)) {
// Initialize the items array
data.items = [];
const items = [];
// Get the values and labels arrays from schema
const values = currentSchema.allowedValues;
@ -59,11 +65,14 @@ OHIF.mixins.schemaData = new OHIF.Mixin({
// Iterate the allowed values array
for (let i = 0; i < values.length; i++) {
// Push the current item to the items array
data.items.push({
items.push({
value: values[i],
label: labels[i] || values[i]
});
}
// Add the items to a reactive instance
data.items = new ReactiveVar(items);
}
},

View File

@ -1,5 +1,6 @@
import { OHIF } from 'meteor/ohif:core';
import { Template } from 'meteor/templating';
import { $ } from 'meteor/jquery';
/*
* input: controls a select2 component
@ -11,8 +12,27 @@ OHIF.mixins.select2 = new OHIF.Mixin({
const instance = Template.instance();
const component = instance.component;
// Set the element to be controlled
component.$element = instance.$('select:first');
// Apply the select2 to the component
component.$element.select2(instance.data.options);
// Store the select2 instance to allow its further destruction
component.select2Instance = component.$element.data('select2');
},
onDestroyed() {
const instance = Template.instance();
const component = instance.component;
// Destroy the select2 instance to remove unwanted DOM elements
component.select2Instance.destroy();
},
events: {
'focus .select2-hidden-accessible'(event, instance) {
// Redirect the focus to select2 focus control in case of hidden
// accessible being focused (e.g. clicking on outer label)
$(event.currentTarget).nextAll('.select2:first').find('.select2-selection').focus();
}
}
}
});

View File

@ -3,6 +3,7 @@
id="{{this.id}}"
class="{{this.class}}"
name="{{this.name}}"
multiple="{{#if this.multiple}}multiple{{/if}}"
{{this.tagAttributes}}
>
{{>UI.contentBlock}}

View File

@ -4,7 +4,7 @@
type='radio'
mixins=(concat 'input ' this.mixins)
labelAfter=(valueIf (isDefined this.labelAfter) this.labelAfter true)
labelClass=(concat 'checkboxLabel' this.labelClass)
labelClass=(concat 'checkboxLabel ' this.labelClass)
wrappers=(concat ''
(valueIf reactive this.label 'wrapperLabel ' '')
this.wrappers

View File

@ -1,12 +1,17 @@
<template name="inputSelect">
{{#baseComponent (extend this
base='baseSelect'
class=(concat 'form-control ' this.class)
mixins=(concat 'select ' this.mixins)
mixins=(concat 'select2 ' this.mixins)
wrappers=(concat ''
(valueIf reactive this.label 'wrapperLabel ' '')
this.wrappers
)
options=(clone this.options
multiple=(valueIf this.multiple true false)
minimumResultsForSearch=(valueIf this.hideSearch
-1 this.minimumResultsForSearch
)
)
)}}
{{>UI.contentBlock}}
{{/baseComponent}}

View File

@ -46,14 +46,15 @@ Template.registerHelper('clone', (...argsArray) => {
return extend(...newArgs);
});
// Return the first thrut value in the given arguments
Template.registerHelper('choose', (...argsArray) => {
// Iterate over the given objects
for (let i = 0; i < argsArray.length; i++) {
// Check if the current value is truth
if (!!argsArray[i]) {
// Return the current value
return argsArray[i];
// Choose the first truthy value in the given values
Template.registerHelper('choose', (...values) => {
let result;
_.each(_.initial(values, 1), value => {
if (result) {
return;
}
}
result = value;
});
return result;
});

View File

@ -59,19 +59,6 @@ Template.registerHelper('or', (...values) => {
return result;
});
// Choose the first truthy value in the given values
Template.registerHelper('choose', (...values) => {
let result;
_.each(_.initial(values, 1), value => {
if (result) {
return;
} else if (value) {
result = value;
}
});
return result;
});
// Return the second parameter if the first is true or the third if it's false
Template.registerHelper('valueIf', (condition, valueIfTrue, valueIfFalse) => {
if (condition) {

View File

@ -10,6 +10,11 @@ Package.onUse(function(api) {
api.use('ecmascript');
api.use('standard-app-packages');
api.use('jquery');
api.use('underscore');
api.use('templating');
api.use('reactive-var');
api.use('natestrauser:select2@4.0.1', 'client');
api.mainModule('main.js', 'client');
});

View File

@ -1,7 +0,0 @@
<template name="helpBlock">
{{#if isInvalidKey key}}
<span class="help-block m-b-none">
{{invalidKeyMessage key}}
</span>
{{/if}}
</template>

View File

@ -1,10 +0,0 @@
import { Template } from 'meteor/templating';
import './helpBlock.html';
Template.helpBlock.onCreated(function helpBlockOnCreated() {
var instance = this;
// Invalid keys comes from Trials schema
instance.invalidKeys = this.data.invalidKeys;
});

View File

@ -1,18 +0,0 @@
<template name="radioOptionGroup">
<div class="{{class}} form-group">
{{#let schema=(getSchema key)}}
<h5>{{schema.label}}</h5>
<div class="radio-group">
{{#each option in schema.allowedValues}}
{{>radioOption
name=key
value=option
checked=(eq value option)
label=option
class="js-form-update"
}}
{{/each}}
</div>
{{/let}}
</div>
</template>

View File

@ -1,40 +0,0 @@
import { Template } from 'meteor/templating';
import './radioOptionGroup.html';
Template.radioOptionGroup.onCreated(function radioOptionGroupOnCreated() {
let instance = this;
instance.currentSchema = this.data.currentSchema;
// Here we set this Template instance's state property to equal the input state data
// The purpose of this is to make our helpers and events more consistent across templates.
instance.state = this.data.state;
// Invalid keys comes from Trials schema
instance.invalidKeys = this.data.invalidKeys;
// Validation Context of registrationWorkflow
instance.validationContext = this.data.validationContext;
// isModified records whether or not the user has unsaved changes
instance.isModified = this.data.isModified;
});
Template.radioOptionGroup.events({
'change .js-form-update'(event, instance) {
const value = event.currentTarget.value;
const key = instance.data.key;
if (event.currentTarget.checked) {
instance.state.set(key, value);
}
}
});
Template.radioOptionGroup.helpers({
value() {
const instance = Template.instance();
const key = instance.data.key;
return instance.state.get(key);
}
});

View File

@ -1,22 +0,0 @@
<template name="select2Input">
<div class="form-group {{#if isInvalidKey key}}has-error{{/if}}">
<label class="control-label">
{{#with getSchema key }}
{{label}}
{{/with }}
</label>
<div>
<select id="{{key}}"
name="{{key}}"
required="{{isRequired}}"
class="form-control js-form-update">
{{#each option in options}}
<option value="{{option.id}}">
{{option.text}}
</option>
{{/each}}
</select>
{{> helpBlock}}
</div>
</div>
</template>

View File

@ -1,80 +0,0 @@
import { Template } from 'meteor/templating';
import './select2Input.html';
Template.select2Input.onCreated(function selectInputOnCreated() {
let instance = this;
instance.currentSchema = this.data.currentSchema;
// Here we set this Template instance's state property to equal the input state data
// The purpose of this is to make our helpers and events more consistent across templates.
instance.state = this.data.state;
// Invalid keys comes from Trials schema
instance.invalidKeys = this.data.invalidKeys;
// Validation Context of registrationWorkflow
instance.validationContext = this.data.validationContext;
// isModified records whether or not the user has unsaved changes
instance.isModified = this.data.isModified;
});
Template.select2Input.onRendered(function select2InputOnRendered() {
const instance = this;
Meteor.defer(function() {
// Initialize select2 box
const selectBox = instance.$('select');
selectBox.select2(instance.data.select2Options);
// Set selected value(s) from current state;
const key = instance.data.key;
const value = instance.state.get(key);
if (value) {
selectBox.val(value).trigger('change');
}
});
});
Template.select2Input.helpers({
options() {
const instance = Template.instance();
if (instance.data.options) {
return instance.data.options;
}
const key = instance.data.key;
const schema = instance.currentSchema.schema(key);
const allowedValues = schema.allowedSelect2Values;
return allowedValues.map((value) => {
return {
id: value,
text: value
};
});
}
});
Template.select2Input.events({
'focus .select2'(event, instance) {
var control = event.currentTarget;
// Find select element that is assigned to select2
var hiddenSelect2Box = $(control).prev('select');
// Prevent auto open for select2 boxes that has multiple attribute
if (!hiddenSelect2Box || $(hiddenSelect2Box).prop('multiple')) {
return;
}
$(hiddenSelect2Box).select2('open');
},
'change .js-form-update'(event, instance) {
const value = $(event.currentTarget).val();
const key = instance.data.key;
instance.state.set(key, value);
}
});

View File

@ -1,23 +0,0 @@
<template name="selectInput">
<div class="form-group {{#if isInvalidKey key}}has-error{{/if}}">
{{ #with getSchema key }}
<label class="control-label">
{{label}}
</label>
<div>
<select id="{{../key}}"
name="{{../key}}"
required="{{isRequired}}"
class="form-control js-form-update">
{{#each option in allowedValues}}
<option value="{{option}}"
selected="{{#if eq selectedOptionValue option}}selected{{/if}}">
{{option}}
</option>
{{/each}}
</select>
{{> helpBlock }}
</div>
{{ /with }}
</div>
</template>

View File

@ -1,38 +0,0 @@
import { Template } from 'meteor/templating';
import './selectInput.html';
Template.selectInput.onCreated(function selectInputOnCreated() {
let instance = this;
instance.currentSchema = this.data.currentSchema;
// Here we set this Template instance's state property to equal the input state data
// The purpose of this is to make our helpers and events more consistent across templates.
instance.state = this.data.state;
// Invalid keys comes from Trials schema
instance.invalidKeys = this.data.invalidKeys;
// Validation Context of registrationWorkflow
instance.validationContext = this.data.validationContext;
// isModified records whether or not the user has unsaved changes
instance.isModified = this.data.isModified;
});
Template.selectInput.helpers({
selectedOptionValue() {
const instance = Template.instance();
var key = instance.data.key;
return instance.state.get(key);
}
});
Template.selectInput.events({
'change .js-form-update'(event, instance) {
const value = $(event.currentTarget).val();
const key = instance.data.key;
instance.state.set(key, value);
}
});

View File

@ -1,14 +0,0 @@
import { Template } from 'meteor/templating';
Template.registerHelper('getSchema', function(context) {
const instance = Template.instance();
if (!instance.currentSchema) {
return;
}
if (!context) {
return;
}
return instance.currentSchema.schema(context);
});

View File

@ -1,19 +0,0 @@
import { Template } from 'meteor/templating';
/**
* A global Blaze UI helper function to return whether or not a key is part of the invalidKeys dictionary
*/
Template.registerHelper('isInvalidKey', function(context, format, options) {
if (!context) {
return;
}
const instance = Template.instance();
if (!instance.invalidKeys) {
return;
}
// Show error message if key is at first index
var firstInvalidKey = instance.invalidKeys.get(0);
return (firstInvalidKey && firstInvalidKey.name === context)
});

View File

@ -1,28 +0,0 @@
import { Template } from 'meteor/templating';
/**
* A global Blaze UI helper function to return whether or not a
* field is required based on the Template's specified currentSchema
*/
Template.registerHelper('stateDataWithKey', function(context) {
const instance = Template.instance();
if (!context) {
return {
state: instance.state,
invalidKeys: instance.invalidKeys,
validationContext: instance.validationContext,
currentSchema: instance.currentSchema,
isModified: instance.isModified
};
}
return {
key: context,
state: instance.state,
invalidKeys: instance.invalidKeys,
validationContext: instance.validationContext,
currentSchema: instance.currentSchema,
isModified: instance.isModified
};
});

View File

@ -1,34 +0,0 @@
Package.describe({
name: 'reactive-form-controls',
summary: 'A set of basic form controls that store their state in a ReactiveDict',
version: '0.0.1'
});
Package.onUse(function(api) {
api.versionsFrom('1.3.4.1');
api.use('standard-app-packages');
api.use('ecmascript');
api.use('jquery');
api.use('stylus');
api.use('reactive-dict');
api.use('templating');
api.use('natestrauser:select2@4.0.1', 'client');
api.addFiles('client/helpers/getSchema.js', ['client']);
api.addFiles('client/helpers/isInvalidKey.js', ['client']);
api.addFiles('client/helpers/stateDataWithKey.js', ['client']);
api.addFiles('client/components/helpBlock/helpBlock.html', ['client']);
api.addFiles('client/components/helpBlock/helpBlock.js', ['client']);
api.addFiles('client/components/radioOptionGroup/radioOptionGroup.html', ['client']);
api.addFiles('client/components/radioOptionGroup/radioOptionGroup.js', ['client']);
api.addFiles('client/components/selectInput/selectInput.html', ['client']);
api.addFiles('client/components/selectInput/selectInput.js', ['client']);
api.addFiles('client/components/select2Input/select2Input.html', ['client']);
api.addFiles('client/components/select2Input/select2Input.js', ['client']);
});