Lookup target
Let a form input select a value from fetched records.
A lookup target connects an input to a lookup registered on a resource. The
lookup declares the fields its records return. The target names the lookup and
supplies the args for its params. An input variant holds the target and names
the two fields the control reads: value, the field a taken record writes into
the input, and label, the field the closed input shows.
Two input variants use a lookup target. lookupVia makes a picker with a
search. optionsVia makes a closed list. An enum field and a ref field accept
both.
API
lookupVia(
source: LookupDefinition | (() => LookupDefinition),
args?: (FieldBuilder | string | number | boolean)[],
options: {
value: string
label: string
create?: VisitTarget
}
): SearchInputVariant
optionsVia(
source: LookupDefinition | (() => LookupDefinition),
args?: (FieldBuilder | string | number | boolean)[],
options: {
value: string
label: string
}
): OptionsInputVariant
value and label must name fields the lookup’s resolver declares. TypeScript
checks both names, and checks that the value field’s value fits the input
field. The value is a scalar, always, because that is what a store holds.
args is required when the lookup declares params, and not permitted when it
declares none. The tuple has one entry for each param, in param order. A field
builder is read on the client as the value of the field with that name on the
same record as the input. A literal is sent as it is.
Search picker
The picker searches the lookup and shows its declared fields. Taking a record
writes its value field to the input.
import {
datalist,
defineResource,
fields,
lookupVia,
} from '@backlit/sdk'
import { customerPicker, customers } from './customers.ts'
const orders = defineResource('orders', [
fields.ref('customer', () => customers, {
label: 'Customer',
display: 'name',
inputVariant: lookupVia(customerPicker, {
value: 'id',
label: 'name',
}),
}),
])
orders.view('detail', () =>
datalist({
customer: { id: 42, name: 'A. Customer' },
}).setFields(orders.pick('customer'))
)
export default ordersimport { defineResource, fields } from '@backlit/sdk'
export const customers = defineResource('customers', [
fields.number('id'),
fields.text('name', { label: 'Name' }),
])
export const customerPicker = customers.lookup(
'picker',
(_ctx, lookup) =>
lookup
.setFields(customers.pick('id', 'name'))
.withData([])
){
"kind": "success",
"status": 200,
"node": {
"kind": "view",
"resource": "orders",
"name": "detail",
"slots": {
"content": [
{
"kind": "datalist",
"fields": [
{
"kind": "ref",
"name": "customer",
"label": "Customer",
"display": {
"kind": "text",
"name": "name",
"label": "Name"
},
"inputVariant": {
"kind": "search",
"lookup": {
"kind": "lookup",
"resource": "customers",
"name": "picker"
},
"value": "id",
"label": "name"
}
}
],
"data": {
"customer": "A. Customer"
}
}
]
}
}
}Closed list with params
The lookup declares a param. The state input supplies the country field as its arg. The client reads the country beside the state, asks the lookup when the country has a value, and asks again when it changes. A change clears the state.
import {
form,
defineResource,
fields,
optionsVia,
} from '@backlit/sdk'
import { z } from 'zod'
import { countries, countryPicker } from './countries.ts'
import { states, statePicker } from './states.ts'
const country = fields.ref('country', () => countries, {
label: 'Country',
display: 'name',
inputVariant: optionsVia(countryPicker, {
value: 'code',
label: 'name',
}),
})
const state = fields.ref('state', () => states, {
label: 'State',
display: 'name',
inputVariant: optionsVia(statePicker, [country], {
value: 'code',
label: 'name',
}),
})
const orders = defineResource('orders', [country, state])
const placeOrder = orders.action(
'place',
z.object({ country: z.string(), state: z.string() }),
() => {}
)
orders.view('create', () => {
const formBuilder = form(placeOrder)
return formBuilder.setContent([
formBuilder.fields(...orders.pick('country', 'state')),
])
})
export default ordersimport { defineResource, fields } from '@backlit/sdk'
export
const countries = defineResource('countries', [
fields.text('code'),
fields.text('name', { label: 'Name' }),
])
export const countryPicker = countries.lookup(
'picker',
(_ctx, lookup) =>
lookup
.setFields(countries.pick('code', 'name'))
.withData([
{ code: 'US', name: 'United States' },
{ code: 'CA', name: 'Canada' },
])
)import { defineResource, fields } from '@backlit/sdk'
const STATES = [
{ code: 'CA', name: 'California', country: 'US' },
{ code: 'NY', name: 'New York', country: 'US' },
{ code: 'ON', name: 'Ontario', country: 'CA' },
]
export const states = defineResource('states', [
fields.text('code'),
fields.text('name', { label: 'Name' }),
])
export const statePicker = states.lookup(
'byCountry',
{ params: ['country'] },
(ctx, lookup) =>
lookup
.setFields(states.pick('code', 'name'))
.withData(
STATES.filter(
(state) =>
state.country === ctx.paramValues.country
)
)
){
"kind": "success",
"status": 200,
"node": {
"kind": "view",
"resource": "orders",
"name": "create",
"slots": {
"content": [
{
"kind": "form",
"target": {
"resource": "orders",
"action": "place"
},
"slots": {
"content": [
{
"kind": "form-fields",
"fields": [
{
"kind": "ref",
"name": "country",
"label": "Country",
"display": {
"kind": "text",
"name": "name",
"label": "Name"
},
"inputVariant": {
"kind": "options",
"lookup": {
"kind": "lookup",
"resource": "countries",
"name": "picker"
},
"value": "code",
"label": "name"
}
},
{
"kind": "ref",
"name": "state",
"label": "State",
"display": {
"kind": "text",
"name": "name",
"label": "Name"
},
"inputVariant": {
"kind": "options",
"lookup": {
"kind": "lookup",
"resource": "states",
"name": "byCountry",
"args": [
{
"path": "country"
}
]
},
"value": "code",
"label": "name"
}
}
]
}
]
}
}
]
}
}
}Create target
Pass create to lookupVia to let the picker offer to make the record it
cannot find: one complete visit target, ordinarily opening a create view in a
dialog over the form.
lookupVia(categoryPicker, {
value: 'name',
label: 'name',
create: visitTo(categoryCreate, [], {
text: 'New category',
mode: 'drawer',
}),
})
Thunk
Pass the lookup definition as a function when resource modules refer to each other. Backlit reads it when it creates the protocol node.
import {
datalist,
defineResource,
fields,
lookupVia,
} from '@backlit/sdk'
import { customerPicker, customers } from './customers.ts'
const orders = defineResource('orders', [
fields.ref('customer', () => customers, {
label: 'Customer',
display: 'name',
inputVariant: lookupVia(() => customerPicker, {
value: 'id',
label: 'name',
}),
}),
])
orders.view('detail', () =>
datalist({
customer: { id: 42, name: 'A. Customer' },
}).setFields(orders.pick('customer'))
)
export default ordersimport { defineResource, fields } from '@backlit/sdk'
export const customers = defineResource('customers', [
fields.number('id'),
fields.text('name', { label: 'Name' }),
])
export const customerPicker = customers.lookup(
'picker',
(_ctx, lookup) =>
lookup
.setFields(customers.pick('id', 'name'))
.withData([])
){
"kind": "success",
"status": 200,
"node": {
"kind": "view",
"resource": "orders",
"name": "detail",
"slots": {
"content": [
{
"kind": "datalist",
"fields": [
{
"kind": "ref",
"name": "customer",
"label": "Customer",
"display": {
"kind": "text",
"name": "name",
"label": "Name"
},
"inputVariant": {
"kind": "search",
"lookup": {
"kind": "lookup",
"resource": "customers",
"name": "picker"
},
"value": "id",
"label": "name"
}
}
],
"data": {
"customer": "A. Customer"
}
}
]
}
}
}