Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Select model.bind with server-side options #94

Closed
heruan opened this issue May 27, 2015 · 10 comments
Closed

Select model.bind with server-side options #94

heruan opened this issue May 27, 2015 · 10 comments
Labels

Comments

@heruan
Copy link

heruan commented May 27, 2015

When the selected value of a <select> is bound to a property of a server-side fetched model, and the available options are fetched with another API call, Aurelia binding cannot know which is the selected option since it comes from a separate request and it's not a reference from the options array.

Is there a best practice to handle this?

Use case HTML:

<select value.bind="job.assignee">
    <option repeat.for="employee of employees" model.bind="employee">${employee.name}</option>
</select>

and JavaScript:

import {HttpClient} from 'aurelia-http-client';

export class Job {
    constructor() {
        this.http = new HttpClient();
    }
    activate(params) {
        return Promise.all([
            this.http.get('/api/jobs/' + params.id).then(http => {
                this.job = JSON.parse(http.response);
            }),
            this.http.get('/api/employees').then(http => {
                this.employees = JSON.parse(http.response);
            })
         ]);
     }
}

where the API request /api/jobs/:id returns:

{
    "id": 1234,
    "assignee": {
       "name": "Jack",
       "age": 20
    }
}

and the /api/employees returns:

[
   {
       "name": "John",
       "age": 28
    }, {
       "name": "Jack",
       "age": 20
    }
]

In this case the <select> initial value would be the first option John instead of the correct option Jack.

@jdanyow
Copy link
Contributor

jdanyow commented May 28, 2015

Great question- here's a few options:

brute force

import {HttpClient} from 'aurelia-http-client';
import {employeeComparer} from './employees';

export class Job {
    constructor() {
        this.http = new HttpClient();
    }
    activate(params) {
        return Promise.all([
            this.http.get('/api/jobs/' + params.id).then(http => {
                this.job = JSON.parse(http.response);
            }),
            this.http.get('/api/employees').then(http => {
                this.employees = JSON.parse(http.response);
            })
         ]).then(() => {
             var assignee = this.job.assignee;
             if (assignee) {
                 this.job.assignee = this.employees.filter(employee => employeeComparer(employee, assignee))[0];
             }
         });
     }
}

converter

export class EmployeeToIDConverter {
  toView(employee, employees) {
    return employee ? employee.id : null;
  }
  fromView(id, employees) {
     return id ? employees.filter(e => e.id === id)[0] : null;
  }
}
<select value.bind="job.assignee | employeeToID:employees">
    <option repeat.for="employee of employees" model.bind="employee | employeeToID:$parent.employees">${employee.name}</option>
</select>

enhance Aurelia's select binding logic

Maybe we should add support for an equality-comparer attribute that would be used like this:

<select value.bind="job.assignee" equality-comparer.bind="employeeComparer">
    <option repeat.for="employee of employees" model.bind="employee">${employee.name}</option>
</select>

What do you think?

@heruan
Copy link
Author

heruan commented May 29, 2015

Thank you @jdanyow for your suggestions! After further investigation, I'm tempted to use the JSON.parse() optional argument reviver to return the same instance whenever the same object is parsed. That would require a map of retrieved objects (trivial) and a well-written reviver function to handle seamlessly [nested-]arrays and nested-objects (definitely non-trivial).

@heruan
Copy link
Author

heruan commented May 29, 2015

I came up to this reviver:

var map = new Map();
var reviver = function(k, v) {
    if (v === null || Array.isArray(v)) return v;
    if (typeof v !== 'object') return v;
    var key = JSON.stringify(v); // this is a simplified strategy to guess object identity
    if (map.has(key)) {
        return map.get(key);
    }
    map.set(key, v);
    return v;
};

applied to the former use case:

import {HttpClient} from 'aurelia-http-client';

export class Job {
    constructor() {
        this.http = new HttpClient();
        this.map = new Map();
        var _map = this.map;
        this.reviver = function(k, v) {
            if (v === null || Array.isArray(v)) return v;
            if (typeof v !== 'object') return v;
            var key = JSON.stringify(v);
            if (_map.has(key)) {
                return _map.get(key);
            };
            _map.set(key, v);
            return v;
        };
    }
    activate(params) {
        return Promise.all([
            this.http.get('/api/jobs/' + params.id).then(http => {
                this.job = JSON.parse(http.response, this.reviver);
            }),
            this.http.get('/api/employees').then(http => {
                this.employees = JSON.parse(http.response, this.reviver);
            })
         ]);
     }
}

and the binding works like a charm (see this gist for a test in NodeJS).

Obviously here I'm guessing object identity based on its string representation (which may vary between the two API requests), but one can use other strategies, e.g. key properties; also, JSON schema may help identifying the type of each property.

@jdanyow
Copy link
Contributor

jdanyow commented May 29, 2015

Cool stuff! One question about the reviver- why not use k as the key instead of stringifying v to produce the key? Apologies if this is a silly question, I don't know anything about revivers.

Edit

stopped being lazy and learned why 💤 Ignore this post...

@plwalters
Copy link
Contributor

@jdanyow I'm for adding the third option you listed but with an easier name like matcher :) Even a simple object is having problems matching up.

@jdanyow
Copy link
Contributor

jdanyow commented Jul 3, 2015

re-opening- @PWKad i like matcher

@jdanyow jdanyow reopened this Jul 3, 2015
@heruan
Copy link
Author

heruan commented Jul 6, 2015

I'm using this as a Reviver now, with slight modifications from the previous one:

  • I pass a pk argument which is the /primary key/ (it must be a UUID, unique among collection of different objects);
  • I use Object.assign() to update the current instance of the object.
export class Reviver {
    constructor() {
        this.map = new Map();
    }
    revive(json, pk) {
        var self = this;
        return JSON.parse(json, function(k, v) {
            if (v === null || Array.isArray(v)) return v;
            if (typeof v !== 'object') return v;
            var key = v[pk];
            if (!self.map.has(key)) {
                self.map.set(key, v);
                return v;
            };
            return Object.assign(self.map.get(key), v);
        });
    }
}

I'm still looking for a stronger strategy to discriminate object identity…

P.S. I see Aurelia's http-client uses a reviver in JSON.parse(). Is it configurable?

@plwalters
Copy link
Contributor

@heruan The reviver in the http-client is definitely configurable, you can set a shared reviver using .reviver(revfunc) during configuration or per request using .withReviver()

@32graham
Copy link
Contributor

I really like the matcher idea. This scenario happens on nearly every page I write for our line-of-business app.

@jdanyow
Copy link
Contributor

jdanyow commented Nov 25, 2015

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

5 participants