Why use a data source?
Static ngs-option elements are perfect for short lists. A data source is better when options come from a server, the list is large, or users need remote search. The select calls your function when it needs data, passes paging and search state, and renders the returned items as options.
The data source contract
A SelectDataSource receives a request with search, page, pageSize, selectedValues, reason, and an optional signal. The important rule is that the first request must be able to return the currently selected option. That lets the trigger show a label even when the option is not part of the first loaded page.
import { SelectDataSource, SelectDataSourceOption } from '@ngstarter-ui/components/select';
interface UserOption {
id: string;
name: string;
team: string;
}
const usersDataSource: SelectDataSource<UserOption> = async request => {
const response = await usersApi.search({
search: request.search,
page: request.page,
pageSize: request.pageSize,
selectedValues: request.selectedValues,
signal: request.signal,
});
const selected = request.reason === 'initial'
? await usersApi.findByIds(request.selectedValues)
: [];
return {
items: toOptions([...selected, ...response.items]),
hasMore: response.hasMore,
nextCursor: response.nextPage,
};
};
function toOptions(users: UserOption[]): SelectDataSourceOption<UserOption>[] {
const seen = new Set<string>();
return users
.filter(user => {
if (seen.has(user.id)) {
return false;
}
seen.add(user.id);
return true;
})
.map(user => ({
label: `${user.name} - ${user.team}`,
value: user.id,
data: user,
}));
}Use it in ngs-select
Pass the function to [dataSource]. Enable searchable when the panel should include the remote search input. pageSize controls how many options load per request, and minSearchLength avoids sending short search terms to the API.
<ngs-form-field>
<ngs-label>Owner</ngs-label>
<ngs-select
[formControl]="owner"
[dataSource]="usersDataSource"
[pageSize]="20"
searchable
[minSearchLength]="1">
<ng-template ngsOptionContentDef let-user let-label="label">
<strong>{{ user?.name || label }}</strong>
<span>{{ user?.team }}</span>
</ng-template>
<ng-template ngsSelectValueDef let-user let-label="label">
{{ user?.name || label }}
</ng-template>
</ngs-select>
</ngs-form-field>Register it for Form Builder
Form Builder select fields use named data sources. Register each source with an id, a human-readable name, and the same SelectDataSource function. The name is what editors see in the select field inspector.
import {
provideFormBuilderSelectDataSource,
} from '@ngstarter-ui/components/form-builder';
@Component({
providers: [
provideFormBuilderSelectDataSource({
id: 'users',
name: 'Users',
dataSource: usersDataSource,
optionContentComponent: UserOptionComponent,
valueComponent: UserValueComponent,
}),
],
})
export class FormBuilderScreen {}Save the selected data source in the schema
A Form Builder select field stores optionsSource: 'dataSource' and the registered data source id. Static custom options still use optionsSource: 'static' and the regular options array.
const schema: FormBuilderSchema = {
sections: [
{
id: 'assignment',
title: 'Assignment',
fields: [
{
id: 'owner',
name: 'owner',
type: 'select',
label: 'Owner',
optionsSource: 'dataSource',
dataSource: 'users',
},
],
},
],
};Custom option and value templates
Direct ngs-select usage can customize async rows with ngsOptionContentDef and selected values with ngsSelectValueDef. Form Builder uses component classes instead, registered as optionContentComponent and valueComponent on the data source definition.
@Component({
selector: 'app-user-option',
template: `
<div class="flex flex-col">
<strong>{{ data?.name || label }}</strong>
<span>{{ data?.team }}</span>
</div>
`,
})
export class UserOptionComponent {
readonly data = input<UserOption | null>(null);
readonly label = input('');
}Implementation checklist
- Return selected options on the initial request when
selectedValuesis not empty. - Deduplicate selected options and page results before returning items.
- Respect
request.signalwhen your HTTP client supports aborting stale requests. - Use stable option values such as database ids, not labels.
- Keep Form Builder data source ids stable because schemas persist them.