@llselect/angularjs API
The complete attribute surface of the @llselect/angularjs directives, one entry per attribute. Install and loading, the two name attributes, integration gotchas and the design rationale live in the README; this page is only the reference.
Two independent files (see Files):
llselect-angularjs.js- modulellselect:<llselect-single>/<llselect-multiple>, driven by anng-options-stylell-optionsexpression.llselect-ui-select.js- modulellselect.uiCompat:<ui-llselect>, which takes ui-select's call-site markup instead. Needsllselect-angularjs.jsloaded too.
<llselect-single>
Single selection. ng-model holds the chosen item itself, or the select as projection when the ll-options expression has one. Takes the shared attributes.
<llselect-single name="fruit" ng-model="picked" required ll-filterable="true"
ll-options="f.id as f.name group by f.type disable when f.soldOut for f in fruits track by f.id">
</llselect-single>
<llselect-multiple>
Multiple selection. ng-model holds an array of chosen items (or of select as projections), and required treats [] as empty. Takes the shared attributes plus its own.
Shared attributes
Attributes of both <llselect-single> and <llselect-multiple>. Every entry opens with its binding mode in bold:
- Expression:
$eval'd against the scope ONCE at link time. llselect resolves its settings bag once at construction, so a later scope change does not move them; only the method-backedll-disabledis watched (see the Gotchas). String values need their own quotes:ll-placeholder="'Pick one'". - Literal: plain attribute text.
- Flag: acts by presence alone.
- Event expression: evaluated on EACH event, inside a digest, like
ng-click. Onlyll-on-open/ll-on-close.
App-wide defaults for arrow / filterable / highlight / popupWidthPolicy / uiTranslationPack are set once via llselectConfigProvider; a per-element attribute always wins.
ng-model
Expression, required. The chosen item on <llselect-single>; the array of chosen items on <llselect-multiple>. With select as in ll-options, the projected value(s) instead.
ng-change
Expression. Runs on each committed user choice - exactly like on a native control, no ll-change needed.
- These directives drive a real ngModel;
ng-changeis the standard$viewChangeListenerspipeline, which runs only on the$setViewValuepath: a real user choice. - It does NOT fire on load, on a programmatic model write, or when a data reload drops the chosen item - the write-back gate keeps those out of the view-change path (see the Gotchas).
- The same holds for everything else riding the ngModel pipeline: validators,
$dirty, angular-validation.
ll-on-open
Event expression -> onOpen. Evaluated on each open, inside a digest, so scope writes show up at once.
- Fires on every open that actually happens, user-driven or programmatic (
instance().open()). A call that does not actually open the popup - already open, disabled, or the trigger scrolled out of view / clipped - does not fire. - An error thrown by the expression goes to
$exceptionHandler, likeng-click; it never aborts the open. - This is the only way to observe opening from the directive: the core
onOpensetting is construction-frozen and the directive owns it.
ll-on-close
Event expression -> onClose. Evaluated on each close, inside a digest.
- Same rules as
ll-on-open. - It also fires for the close that destroying the element runs (for example
ng-ifremoving an open control): the popup did close, and AngularJS broadcasts$destroybefore disabling the scope. - In that teardown case, writes to parent-owned state (
vm.*, a service) persist; writes to the dying child scope itself are lost with it.
ll-options
ng-options grammar, required. Names each item's text, its identity and its model value in one line:
select as label group by group disable when disable for (key, value) in collection track by trackBy
| ng-options clause | llselect |
|---|---|
label (group 2, else group 1) |
itemToStringFn |
group by (group 3) |
itemToGroupKeyFn |
disable when (group 4) |
itemDisabledFn |
in collection (group 8) |
setItems(), via $watchCollection |
track by (group 9) |
compareFn |
select as (group 1, when as is present) |
no equivalent, by design |
select asis the ngModel value projection: "what does this item become in the model". Deliberately no llselect setting - the projection belongs to the app, andng-optionsis the app stating it. (Also why the core has noitemToValueFn: reusingitemToStringwould conflate display with identity, so switchinguiTranslationPackto another language would change your submitted values.)track byis a per-item hash;compareFnis pairwise equality. Same semantic, different shape:compareFn: (a, b) => sameValueZero(trackBy(a), trackBy(b))-===, plusNaNequalsNaN, the core's default identity rule.(key, value) in objectcollections throw - pass an array (see Not supported).NG_OPTIONS_REGEXPand its 9 capture groups are copied verbatim fromangular.js(MIT, (c) 2010-2020 Google LLC) intollselect-angularjs.js; nothing else from AngularJS is copied.
name
Literal. AngularJS form registration (myForm.<name>): myForm.$valid, myForm.<name>.$error.required and myForm.$dirty all work - with no native <select> and no hidden input.
- The value is NOT posted by a plain form submit, and the package never generates a
namefor you. - AngularJS's
nameand HTML'snameare unrelated mechanisms: The twonameattributes in the README.
required
Flag. The ngModel required validator. On <llselect-multiple>, [] counts as empty via a $isEmpty override - AngularJS's default would let required silently pass on an empty multi-selection (see the Gotchas).
ll-disabled
Expression, watched -> setDisabled(). Disables the widget. The only watched attribute, because it maps to a method; the other attributes feed construction-time settings, read once.
Disabling must go through llselect's own setDisabled():
- The state belongs on the inner TRIGGER element:
setDisabled()setsaria-disabled/data-disabled(and managestabindex) on the focusable combobox itself. Adisabled="true"attribute would sit on this host element - not a form control, so the browser ignores it completely. - Hover must survive: llselect never uses the native
disabledattribute, which suppresses pointer events. A disabled trigger stays hoverable (and focusable via the corefocusableWhenDisabledsetting), so a tooltip can still explain WHY it is disabled.
ng-disabled is the same trap one level up: it is one $watch whose only action is toggling that inert host attribute (angular.js:24530), so the markup looks applied while the widget stays fully interactive. See Not supported.
ll-placeholder
Expression -> placeholder. The trigger's empty-state text. A string, so it needs its own quotes: ll-placeholder="'Pick one'". Per-field copy, which is why it has no app-wide default.
ll-filterable
Expression -> filterable. Shows the search box. true / false / a predicate (items) => boolean.
ll-filter-fn
Expression -> filterFn. Custom match logic for the search box.
- Evaluated once at link time to a function
(item, query) => boolean. queryis the raw input: not trimmed, not lower-cased. Normalize it yourself.- Not called while the query is empty (an empty box shows every item).
- Without it, the default filter is a case-insensitive substring match against the item text from
ll-options- what is SHOWN is what matches.
A real case: items are {interfaceType: 'vlan', interfaceNo: '2'}, shown as VLAN 2. The default filter already matches vlan 2 (the shown text is the haystack). The custom fn below also matches the space-less vlan2:
$scope.ifaceText = function (i) { return i.interfaceType.toUpperCase() + ' ' + i.interfaceNo }
$scope.ifaceMatch = function (i, query) {
function norm(s) { return String(s).toLowerCase().replace(/\s+/g, '') }
return norm($scope.ifaceText(i)).includes(norm(query))
}
<llselect-single ng-model="picked" ll-filterable="true" ll-filter-fn="ifaceMatch"
ll-options="ifaceText(i) for i in interfaces track by i.interfaceNo">
</llselect-single>
ll-clearable
Expression -> clearable. Shows the trigger's clear (x) button.
- Clearing writes the empty value into
ng-model:undefined(single) /[](multiple), through the normalng-changepipeline. - If the model must stay a plain
string, add a real "none" item toll-optionsinstead of enabling this - the same pattern as a native<select>placeholder option.
ll-popup-width-policy
Expression -> popupWidthPolicy. 'fit-content' (llselect's default) / 'match-trigger'.
ll-arrow
Literal. The trigger arrow icon: chevron (default) / triangle / none.
- The chevron default is this package being batteries-included, unlike the core (which ships no arrow so the app decides).
noneopts out and leaves the slot to the theme.- A custom arrow means editing your copy of
llselect-angularjs.js- which is what a copy-paste package is for.
ll-item-content-fn
Expression -> createItemContentElFn. Custom visible content for each option row.
- Evaluated once at link time to a function
(item) => HTMLElement | null. null(for one item, or no attribute at all) = the plain item text fromll-options.- Runs per rendered row per render (open / filter / list change), entirely outside any digest. The element is NOT
$compiled - no Angular directives or bindings inside; build plain DOM (document.createElement, or clone a<template>). - To highlight the filter matches, wrap your text with the core helper
llselect.createHighlightedTextEl(text, query)and read the query via the controller'sinstance().getFilterQuery()- the per-keystroke re-render keeps the marks current. When the DEFAULT text is all you need,ll-highlightdoes that wiring for you. - The accessible name and the filter text stay owned by the
labelclause ofll-optionsno matter what you render (the library sets the option'saria-labelfrom it). - On
<llselect-multiple>the element renders beside the default checkbox icon;ll-checkboxes="false"hands it the whole row. - Need real per-row Angular templates? That is
<ui-llselect>- one child scope and one$compileper row is exactly the trade it prices in.
$scope.renderRow = function (fruit) {
var row = document.createElement('span')
var icon = document.createElement('i')
icon.className = 'mdi mdi-' + fruit.icon
icon.setAttribute('aria-hidden', 'true')
row.append(icon, ' ' + fruit.name)
return row
}
<llselect-single ng-model="picked" ll-item-content-fn="renderRow"
ll-options="f.name for f in fruits"></llselect-single>
ll-highlight
Expression -> boolean. Wraps each filter-query match in the default item text in a <mark> element (the core createHighlightedTextEl helper).
- The marks re-render per keystroke and clear with the query.
- Only the DEFAULT item text: combined with
ll-item-content-fnit throws - a custom content fn owns its whole content, so callllselect.createHighlightedTextElinside it instead. - On
<llselect-multiple>the text beside the default checkbox gets the marks. - App-wide default: the
highlightkey ofllselectConfigProvider. - The option's accessible name stays the plain
ll-optionstext (core contract).
ll-trigger-content-fn
Expression -> createTriggerContentElFn. Custom visible content for the trigger, under the same rules as ll-item-content-fn (outside any digest, never $compile'd).
<llselect-single>: the function receives{ chosenItem, items };null= the default rendering (the chosen item's text, or the placeholder).<llselect-multiple>: receives{ chosenItems, items };null= the count summary / tags. A returned element overrides both display modes.- The trigger does not mirror rich rows by itself - feeding this the same renderer as
ll-item-content-fnis what does that (demo 7).
ll-aria-label
Literal -> ariaLabel. The accessible name. The field's name resolves by the FIRST set rung, mirroring the W3C accessible-name computation order:
ll-aria-labelledby.ll-aria-label(this attribute).ll-label-el.- None set: the field is unnamed - a WAI-ARIA 1.2 violation, warned once per page in the console.
ll-aria-labelledby
Literal -> ariaLabelledBy. Space-separated element id(s) of the visible label.
ll-label-el
Literal -> labelEl. The id of your external label element. Native <label for> cannot target these divs; this wires both halves of the label relationship:
- The element names the field (
aria-labelledby), and clicking it focuses the trigger - focus only, never open, native<label>behavior. - Resolved once at link time via
getElementById; an unknown id throws (reported in the console, never silent). - Want only the accessible-name half? Use
ll-aria-labelledby.
<llselect-multiple> only
ll-trigger-display
Expression -> triggerDisplay. 'count' (default) / 'tags' - quoted: ll-trigger-display="'tags'".
ll-tag-content-fn
Expression -> createTagContentElFn. Custom content for one tag chip in ll-trigger-display="'tags'" mode.
- A function
(item) => HTMLElement | null;null= the plain item text. - The library still owns the chip container, the remove (x) button, and the button's
aria-label(Remove <label>). - The chip itself is a generic
<span>that ARIA prohibits naming - for icon-only content include your own visually hidden text if the chip should be announced as more than its remove button.
ll-tag-remove-button-content-fn
Expression -> createTagRemoveButtonContentElFn. The decorative icon inside each tag's remove (x) button.
- A function
(item) => HTMLElement | SVGElement | null;null(the default) = the theme's CSS glyph draws the x. - The library always owns the button, its click, and its
aria-label.
ll-choose-all-row
Expression -> chooseAllRow. A tri-state choose-all row as the first row of the popup; it gets the tri-state icon matching the row checkboxes plus the pack's counting text.
ll-hide-chosen-rows
Expression -> hideChosenRows. Chosen items' rows leave the popup list; unchoosing (e.g. removing a tag) puts them back.
- Pairs naturally with
ll-trigger-display="'tags'": the tags show what is chosen, the popup lists what is still choosable. - With the default checkbox rows, every rendered checkbox is unchecked by construction - consider
ll-checkboxes="false". - With
ll-choose-all-row, the row acts as "choose everything still listed" and disappears with the last listed row. - When every item is chosen, the popup shows the no-results message.
ll-checkboxes
Expression, default true. Whether <llselect-multiple> rows get this package's live checkbox icons.
- On by default - the same batteries-included trade as the arrow.
ll-checkboxes="false"strips every checkbox visual, including the choose-all row's icon; that row then shows only the counting text, which is also the core's own default.- The core itself ships no icons and no default indicator; per item its answer is the subclass recipe (demo 5.4 / 5.5).
- Single-select never gets checkboxes - a radio-like look would misstate multiplicity.
llselectConfigProvider
A house style set once, rather than repeated on 40 elements. Per-element ll-* attributes always win over it.
angular.module('app', ['llselect'])
.config(['llselectConfigProvider', function (llselectConfigProvider) {
llselectConfigProvider.defaults({
arrow: 'chevron', // 'chevron' | 'triangle' | 'none' | null (null = the chevron default)
filterable: true, // boolean, or a predicate (items) => boolean
highlight: true, // wrap filter matches in the default item text in <mark>
popupWidthPolicy: 'match-trigger', // llselect's own default is 'fit-content'
uiTranslationPack: llselectI18n.zhTW, // an llselect language pack
})
}])
defaults()
Takes the defaults bag above. Only settings that are app-wide by nature are accepted; placeholder is deliberately absent for the mirror-image reason - it IS per-field copy. An unknown key throws rather than being ignored, so a typo cannot silently do nothing.
arrow
'chevron' | 'triangle' | 'none' | null - null (the default) resolves to this package's batteries-included chevron; 'none' leaves the arrow slot to the theme. The app-wide default behind ll-arrow.
filterable
Boolean, or a predicate (items) => boolean. The app-wide default behind ll-filterable.
highlight
Boolean. The app-wide default behind ll-highlight: wrap each filter-query match in the default item text in <mark>.
popupWidthPolicy
'fit-content' (llselect's own default) / 'match-trigger'. The app-wide default behind ll-popup-width-policy.
uiTranslationPack
An llselect language pack (e.g. llselectI18n.zhTW). The clearest app-wide-by-nature case: an app picks its language once, and llselect's chrome strings are not per-field copy. Applied when a widget is built; to switch the language of live widgets, see Switching the UI language at runtime.
Reaching the instance from your own directive
Both directives publish a controller under their directive names (llselectSingle / llselectMultiple). An app-owned attribute directive on the same element can require it and drive the full llselect public API - the door for app-wide policies (a permission-driven disable, forced focusableWhenDisabled tooltips, ...).
requiretakes the array form or, since AngularJS 1.5, the named object form.- With
?the entry isnullon elements that are not llselect - what keeps a generic directive safe on native form controls.
instance()
Returns the live LLSelectSingle / LLSelectMultiple. Late-bound: the widget is constructed at link time, AFTER controllers instantiate - call it from a $watch or event handler, never from a controller constructor. Before link it throws; it never returns null.
A generic permission-driven disable that works on llselect AND native form controls (this exact shape is pinned by a test):
angular.module('app').directive('ownDisabled', ['permissions', function (permissions) {
return {
restrict: 'A',
require: { single: '?llselectSingle', multiple: '?llselectMultiple' },
link: function (scope, element, attrs, ctrls) {
var api = ctrls.single || ctrls.multiple // null on non-llselect elements
scope.$watch(function () { return permissions.canEdit() }, function (ok) {
if (api) {
api.instance().setDisabled(!ok) // llselect: state lives on the trigger; see ll-disabled
} else {
element.prop('disabled', !ok) // native form controls
}
})
},
}
}])
<llselect-single own-disabled ng-model="vm.fruit" ll-options="f for f in vm.fruits"></llselect-single>
<input own-disabled type="text">
Switching the UI language at runtime
The core method setUiTranslationPack(pack) swaps llselect's own UI strings in place, so switching language needs no widget rebuild. The wiring belongs to the app, because the current language is app state; it uses the same require + instance() door.
This recipe integrates angular-translate and changes no call site (this exact shape is pinned by a test):
angular.module('app')
.constant('LLSELECT_I18N_PACKS', { 'zh-TW': llselectI18n.zhTW, ja: llselectI18n.ja })
.directive('llselectSingle', i18nPackSync('llselectSingle'))
.directive('llselectMultiple', i18nPackSync('llselectMultiple'))
function i18nPackSync(ctrlName) {
return ['$translate', 'LLSELECT_I18N_PACKS', function ($translate, PACKS) {
return {
restrict: 'E',
require: ctrlName,
link: function (scope, element, attrs, ctrl) {
scope.$watch(function () { return $translate.use() }, function (langKey) {
if (langKey) { ctrl.instance().setUiTranslationPack(PACKS[langKey] || {}) }
})
},
}
}]
}
- A second directive registered under the SAME name decorates every matching element. AngularJS runs all directives sharing a name (angular.js 1.8.3, the
hasDirectivesregistry). $translate.use()with no argument returns the current language key (angular-translate 2.19.1,$translate.use). While the first language file is still loading it returnsundefined, so the guard skips.- The
$watchapplies the pack for the initial language and again on every switch. It deregisters with the scope, so there is nothing to clean up. - When no pack matches the key,
{}restores the English defaults.setUiTranslationPackmerges its argument over English, never over the previously set pack. - An explicit
ll-placeholderkeeps winning over each new pack'striggerPlaceholder. - A pack set via
llselectConfigProviderseeds widgets when they are built; this directive is what updates the ones already on screen.
<ui-llselect>
The migration bridge for an existing ui-select codebase (llselect-ui-select.js, module llselect.uiCompat; needs llselect-angularjs.js loaded too).
Migrating a call site, at a glance:
| ui-select | <ui-llselect> |
|
|---|---|---|
| Elements | <ui-select> / <ui-select-match> / <ui-select-choices> |
rename to <ui-llselect> / <ui-llselect-match> / <ui-llselect-choices> |
| Item text | does not exist in ui-select | add ll-item-text on <ui-llselect-choices> |
Template content, repeat, track by, group-by, multiple, ... |
as you wrote them | unchanged - see What carries over |
| CSS | ui-select themes | an llselect theme; the bridge takes ui-select's MARKUP, not its CSS |
- Scoping rule: bridge what llselect has; ignore what it does not. Nothing is half-implemented to look compatible.
- It always renders the chevron - every ui-select theme has a caret, so a bare trigger would read as broken.
- How the bridge is built, and why it is not a full ui-select reimplementation: the README,
DESIGN.mdandSPEC.md.
What carries over
| ui-select | <ui-llselect> |
|---|---|
<ui-select-match> template (single) |
<ui-llselect-match> -> createTriggerContentElFn |
<ui-select-match> template (multiple) |
<ui-llselect-match> -> createTagContentElFn - ui-select ng-repeats this slot over $select.selected, so it is per chip, not per trigger |
<ui-select-choices> template |
<ui-llselect-choices> -> createItemContentElFn, $compiled against a per-row child scope |
repeat="p in people" |
setItems via $watchCollection |
alias as item in source |
the ngModel projection, same role as ng-options' select as |
track by |
compareFn |
| filter: $select.search in the repeat |
filterFn. llselect owns the search box and asks per item, so the source expression is re-evaluated once per query and answers membership - your filter expression stays authoritative. No | filter: in the repeat = typing filters nothing, exactly as in ui-select |
group-by |
itemToGroupKeyFn |
ui-disable-choice |
itemDisabledFn |
multiple |
LLSelectMultiple (+ triggerDisplay: 'tags') |
search-enabled |
filterable. Defaults to true, following ui-select's default rather than llselect's false - it is ui-select's markup, so its defaults are what the call site expects |
remove-selected (multiple only, as in ui-select) |
hideChosenRows. Defaults to true like ui-select (chosen items leave the dropdown), rather than llselect's false - same fidelity rule as search-enabled. Pass remove-selected="false" to keep chosen rows listed |
placeholder, allow-clear (on <ui-select-match>) |
placeholder, clearable |
on-select, on-remove |
derived from onChange by diffing against the previous set |
ng-disabled / the disabled attribute |
setDisabled(), via attrs.$observe('disabled') - the exact mechanism ui-select itself uses, its string quirks included (a truthy string like interpolated "false" disables). The observed attribute stays inert on the host, so hover - and a why-tooltip - keep working while disabled |
aria-labelledby / aria-label / title (on <ui-llselect>) |
ariaLabelledBy / ariaLabel - the field's accessible name. Precedence follows ARIA: aria-labelledby (forwarded verbatim, names the field by reference) wins over aria-label, which wins over title; title keeps ui-select parity (its templates feed their aria labels from it) |
$select.selected, $select.search, $select.multiple |
published on each template's scope |
$index |
from createItemEl(item, index) |
Filtering
Your | filter: chain in repeat IS the custom filter - same syntax as ui-select, so a migrated call site keeps its matching behavior. The running case (same as ll-filter-fn): items are {interfaceType: 'vlan', interfaceNo: '2'}, shown as VLAN 2 via ll-item-text, and the space-less vlan2 must still match. ui-select style, that is a registered filter:
angular.module('app').filter('ifaceMatch', function () {
function norm(s) { return String(s).toLowerCase().replace(/\s+/g, '') }
return function (items, query) {
if (!query) { return items }
return (items || []).filter(function (i) {
return norm(i.interfaceType + ' ' + i.interfaceNo).includes(norm(query))
})
}
})
<ui-llselect-choices repeat="i in vm.interfaces | ifaceMatch: $select.search" ll-item-text="ifaceText(i)">
Differences from real ui-select:
- Cost: for TYPING, ui-select re-evaluates the repeat (filter included) on every digest, per row; the bridge evaluates it once per typed query and answers membership from the result. The bridge's items
$watchCollectionstill evaluates the full expression (with an empty query) once per digest to detect collection changes - same order of work as ui-select's own watcher, so no regression, but not free either. - The search box is llselect's own;
$select.searchis still published to your templates (e.g. for| highlight:). - A bare
| filter: $select.searchmatches each property separately, so a query spanning two fields (vlan 2) matches nothing - in real ui-select AND here. That is Angular'sfilterfilter; write one like the above. - Async searching (
refresh,refresh-delay,minimum-input-length,spinner-enabled) is not bridged: Not supported.
ll-item-text
Expression -> itemToStringFn. This attribute does not exist in ui-select - it is the ONE thing you add when migrating. llselect needs one string per item - the option's accessible name and the plain-text renderings (trigger fallback, template-less rows) - and ui-select's markup has no place that states it (its row content is template DOM).
Sits on <ui-llselect-choices>, written over the repeat variable:
<!-- ui-select, before -->
<ui-select ng-model="vm.person">
<ui-select-match placeholder="Pick a person">{{$select.selected.name}}</ui-select-match>
<ui-select-choices repeat="p in vm.people | filter: $select.search">
<span>{{p.name}}</span> <small>{{p.role}}</small>
</ui-select-choices>
</ui-select>
<!-- ui-llselect, after: the renamed elements + ll-item-text. Template content unchanged. -->
<ui-llselect ng-model="vm.person">
<ui-llselect-match placeholder="Pick a person">{{$select.selected.name}}</ui-llselect-match>
<ui-llselect-choices repeat="p in vm.people | filter: $select.search" ll-item-text="p.name">
<span>{{p.name}}</span> <small>{{p.role}}</small>
</ui-llselect-choices>
</ui-llselect>
Any of ui-select's template shapes carries over - plus one ui-select cannot do:
<!-- elements: full control of the row -->
<ui-llselect-choices repeat="p in vm.people | filter: $select.search" ll-item-text="p.name">
<span>{{p.name}}</span> <small>{{p.role}}</small>
</ui-llselect-choices>
<!-- a bare text node - ui-select accepts this shape too (its transclusion
appends every node, element or not) -->
<ui-llselect-choices repeat="p in vm.people | filter: $select.search" ll-item-text="p.name">
{{p.name}} ({{p.role}})
</ui-llselect-choices>
<!-- no template at all: each row renders the ll-item-text string. Impossible
in ui-select - there the template is the only source of item text. -->
<ui-llselect-choices repeat="p in vm.people | filter: $select.search" ll-item-text="p.name"></ui-llselect-choices>
- The template and
ll-item-textare separate channels: the template is what a row SHOWS,ll-item-textis the plain string (the accessible name + plain-text renderings). The bridge never derives one from the other - reading the rendered DOM back would put the role hint above into the option's spoken name. - Searching does NOT use it. The
| filter: $select.searchexpression inrepeatstays the filter, exactly as in ui-select (see What carries over): there is no single "search string" - your filter expression decides what matches (a bare| filter:matches every property of the item). - Without
ll-item-text, an object item degrades toString(item)("[object Object]").
Deliberate deviations
- No
scope: true. ui-select creates a child scope for<ui-select>, which silently shadows a non-dottedng-model:ng-model="p"writesponto the child and the parent never sees it. (That is the real reason ui-select's docs pushng-model="ctrl.p".) Every template<ui-llselect>compiles gets its own child scope anyway, so$selectlives there instead andng-modelkeeps the parent scope. Strictly better, and more compatible in practice. - The
highlightfilter is not provided. It is ui-select's, not llselect's, so the rule says do not bridge it. It is 8 lines;app.jscopies it from ui-select (MIT) so the demo's templates work without loading ui-select. Copy it the same way if your templates use| highlight: $select.search. Its.ui-select-highlightclass also needs ui-select's one CSS line (font-weight: bold) - theme CSS is not bridged either, so copy that rule too (the demo'sstyle.cssdoes). allow-clearalso works in multiple mode. Real ui-select renders the clear button only in single mode. Itsmatch-multipletemplates ignore the attribute, and the default is false (uiSelectMatchDirective.js:25). llselect's multiple does have a clear button, so the bridge honors the attribute there too. This is treated as a gap in ui-select, not a design choice: its ownremoveSelectedcarries the same kind of single-mode TODO. The default is unchanged - no attribute, no button. The only visible effect: a multiple that carried a deadallow-clearnow gets a working x that clears the whole selection.- Without
track by, items are compared withangular.equals. One compare rule; its consequences:- Multiple mode is ui-select's own comparison:
_isItemSelecteddeep-compares (uiSelectController.js:332). A reload's structurally equal fresh object is the SAME item: one chip survives, the row stays hidden underremove-selected, and a second entry cannot be added. - Single mode uses the same compare, so
aria-selectedand open-focus survive a reload. With identity they silently dropped while the trigger kept showing the choice. - The shown selection re-renders from the list's fresh object (llselect swaps the stored reference); the model keeps its own object, like ui-select.
- Items that reference each other (parent / child back-references) REQUIRE
track by:angular.equalsrecurses with no cycle guard. In multiple mode ui-select's own comparison shares this hazard; in single mode the deep compare is this bridge's addition (the single-mode sub-bullet), so there the constraint is the bridge's own. - Structurally equal duplicates are not supported; each mode has its own symptom:
- Multiple: a model that already holds duplicate entries renders one chip, and the first user change writes the deduplicated list back. ui-select renders every duplicate chip; the UI here can never create such a model.
- Single: every row equal to the chosen item is marked selected whenever the list renders - and it renders on every popup open, each filter keystroke, new items, and any
rerender(). If nothing is chosen, or something else is, no duplicate is marked. (A change that repaints one row while the popup stays open - the clear button, a model write - refreshes only the first equal row; see MEDIUM-77 below.) - Project-wide policy (resolved, MEDIUM-77): items must be UNIQUE under
compareFn- it defines item identity and the selection is a set. The coresetItemswarns once per page for the defaultcompareFn; these ui-select-bridge symptoms are the documented consequence of feeding duplicates and are the caller's to avoid. (Theangular.equalscompare is a customcompareFn, so the core's O(n) scan does not fire here - usetrack bywith unique keys.)
- Cost: the deep compare costs what ui-select pays per check;
track byreplaces it with a key compare.
- Multiple mode is ui-select's own comparison:
allow-clearis read once, as a static value. Real ui-select$observes the attribute, so an interpolatedallow-clear="{{vm.canClear}}"can flip at runtime. In llselectclearableis a construction-time setting (the settings-freeze rule), and the slot attributes are captured when the directive template is COMPILED - before any interpolation runs - soallow-clear="{{expr}}"can never work, and re-linking the same markup (ng-if) re-reads the same uninterpolated text. Write a literalallow-clear="true"(or the bare attribute). To flip clearability at runtime, switch between two literal templates: two<ui-llselect>blocks behind complementaryng-ifexpressions (AngularJS 1.x has nong-else), each with its literalallow-clearvalue.ng-ifcreates a child scope, so theng-modelthere must be dotted (vm.value) - a bare name would be shadowed.
Note
Rebuilding the highlight filter on the core createHighlightedTextEl helper was considered and rejected. A filter name is app-global, so the bridge's version would silently replace ui-select's filter everywhere the app uses it. The two also render differently: ui-select's filter never escapes the text, while the core helper escapes everything. Full reasoning is in DESIGN.md, under "Why the highlight filter is not rebuilt on the core helper".
Ignored attributes are listed under Not supported.
Not supported
Deliberate gaps. Each is reported or simply absent, never silently half-working.
- What "reported" means here (verified, not assumed): a directive's
thrownever reaches your code -$compile'sinvokeLinkFnwraps every link function in its owntry/catchand hands the error to$exceptionHandler(angular.js:11374), which by default logs it. So a badll-optionsdoes not crash the page; the widget simply never renders and the reason is in the console. Every AngularJS directive works this way,uiSelectMinErrincluded.
Both directive sets:
(key, value) in objectcollections. Pass an array.- Native
<form>submission. See The twonameattributes in the README.
llselect-angularjs.js:
- Filters on the collection (
ll-options="c for c in colors | filter:q"). Filter in your controller and let$watchCollectionsee the result. (<ui-llselect>does support| filter:insiderepeat, because that is ui-select's own filtering mechanism.) ng-disabled/ a plaindisabledattribute. Both only toggle the host'sdisabledattribute, which nothing here honors - the widget stays fully interactive while the markup claims otherwise. Usell-disabled; its entry has the two reasons disabling must go throughsetDisabled().
llselect-ui-select.js - ignored attributes, because llselect has no such concept:
tagging,tagging-label,tagging-tokens(llselect never creates items).refresh,refresh-delay,minimum-input-length,spinner-enabled(no async data-fetching API; root README, "No asynchronous data-fetching API").sortable,limit,paste,append-to-body,close-on-select,theme.$selectmembers that take a row scope:isActive,isDisabled,isLocked, pluson-highlightandui-lock-choice. See Why it is not a full ui-select reimplementation in the README.