@legumeinfo/web-components
Version:
Web Components for the Legume Information System and other AgBio databases
637 lines • 25.1 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { LitElement, css, html } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { live } from 'lit/directives/live.js';
import { createRef, ref } from 'lit/directives/ref.js';
import { LisCancelPromiseController } from './controllers';
import { LisPaginatedSearchMixin, } from './mixins';
/**
* @htmlElement `<lis-pangene-lookup-element>`
*
* A Web Component that provides an interface for looking up pangenes and
* displaying results in a view table. The component uses the
* {@link mixins!LisPaginatedSearchMixin | `LisPaginatedSearchMixin`} mixin. See the
* mixin docs for further details.
*
* @example
* {@link !HTMLElement | `HTMLElement`} properties can only be set via
* JavaScript. This means the {@link searchFunction | `searchFunction`} property
* must be set on a `<lis-pangene-lookup-element>` tag's instance of the
* {@link LisPangeneLookupElement | `LisPangeneLookupElement`} class. For example:
* ```html
* <!-- add the Web Component to your HTML -->
* <lis-pangene-lookup-element id="pangene-lookup"></lis-pangene-lookup-element>
*
* <!-- configure the Web Component via JavaScript -->
* <script type="text/javascript">
* // a site-specific function that sends a request to a pangene lookup API
* function getPangenes(lookupData, page, {abortSignal}) {
* // returns a Promise that resolves to a lookup result object
* }
* // get the pangene lookup element
* const lookupElement = document.getElementById('pangene-lookup');
* // set the element's searchFunction property
* lookupElement.searchFunction = getPangenes;
* </script>
* ```
*
* @example
* Data must be provided for the genus, species, strain, assembly, and annotation selectors in the
* lookup form. This can be done by setting the form's {@link formData | `formData`}
* attribute/property directly or by setting the
* {@link formDataFunction | `formDataFunction`} property. Setting the latter will call
* the function immediately and set the {@link formData | `formData`} value using the
* result. For example:
* ```html
* <!-- add the Web Component to your HTML -->
* <lis-pangene-lookup-element id="pangene-lookup"></lis-pangene-lookup-element>
*
* <!-- configure the Web Component via JavaScript -->
* <script type="text/javascript">
* // a site-specific function that gets genus, species, strain, assembly, and annotation data from an API
* function getPangeneFormData() {
* // returns a Promise that resolves to a form data object
* }
* // get the pangene looktup element
* const lookupElement = document.getElementById('pangene-lookup');
* // set the element's formDataFunction property
* lookupElement.formDataFunction = getPangeneFormData;
* </script>
* ```
*
* @example
* The {@link genus | `genus`}, {@link species | `species`}, {@link strain | `strain`},
* {@link assembly | `assembly`}, and {@link annotation | `annotation`} properties can be used to
* limit all lookups to a specific genus, species, strain, assembly, and annotation. This will cause
* the genus, species, strain, assembly, and annotation fields of the lookup form to be
* automatically set and disabled so that users cannot change them.
* For example:
* ```html
* <!-- restrict the genus via HTML -->
* <lis-pangene-lookup-element genus="Glycine"></lis-pangene-lookup-element>
*
* <!-- restrict the genus and species via HTML -->
* <lis-pangene-lookup-element genus="Glycine" species="max"></lis-pangene-lookup-element>
*
* <!-- restrict the genus and species via JavaScript -->
* <lis-pangene-lookup-element id="pangene-lookup"></lis-pangene-lookup-element>
*
* <script type="text/javascript">
* // get the pangene lookup element
* const lookupElement = document.getElementById('pangene-lookup');
* // set the element's genus and species properties
* lookupElement.genus = "Cicer";
* lookupElement.species = "arietinum";
* </script>
* ```
*
* @example
* The {@link genesExample | `genesExample`} property can be used to set the example text for the
* gene identifiers input field. For example:
* ```html
* <!-- set the example text via HTML -->
* <lis-pangene-lookup-element genesExample="Glyma.13G357700 Glyma.13G357702"></lis-pangene-lookup-element>
*
* <!-- set the example text via JavaScript -->
* <lis-pangene-lookup-element id="pangene-lookup"></lis-pangene-lookup-element>
*
* <script type="text/javascript">
* // get the pangene lookup element
* const lookupElement = document.getElementById('pangene-lookup');
* // set the element's example text properties
* lookupElement.genesExample = 'Glyma.13G357700 Glyma.13G357702';
* </script>
* ```
*/
let LisPangeneLookupElement = class LisPangeneLookupElement extends LisPaginatedSearchMixin(LitElement)() {
_splitGenesFunctionWrapper(fn) {
return (data, options) => {
// @ts-expect-error Property 'trim' does not exist on type 'string[]'
const genes = data['genes'].trim().split(this.genesRegexp);
const modifiedData = { ...data, genes };
return fn(modifiedData, options);
};
}
willUpdate(changedProperties) {
if (changedProperties.has('searchFunction')) {
// @ts-expect-error incompatible types
this.searchFunction = this._splitGenesFunctionWrapper(this.searchFunction);
}
if (changedProperties.has('downloadFunction') &&
this.downloadFunction !== undefined) {
// @ts-expect-error incompatible types
this.downloadFunction = this._splitGenesFunctionWrapper(this.downloadFunction);
}
}
constructor() {
super();
/**
* The data used to construct the lookup form in the template.
*
* @attribute
*/
this.formData = { genuses: [] };
/**
* An optional property that can be used to load the form data via an external function.
* If used, the `formData` attribute/property will be updated using the result.
*/
this.formDataFunction = () => Promise.reject(new Error('No form data function provided'));
/**
* What regular experssion should be used to parse the input gene identifiers.
*
* @attribute
*/
this.genesRegexp = /\s+/;
/**
* The maximum number of input gene identifiers.
* Warning: setting this number too high can cause queries to hit web browsers' URL size limit.
*
* @attribute
*/
this.genesLimit = 100;
// the selected index of the genus select element
this.selectedGenus = 0;
// the selected index of the species select element
this.selectedSpecies = 0;
// the selected index of the strain select element
this.selectedStrain = 0;
// the selected index of the assembly select element
this.selectedAssembly = 0;
// the selected index of the annotation select element
this.selectedAnnotation = 0;
// a controller that allows in-flight form data requests to be cancelled
this.formDataCancelPromiseController = new LisCancelPromiseController(this);
// bind to the loading element in the template
this._formLoadingRef = createRef();
this.queryStringReflection = false;
this.resultAttributes = ['input', 'panGeneSet', 'target'];
this.tableHeader = {
input: 'Input',
panGeneSet: 'PanGene Set',
target: 'Target',
};
this.tableColumnClasses = {
description: 'uk-table-expand',
};
}
// called after every component update, e.g. when a property changes
updated(changedProperties) {
// call the formDataFunction every time its value changes
if (changedProperties.has('formDataFunction')) {
this._getFormData();
}
// use querystring parameters to update the selectors when the form data changes
if (changedProperties.has('formData') ||
changedProperties.has('genus') ||
changedProperties.has('species') ||
changedProperties.has('strain') ||
changedProperties.has('assembly') ||
changedProperties.has('annotation')) {
this._initializeSelections();
}
}
// gets the data for the lookup form
_getFormData() {
var _a;
// update the loading element
(_a = this._formLoadingRef.value) === null || _a === void 0 ? void 0 : _a.loading();
// make the form data function cancellable
this.formDataCancelPromiseController.cancel();
const options = {
abortSignal: this.formDataCancelPromiseController.abortSignal,
};
const formDataPromise = this.formDataFunction(options);
// call the cancellable function
this.formDataCancelPromiseController.wrapPromise(formDataPromise).then((formData) => {
var _a;
(_a = this._formLoadingRef.value) === null || _a === void 0 ? void 0 : _a.success();
this.formData = formData;
}, (error) => {
var _a;
// do nothing if the request was aborted
if (!(error instanceof Event && error.type === 'abort')) {
(_a = this._formLoadingRef.value) === null || _a === void 0 ? void 0 : _a.failure();
throw error;
}
});
}
// sets the selected indexes based on properties and querystring parameters
async _initializeSelections() {
this.selectedGenus = 0;
this.selectedSpecies = 0;
this.selectedStrain = 0;
this.selectedAssembly = 0;
this.selectedAnnotation = 0;
}
// called when a genus is selected
_selectGenus(event) {
if (event.target != null) {
this.selectedGenus = event.target.selectedIndex;
this.selectedSpecies = 0;
this.selectedStrain = 0;
this.selectedAssembly = 0;
this.selectedAnnotation = 0;
}
}
// renders the genus selector
_renderGenusSelector() {
const options = this.formData.genuses.map(({ genus }) => {
return html `<option value="${genus}">${genus}</option>`;
});
// HACK: the disabled attribute can't be set via template literal...
if (this.genus !== undefined) {
const value = this.selectedGenus
? this.formData.genuses[this.selectedGenus - 1].genus
: '';
return html `
<select
required
class="uk-select uk-form-small"
disabled
.selectedIndex=${live(this.selectedGenus)}
="${this._selectGenus}"
>
<option value="">-- select one --</option>
${options}
</select>
<input type="hidden" name="genus" value="${value}" />
`;
}
return html `
<select
required
class="uk-select uk-form-small"
name="genus"
.selectedIndex=${live(this.selectedGenus)}
="${this._selectGenus}"
>
<option value="">-- select one --</option>
${options}
</select>
`;
}
// called when a species is selected
_selectSpecies(event) {
if (event.target != null) {
this.selectedSpecies = event.target.selectedIndex;
this.selectedStrain = 0;
this.selectedAssembly = 0;
this.selectedAnnotation = 0;
}
}
// renders the species selector
_renderSpeciesSelector() {
let options = [html ``];
if (this.selectedGenus) {
options = this.formData.genuses[this.selectedGenus - 1].species.map(({ species }) => {
return html `<option value="${species}">${species}</option>`;
});
}
// HACK: the disabled attribute can't be set via template literal...
if (this.genus !== undefined && this.species !== undefined) {
const value = this.selectedGenus && this.selectedSpecies
? this.formData.genuses[this.selectedGenus - 1].species[this.selectedSpecies - 1].species
: '';
return html `
<select
class="uk-select uk-form-small"
disabled
.selectedIndex=${live(this.selectedSpecies)}
="${this._selectSpecies}"
>
<option value="">-- any --</option>
${options}
</select>
<input type="hidden" name="species" value="${value}" />
`;
}
return html `
<select
class="uk-select uk-form-small"
name="species"
.selectedIndex=${live(this.selectedSpecies)}
="${this._selectSpecies}"
>
<option value="">-- any --</option>
${options}
</select>
`;
}
// called when an strain is selected
_selectStrain(event) {
if (event.target != null) {
this.selectedStrain = event.target.selectedIndex;
}
}
// renders the strain selector
_renderStrainSelector() {
let options = [html ``];
if (this.selectedGenus && this.selectedSpecies) {
options = this.formData.genuses[this.selectedGenus - 1].species[this.selectedSpecies - 1].strains.map(({ strain }) => {
return html `<option value="${strain}">${strain}</option>`;
});
}
// HACK: the disabled attribute can't be set via template literal...
if (this.genus !== undefined &&
this.species !== undefined &&
this.strain !== undefined) {
const value = this.selectedGenus && this.selectedSpecies && this.selectedStrain
? this.formData.genuses[this.selectedGenus - 1].species[this.selectedSpecies - 1].strains
: '';
return html `
<select
class="uk-select uk-form-small"
disabled
.selectedIndex=${live(this.selectedStrain)}
="${this._selectStrain}"
>
<option value="">-- any --</option>
${options}
</select>
<input type="hidden" name="strain" value="${value}" />
`;
}
return html `
<select
class="uk-select uk-form-small"
name="strain"
.selectedIndex=${live(this.selectedStrain)}
="${this._selectStrain}"
>
<option value="">-- any --</option>
${options}
</select>
`;
}
// called when an assembly is selected
_selectAssembly(event) {
if (event.target != null) {
this.selectedAssembly = event.target.selectedIndex;
this.selectedAnnotation = 0;
}
}
// renders the assembly selector
_renderAssemblySelector() {
let options = [html ``];
if (this.selectedGenus && this.selectedSpecies && this.selectedStrain) {
options = this.formData.genuses[this.selectedGenus - 1].species[this.selectedSpecies - 1].strains[this.selectedStrain - 1].assemblies.map(({ assembly }) => {
return html `<option value="${assembly}">${assembly}</option>`;
});
}
// HACK: the disabled attribute can't be set via template literal...
if (this.genus !== undefined &&
this.species !== undefined &&
this.strain !== undefined &&
this.assembly !== undefined) {
const value = this.selectedGenus &&
this.selectedSpecies &&
this.selectedStrain &&
this.selectedAssembly
? this.formData.genuses[this.selectedGenus - 1].species[this.selectedSpecies - 1].strains[this.selectedStrain - 1].assemblies
: '';
return html `
<select
class="uk-select uk-form-small"
disabled
.selectedIndex=${live(this.selectedAssembly)}
="${this._selectAssembly}"
>
<option value="">-- any --</option>
${options}
</select>
<input type="hidden" name="assembly" value="${value}" />
`;
}
return html `
<select
class="uk-select uk-form-small"
name="assembly"
.selectedIndex=${live(this.selectedAssembly)}
="${this._selectAssembly}"
>
<option value="">-- any --</option>
${options}
</select>
`;
}
// called when an annotation is selected
_selectAnnotation(event) {
if (event.target != null) {
this.selectedAnnotation = event.target.selectedIndex;
}
}
// renders the annotation selector
_renderAnnotationSelector() {
let options = [html ``];
if (this.selectedGenus &&
this.selectedSpecies &&
this.selectedStrain &&
this.selectedAssembly) {
options = this.formData.genuses[this.selectedGenus - 1].species[this.selectedSpecies - 1].strains[this.selectedStrain - 1].assemblies[this.selectedAssembly - 1].annotations.map(({ annotation }) => {
return html `<option value="${annotation}">${annotation}</option>`;
});
}
// HACK: the disabled attribute can't be set via template literal...
if (this.genus !== undefined &&
this.species !== undefined &&
this.strain !== undefined &&
this.assembly !== undefined &&
this.annotation !== undefined) {
const value = this.selectedGenus &&
this.selectedSpecies &&
this.selectedStrain &&
this.selectedAssembly &&
this.selectedAnnotation
? this.formData.genuses[this.selectedGenus - 1].species[this.selectedSpecies - 1].strains[this.selectedStrain - 1].assemblies[this.selectedAssembly - 1].annotations
: '';
return html `
<select
class="uk-select uk-form-small"
disabled
.selectedIndex=${live(this.selectedAnnotation)}
="${this._selectAnnotation}"
>
<option value="">-- any --</option>
${options}
</select>
<input type="hidden" name="annotation" value="${value}" />
`;
}
return html `
<select
class="uk-select uk-form-small"
name="annotation"
.selectedIndex=${live(this.selectedAnnotation)}
="${this._selectAnnotation}"
>
<option value="">-- any --</option>
${options}
</select>
`;
}
// called when the form is submitted to run custom field validation
_validateForm(e) {
const formElement = e.target;
if (formElement == null)
return;
// check genes textarea validity
const genesElement = formElement.genes;
const identifiers = genesElement.value.trim().split(this.genesRegexp);
let genesValidity = '';
if (identifiers.length > this.genesLimit) {
genesValidity = `No more than ${this.genesLimit} gene identifiers allowed.`;
}
genesElement.setCustomValidity(genesValidity);
// check form validity; will catch standard and custom invalid fields
if (!formElement.checkValidity()) {
e.preventDefault();
e.stopPropagation();
formElement.reportValidity();
}
}
/** @ignore */
// used by LisPaginatedSearchMixin to draw the lookup form part of template
renderForm() {
// render the form's selectors
const genusSelector = this._renderGenusSelector();
const speciesSelector = this._renderSpeciesSelector();
const strainSelector = this._renderStrainSelector();
const assemblySelector = this._renderAssemblySelector();
const annotationSelector = this._renderAnnotationSelector();
// render the optional download button
let downloadButton = html ``;
if (this.downloadFunction !== undefined) {
downloadButton = html `
<button
type="submit"
value="download"
class="uk-button uk-button-default"
>
Download
</button>
<lis-inline-loading-element
${ref(this._downloadingRef)}
></lis-inline-loading-element>
`;
}
// render the form
return html `
<form class="uk-form-stacked" novalidate ="${this._validateForm}">
<fieldset class="uk-fieldset">
<legend class="uk-legend">Pangene Lookup</legend>
<lis-loading-element
${ref(this._formLoadingRef)}
></lis-loading-element>
<div class="uk-margin uk-grid-small" uk-grid>
<div class="uk-width-1-1@s">
<label class="uk-form-label" for="identifier"
>Gene Identifiers</label
>
<textarea
required
class="uk-textarea"
rows="5"
name="genes"
></textarea>
<lis-form-input-example-element
.text=${this.genesExample}
></lis-form-input-example-element>
</div>
</div>
<label class="uk-form-label"
>Constraints target pangenes must satisfy</label
>
<div class="uk-margin uk-grid-small" uk-grid>
<div class="uk-width-1-3@s">
<label class="uk-form-label" for="genus">Genus</label>
${genusSelector}
</div>
<div class="uk-width-1-3@s">
<label class="uk-form-label" for="species">Species</label>
${speciesSelector}
</div>
<div class="uk-width-1-3@s">
<label class="uk-form-label" for="strain">Strain</label>
${strainSelector}
</div>
</div>
<div class="uk-margin uk-grid-small" uk-grid>
<div class="uk-width-1-3@s">
<label class="uk-form-label" for="assembly">Assembly</label>
${assemblySelector}
</div>
<div class="uk-width-1-3@s">
<label class="uk-form-label" for="annotation">Annotation</label>
${annotationSelector}
</div>
</div>
<div class="uk-margin">
<button type="submit" class="uk-button uk-button-primary">
Lookup
</button>
${downloadButton}
</div>
</fieldset>
</form>
`;
}
};
/** @ignore */
// used by Lit to style the Shadow DOM
// not necessary but exclusion breaks TypeDoc
LisPangeneLookupElement.styles = css ``;
__decorate([
property()
], LisPangeneLookupElement.prototype, "formData", void 0);
__decorate([
property({ type: Function, attribute: false })
], LisPangeneLookupElement.prototype, "formDataFunction", void 0);
__decorate([
property({ type: String })
], LisPangeneLookupElement.prototype, "genus", void 0);
__decorate([
property({ type: String })
], LisPangeneLookupElement.prototype, "species", void 0);
__decorate([
property({ type: String })
], LisPangeneLookupElement.prototype, "strain", void 0);
__decorate([
property({ type: String })
], LisPangeneLookupElement.prototype, "assembly", void 0);
__decorate([
property({ type: String })
], LisPangeneLookupElement.prototype, "annotation", void 0);
__decorate([
property({ type: String })
], LisPangeneLookupElement.prototype, "genesExample", void 0);
__decorate([
property({ type: RegExp })
], LisPangeneLookupElement.prototype, "genesRegexp", void 0);
__decorate([
property({ type: Number })
], LisPangeneLookupElement.prototype, "genesLimit", void 0);
__decorate([
state()
], LisPangeneLookupElement.prototype, "selectedGenus", void 0);
__decorate([
state()
], LisPangeneLookupElement.prototype, "selectedSpecies", void 0);
__decorate([
state()
], LisPangeneLookupElement.prototype, "selectedStrain", void 0);
__decorate([
state()
], LisPangeneLookupElement.prototype, "selectedAssembly", void 0);
__decorate([
state()
], LisPangeneLookupElement.prototype, "selectedAnnotation", void 0);
LisPangeneLookupElement = __decorate([
customElement('lis-pangene-lookup-element')
], LisPangeneLookupElement);
export { LisPangeneLookupElement };
//# sourceMappingURL=lis-pangene-lookup-element.js.map