llselect + AngularJS 1.x - examples

Example directives, not a supported wrapper. Copy llselect-angularjs.js into your project and adapt it; a ui-select migration adds llselect-ui-select.js beside it. The reasoning, the source references, and the list of deliberate gaps are in angularjs/README.md.

Run npm run build, then npm run serve:site, and open /demo/angularjs/examples.html. AngularJS itself comes from a CDN; nothing AngularJS-related is in the package.

1. ui-select migration: <ui-llselect>, the compatibility directive

For ui-select migrations only. This is the compatibility directive for an existing ui-select codebase. If you were not using ui-select, use <llselect-single> / <llselect-multiple> from section 2 on instead.

A separate directive (llselect-ui-select.js, module llselect.uiCompat) that takes ui-select's call-site markup, so an existing ui-select codebase can migrate without rewriting every call site.

Migrating a call site: rename the elements (ui-select family to ui-llselect family) and add ll-item-text - ui-select has no item-to-string concept, and llselect needs one for the option's accessible name. The template content carries over untouched. CSS does not - llselect owns the DOM, so this is styled by the llselect theme.

1a. Rich row template (highlight + ui-disable-choice)

{{$select.selected.name}} {{p.role}}{{p.suspended ? ' - suspended' : ''}}
ng-model = {{ vm.person | json }}
markup

1b. Multiple: chips from the match template

As in real ui-select, a chosen person leaves the dropdown: remove-selected defaults to true (1g shows the opt-out).

{{$item.name}}
ng-model = {{ vm.people.length }} selected
markup

1c. allow-clear

{{$select.selected.name}}
ng-model = {{ vm.person2.name || 'undefined' }} (ui-select's own allow-clear carries over: the x clears back to undefined)
markup

1d. Template shapes: bare text / none

The template has three shapes: elements (above), a bare text node, or none at all - rows then render the ll-item-text string itself, which ui-select cannot do (its template is its only source of item text).

{{$select.selected.name}} {{p.name}} ({{p.role}})
ng-model = {{ vm.person3.name || 'undefined' }} / {{ vm.person4.name || 'undefined' }}
markup

1e. Custom filter, ui-select style

The | filter: chain IS the custom filter, re-evaluated once per typed query. Try vlan2 - the ifaceMatch filter (app.js) ignores spaces.

{{vm.ifaceText($select.selected)}}
ng-model = {{ vm.iface2 | json }}
markup

1f. ng-disabled + uib-tooltip (angular-ui-bootstrap)

Interop with uib-tooltip from angular-ui-bootstrap - the component kit most AngularJS apps already carry: both directives sit on the same element, no conflict. ng-disabled carries over here (ui-select parity, via attrs.$observe; the main directives use ll-disabled). The attribute is inert on this host, so hover works in BOTH states - the tooltip keeps working while disabled, which a native disabled control forbids.

{{$select.selected.name}}
ng-model = {{ vm.person5.name || 'undefined' }}
markup

1g. remove-selected: hide or keep chosen rows

Top: the default. A chosen person leaves the list, exactly like ui-select (remove-selected defaults to true; bridged onto hideChosenRows). Its allow-clear puts everyone back at once - the deviation the API page documents. Bottom: remove-selected="false" keeps chosen rows listed.

{{$item.name}} {{p.name}}
{{$item.name}} {{p.name}}
ng-model = {{ vm.peopleHide.length }} / {{ vm.peopleKeep.length }} selected
markup

Not bridged, by rule (llselect has no such concept, so it is ignored rather than half-implemented): tagging, sortable, append-to-body, async refresh, limit, themes.

2. Basic: <llselect-single>

ll-options takes an ng-options expression. Every clause maps onto an llselect setting; see the table in the README.

ng-model = {{ vm.country || 'undefined' }}
markup

3. Basic: <llselect-multiple> - count / tags / choose-all

The trigger has two displays: the default 'count' summary and ll-trigger-display="'tags'" chips. Rows carry live checkbox icons by default (ll-checkboxes="false" opts out), and ll-choose-all-row="true" adds the tri-state row acting on the visible enabled subset.

3a. Default trigger: count summary

ng-model = {{ vm.langsCount | json }}
markup

3b. Tags trigger

ng-model = {{ vm.langs | json }}
markup

3c. Choose-all row

ll-checkboxes (default true) draws SVG checkboxes on the rows and this row; ll-checkboxes="false" strips them all, leaving plain text - which is also the core's own default (core demo 5.7).

ng-model = {{ vm.langsAll | json }}
markup

3d. Hide chosen rows: ll-hide-chosen-rows

Chosen items leave the popup; removing a tag puts them back, and the clear button puts everything back at once. Pairs with the tags trigger, and with ll-checkboxes="false" - a listed row here is always unchecked, so checkboxes would say nothing.

ng-model = {{ vm.langsHidden | json }}
markup

4. Form validation: name + required + form.$valid

The point of this page. myForm.fruit resolves because AngularJS reads name off this element as a plain attribute - there is no native <select> and no hidden input anywhere. Clear the selection to watch the form go invalid.

myForm.$valid = {{ myForm.$valid }} | myForm.$dirty = {{ myForm.$dirty }}
myForm.fruit.$error = {{ myForm.fruit.$error | json }}
myForm.toppings.$error = {{ myForm.toppings.$error | json }} (toppings shows the $isEmpty override: [] counts as empty)
$dirty must stay {{ myForm.$dirty }} - a data reload is not a user edit.
markup

5. Ids in ng-model: select as

select as is the ngModel value projection. llselect has no equivalent setting on purpose - the app declares it, exactly as ng-options does.

ng-model = {{ vm.userId }} (a number, not the object)
markup

6. ll-disabled

Setting attributes are read once at construction; ll-disabled gets a $watch because it maps onto the setDisabled() method.

markup

7. ll-arrow, and llselectConfigProvider defaults

ll-arrow takes llselect's two built-in arrows and nothing else; a custom one means editing your copy of the file. The house style comes from llselectConfigProvider in .config() (see app.js) - this demo sets arrow: 'chevron' app-wide, which is why the selects above have one without saying so. A per-element ll-arrow wins over it.

markup

8. Custom item HTML: ll-item-content-fn

An expression evaluating to a function (item) => HTMLElement | null, passed straight to llselect's createItemContentElFn. It runs per rendered row per render, outside any digest, and the element is never $compiled - custom rows with zero per-row scope or watcher. The accessible name and filter text stay the item text from ll-options (icons and hints never reach a screen reader). All renderers are in app.js. For real per-row Angular templates, that is <ui-llselect> below.

8a. Icon rows, mirrored into the trigger

renderLangRow builds the row with createElement; feeding the SAME renderer to ll-trigger-content-fn is what makes the trigger show the chosen row - there is no auto-projection.

ng-model = {{ vm.lang || 'undefined' }}
markup

8b. Template-clone flavor, beside the checkboxes

renderLangRowFromTpl clones the <template> and fills it - the row markup lives in HTML, still with no scope and no $compile. On a multiple the element renders beside the default checkbox.

ng-model = {{ vm.langsRich | json }}
markup

8c. Faded hint text

renderUserRow: primary text plus a faded hint pushed to the row's right edge (.user-row), with disable when dimming suspended users on top. The name highlights the filter match via the core createHighlightedTextEl (8f's pattern).

ng-model = {{ vm.user2 || 'undefined' }}
markup

8d. Rich tag chips

ONE renderer feeds rows AND chips (ll-tag-content-fn); ll-tag-remove-button-content-fn swaps the remove icon, else the theme's CSS glyph draws the x.

ng-model = {{ vm.langsTags | json }}
markup

8e. Per-item background tint

What <option style="background-color"> does on a native select (Chromium only): renderTintedLangRow is 8a's row tinted from its icon color, with createCheckmarkSvgEl on the chosen row (rows re-render on chosen changes - never stale). All inline styles, plus one demo CSS line (lang-tinted) zeroing the option element's padding. The whole trigger takes the chosen colors: demo-tint-trigger requires the published controller and paints instance().triggerEl (API.md "Reaching the instance from your own directive").

ng-model = {{ vm.langTinted || 'undefined' }}
markup

8f. Highlight the filter matches: ll-highlight

ll-highlight="true" wraps each query match in the default item text in <mark> (the core createHighlightedTextEl); the marks follow the typing. With a custom ll-item-content-fn call the helper yourself, the way 8c does.

ng-model = {{ vm.hlCountry || 'undefined' }}
markup

9. ghiscoding/angular-validation

A third-party validation library, included because it is the case that started this: it throws unless the field has a name (validation-common.js:460). It reads that name off the ng-model element like everything else in AngularJS, so it needs no native <select> either. Its validation directive is restrict: 'A', require: 'ngModel', and it revalidates from a watch on ctrl.$modelValue - all element-agnostic.

avForm.$valid = {{ avForm.$valid }} | avForm.avFruit.$error = {{ avForm.avFruit.$error | json }}

Three integration facts worth knowing: it hard-depends on pascalprecht.translate; its messages need $translateProvider pointed at its locale files or you get "Could not translate: 'INVALID_REQUIRED'"; and validation is debounced by typingLimit (default 1000 ms), so it is not instant. Its elm.bind('blur') trigger is inert here - blur does not bubble and the focusable element is llselect's inner trigger - but the $modelValue watch is the path that matters for a select.

markup

10. Label element: ll-label-el

Native <label for> cannot target these divs. ll-label-el points at your label's id: the label names the field, and clicking it focuses the trigger - focus only, like a native label. (Core demo 12, as an attribute.)

ng-model = {{ vm.labelFruit || 'undefined' }}
markup

11. Custom filter: ll-filter-fn

Items are {interfaceType: 'vlan', interfaceNo: '2'}, shown as VLAN 2. The default filter matches the shown text (vlan 2 works out of the box); the custom fn also matches the space-less vlan2.

ng-model = {{ vm.iface | json }}
markup

12. Popup header / footer: ll-popup-header-content-fn / ll-popup-footer-content-fn

Content that stays put while the list scrolls: a header between the filter input and the items, a footer under them. Each fn runs once at link time and the returned node is yours to update, for example from ng-change. The same four examples as the core demo's section 14, done with attributes.

12a. Tags in the trigger, the count and a Clear all button in the header

The trigger shows chips, so the header takes over the count. ng-change rewrites it; ll-on-open and ll-on-filter-query-change rewrite the match count. The button reaches the widget through instance().

ng-model = {{ vm.countriesHeader | json }}
markup

12b. Column headings above two-column rows

Rows show a name and a role; the header repeats the same layout in bold. Rows re-render per keystroke, the header never does.

ng-model = {{ vm.userColumns.name || 'undefined' }}
markup

12c. A footer link to a management page

A pinned link under the list. Tab reaches it while the popup is open; Esc on it returns to the trigger. The app closes and navigates.

{{ vm.footerNote }}
markup

12d. Select all and Restore defaults pinned in the header, for comparison with 13a

The same two commands as 13a with the same row look, only pinned. Each is a real button wearing the theme's item class from ctx.classIdMap. Tab reaches them, but they sit outside the arrow-key ring.

ng-model = {{ vm.countriesCommands | json }}
markup

13. Action rows: ll-popup-list-leading-action-rows / ll-popup-list-trailing-action-rows / ll-popup-list-leading-rows-pinned

Commands rendered as option rows above or below the items, so the arrow keys reach them. onActivate receives the widget instance; calling it counts as the user's own change, so ng-change runs like after a click. textFn and disabledFn run again after every change. The same four examples as the core demo's section 15, done with attributes.

13a. Restore defaults above the items, Clear all below

The leading row follows the choose-all row; its disabled state reads the live model. The trailing row sits after the last item.

ng-model = {{ vm.countriesRows | json }} · ng-change ran {{ vm.countriesRowsChanges }} times
markup

13b. Add a country, above the items

The row asks for a name, adds it to the array behind ll-options, writes the model and closes. A row never closes the popup by itself.

ng-model = {{ vm.countryAdded || 'undefined' }}
markup

13c. Sort the list, above the items

The row flips the array behind ll-options, which $watchCollection hands to the widget. A filter query re-applies to the new order, and the chosen item stays chosen.

ng-model = {{ vm.countrySorted || 'undefined' }}
markup

13d. Select all and Restore defaults pinned at the top of the list

13a's leading rows with ll-popup-list-leading-rows-pinned="true": the choose-all row and the command stay in view while the items scroll under them, and keep their place in the arrow-key ring.

ng-model = {{ vm.countriesPinned | json }}
markup

14. UI language switch: instance().setUiTranslationPack follows $translate

llselect's own strings come from a language pack. That covers the placeholder, the search box, the clear and remove buttons, the count summary, the choose-all row and the no-results status. An app picks one once, in .config(): llselectConfigProvider.defaults({ uiTranslationPack: llselectI18n.zhTW }). Widgets already on screen are the app's job. This page runs the recipe from API.md, "Switching the UI language at runtime", verbatim in app.js. Pick a language: every widget on this page follows, chosen values survive, and 2a keeps its explicit ll-placeholder. The page's own copy stays English. RTL lives in the core demo, section 16.

{{$select.selected.name}} {{p.name}}
ng-model = {{ vm.countryI18n || 'undefined' }} · {{ vm.langsI18n | json }} · {{ vm.personI18n.name || 'undefined' }}
markup

15. Full source

The whole of what this page loads. Nothing is generated and nothing is minified: this is the file you copy.

llselect-angularjs.js - <llselect-single> / <llselect-multiple>
ui-llselect.js - the ui-select-shaped bridge
app.js - this demo's own controller