Permalink
Browse files

feat(association-select): Added a new custom element

  • Loading branch information...
RWOverdijk committed Dec 22, 2015
1 parent ae86abb commit ab5d456f468b7cab92cbec6ed62dc5b03150a4e6
Showing with 264 additions and 0 deletions.
  1. +76 −0 src/component/README.md
  2. +6 −0 src/component/association-select.html
  3. +182 −0 src/component/association-select.js
@@ -0,0 +1,76 @@
# Components
Aurelia-orm comes bundled with some (at the time of writing just one) components to simplify working with entity data.
## association-select
> The `<association-select />` component composes a `<select />` element, of which the options have been populated from an endpoint.
**Basic example**
```html
<association-select
value.bind="data.author"
repository.bind="userRepository"
></association-select>
```
**Extended example**
```html
<!-- First, populate a list of categories -->
<association-select
value.bind="data.category.id"
repository.bind="categoryRepository"
></association-select>
<!-- Then populate a list of pages -->
<association-select
value.bind="data.page.id"
repository.bind="pageRepository"
></association-select>
<!-- Then populate a list of groups -->
<association-select
value.bind="data.group.id"
repository.bind="groupRepository"
></association-select>
<!-- And finally, populate a list of authors based on the previous selects -->
<association-select
value.bind="data.author.id"
repository.bind="userRepository"
property="username"
association="[data.page, data.group]"
manyAssociation="data.category"
criteria="{age:{'>':18}}"
></association-select>
```
Following are all attributes, and how they work.
### value
This is the selected value of the element. This functions the same as a regular `<select />` would.
### property
This tells the component which property to use from the data sent back by the resource (using the repository). **Defaults to `name`**.
### repository
This tells the component where it can find the data to populate the element. This is a simple `EntityManager.getRepository('resource')`.
### association
Add the association to the criteria, and listen for changes on the association so it can update when it does.
*This attribute accepts arrays, and can be combined with the `manyAssociation` attribute*.
This roughly translates to:
```javascript
repository.find({association: association.id});
```
### manyAssociation
Almost exactly the same as the `association` attribute, except for a `many` association. This will look up the data from the association's side.
_This attribute does **not** accept arrays, but can be combined with the `association` attribute_.
### criteria
Pass along filter criteria to the element. These will be used to restrict the data returned from the API.
@@ -0,0 +1,6 @@
<template>
<select class="form-control" value.bind="value">
<option selected disabled value="0">- Select a value -</option>
<option model.bind="option.id" repeat.for="option of options">${option[property]}</option>
</select>
</template>
@@ -0,0 +1,182 @@
import {bindable, inject} from 'aurelia-framework';
import {bindingMode, BindingEngine} from 'aurelia-binding';
import {customElement} from 'aurelia-templating';
import {EntityManager, OrmMetadata} from '../index';
import extend from 'extend';
@customElement('association-select')
@inject(BindingEngine, EntityManager)
export class AssociationSelect {
@bindable criteria = null;
@bindable repository;
@bindable property = 'name';
@bindable options;
@bindable association;
@bindable manyAssociation;
@bindable({defaultBindingMode: bindingMode.twoWay}) value;
ownMeta;
/**
* Create a new select element.
*
* @param {BindingEngine} bindingEngine
* @param {EntityManager} entityManager
*/
constructor(bindingEngine, entityManager) {
this._subscriptions = [];
this.bindingEngine = bindingEngine;
this.entityManager = entityManager;
}
/**
* (Re)Load the data for the select.
*/
load() {
this.buildFind()
.then(options => {
let result = options;
this.options = Array.isArray(result) ? result : [result];
});
}
/**
* Get criteria, or default to empty object.
*
* @return {{}}
*/
getCriteria() {
if (typeof this.criteria !== 'object') {
return {};
}
return extend(true, {}, this.criteria);
}
/**
* Build the find that's going to fetch the option values for the select.
* This method works well with associations, and reloads when they change.
*
* @return {Promise}
*/
buildFind() {
let repository = this.repository;
let criteria = this.getCriteria();
let findPath = repository.getResource();
// Check if there are `many` associations. If so, the repository find path changes.
// the path will become `/:association/:id/:entity`.
if (this.manyAssociation) {
let assoc = this.manyAssociation;
let property = this.propertyForResource(assoc.getMeta(), repository.getResource());
findPath = `${assoc.getResource()}/${assoc.id}/${property}`;
} else if (this.association) {
let associations = Array.isArray(this.association) ? this.association : [this.association];
associations.forEach(association => {
criteria[this.propertyForResource(this.ownMeta, association.getResource())] = association.id;
});
} else {
criteria.populate = false;
}
return repository.findPath(findPath, criteria);
}
/**
* Check if all associations have values set.
*
* @return {boolean}
*/
verifyAssociationValues() {
if (this.manyAssociation) {
return !!this.manyAssociation.id;
}
if (this.association) {
let associations = Array.isArray(this.association) ? this.association : [this.association];
return !associations.some(association => {
return !association.id;
});
}
return true;
}
/**
* Add a watcher to the list. Whenever given association changes, the select will reload its contents.
*
* @param {Entity|Array} association Entity or array of Entity instances.
*
* @return {AssociationSelect}
*/
observe(association) {
if (Array.isArray(association)) {
association.forEach(assoc => this.observe(assoc));
return this;
}
this._subscriptions.push(this.bindingEngine.propertyObserver(association, 'id').subscribe(() => {
if (this.verifyAssociationValues()) {
return this.load();
}
this.options = undefined;
}));
return this;
}
/**
* When attached to the DOM, initialize the component.
*/
attached() {
if (!this.association && !this.manyAssociation) {
this.load();
return;
}
this.ownMeta = OrmMetadata.forTarget(this.entityManager.resolveEntityReference(this.repository.getResource()));
if (this.manyAssociation) {
this.observe(this.manyAssociation);
}
if (this.association) {
this.observe(this.association);
}
}
/**
* Find the name of the property in meta, reversed by resource.
*
* @param {Metadata} meta
* @param {string} resource
*
* @return {string}
*/
propertyForResource(meta, resource) {
let associations = meta.fetch('associations');
return Object.keys(associations).filter(key => {
return associations[key] === resource;
})[0];
}
/**
* Dispose all subscriptions on unbind.
*/
unbind() {
this._subscriptions.forEach(subscription => subscription.dispose());
}
}

0 comments on commit ab5d456

Please sign in to comment.