Skip to content
This repository has been archived by the owner. It is now read-only.
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.