@soxa-ui/angular 0.0.0-stage → 0.3.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +613 -2
- package/dist/fesm2022/soxa-ui-angular.mjs +19957 -0
- package/dist/fesm2022/soxa-ui-angular.mjs.map +1 -0
- package/dist/types/soxa-ui-angular.d.ts +11535 -0
- package/package.json +70 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yuri Soldatov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,614 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @soxa-ui/angular
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**The Angular adapter: Soxa UI components rendered on the headless models of the core.**
|
|
4
|
+
|
|
5
|
+
Part of [Soxa UI](https://github.com/youra-h/soxa-ui) — UI components with the logic written once: a
|
|
6
|
+
framework-agnostic core and thin adapters that render it in Vue, React, Angular, Svelte, Solid and
|
|
7
|
+
Web Components.
|
|
8
|
+
|
|
9
|
+
The adapter is wiring, not behavior: it connects a core model to Angular's signals and renders the
|
|
10
|
+
markup — BEM classes `so-*` and `data-*` state attributes. Props, events and slots come from the
|
|
11
|
+
component descriptor, so they match the other five adapters; only the spelling is Angular's: a
|
|
12
|
+
component is always a prefixed attribute, never a tag of its own — on your own element when it has
|
|
13
|
+
an element to render into (`<button so-button>`, `<div so-input>`; the toasts manager has its
|
|
14
|
+
region, so it is `<div so-toasts>`), and on an `<ng-container>` when it renders no element of its
|
|
15
|
+
own (`<ng-container so-virtual>`, `<ng-container so-drag-and-drop>`, like
|
|
16
|
+
`<ng-container so-locale-provider>`) — events are outputs
|
|
17
|
+
named `so` + the core name in camelCase (`show` → `soShow`, `show:before` → `soShowBefore`,
|
|
18
|
+
`element:ready` → `soElementReady`), and slots are filled through the `slot` attribute. Every
|
|
19
|
+
ordinary event output has the prefix: Angular binds `(x)` both to an output and to the DOM event of that name, so
|
|
20
|
+
`(input)` on a field is the browser's event, as on a native `<input>`, and the component's own is
|
|
21
|
+
`(soChangeValue)`.
|
|
22
|
+
|
|
23
|
+
The components are standalone and zoneless — state is a signal, so Zone.js is not required. They
|
|
24
|
+
render on the server (`@angular/platform-server`) and hydrate in the browser
|
|
25
|
+
(`provideClientHydration`); component ids match on both sides.
|
|
26
|
+
|
|
27
|
+
> **Status:** the Angular adapter is in progress — Button, Icon, Label, Frame, ProgressSpinner,
|
|
28
|
+
> ProgressLinear (`<span so-progress-linear>`), Skeleton, Input, NumberInput (a number in the
|
|
29
|
+
> locale's format with − and + step buttons; the form gets the number), Textarea (multi-line,
|
|
30
|
+
> growing with its text, with a character counter), CheckBox, Switch, Slider (`<span so-slider>`),
|
|
31
|
+
> Rating (`<span so-rating>`), the form field `<div so-field>` (label, helper and error linked to
|
|
32
|
+
> the control inside it), Scroller (`<div so-scroller>`), RadioGroup, ListBox, Tags
|
|
33
|
+
> (`<div so-tags>`), Select (`<div so-select>`, a field with a list in a panel), Tabs, Accordion,
|
|
34
|
+
> Pagination (`<nav so-pagination>`), Table, the dates — DateInput (`<div so-date-input>`, a date
|
|
35
|
+
> field of parts), Calendar (`<div so-calendar>`) and DatePicker (`<div so-date-picker>`, a date
|
|
36
|
+
> field with a calendar in a panel) —, the `Virtual` window for long lists and the
|
|
37
|
+
> `<ng-container so-drag-and-drop>` wrapper that lets users reorder the items of Tabs, ListBox and
|
|
38
|
+
> Accordion by dragging (`(soMove)`, `(soMoveBefore)` can cancel) are ported, the rest of the set is on its
|
|
39
|
+
> way. Frame moves the element you wrote into `target` (`body` by default) in the browser; with
|
|
40
|
+
> `contained` it stays in place. Popover (`<span so-popover>`) and Tooltip (`<span so-tooltip>`)
|
|
41
|
+
> are panels on a Frame anchored to the element you write them on. Menu (`<span so-menu>`) is an
|
|
42
|
+
> action menu at a button or, with `openOn="contextmenu"`, on right click, with submenus,
|
|
43
|
+
> checkbox and radio items and an item with its own panel. The modal window Dialog
|
|
44
|
+
> (`<div so-dialog>`) and the sliding panel Drawer (`<div so-drawer>`) are layers themselves, with a
|
|
45
|
+
> backdrop placed before them. Toasts (`<div so-toasts>` around the application, a toast from code
|
|
46
|
+
> with `useToasts().show(…)`) show notifications at an edge of the window. `so-locale-provider`
|
|
47
|
+
> gives a subtree its language and library strings. See
|
|
48
|
+
> [the adapter table](https://github.com/youra-h/soxa-ui#components).
|
|
49
|
+
|
|
50
|
+
## Install
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npm install @angular/forms @soxa-ui/angular @soxa-ui/setup @soxa-ui/theme-oren @soxa-ui/icons-material
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Install `@soxa-ui/setup` as a direct dependency for the `useIcons` and `useTheme` imports below.
|
|
57
|
+
Keep it on the same version as `@soxa-ui/angular`.
|
|
58
|
+
|
|
59
|
+
`@angular/core`, `@angular/common` and `@angular/platform-browser` (Angular 22.2+) are peer
|
|
60
|
+
dependencies. A theme and an icon pack are separate packages — pick the ones you want.
|
|
61
|
+
|
|
62
|
+
Add `provideAttrs()` to the providers of your application config — the one shared by the browser and
|
|
63
|
+
the server:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import { ApplicationConfig } from '@angular/core'
|
|
67
|
+
import { provideAttrs } from '@soxa-ui/angular'
|
|
68
|
+
|
|
69
|
+
export const appConfig: ApplicationConfig = {
|
|
70
|
+
providers: [provideAttrs()],
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
It lets the bindings and listeners you write on a component reach the node inside it they belong to
|
|
75
|
+
(see the fields below). It is required: without it a field, Select, a collection item, ListBox,
|
|
76
|
+
Tabs, Table and Toasts throw an error telling you to add it. Put it in the application config, not in the providers
|
|
77
|
+
of a route or a component — there it cannot replace the renderer of the application.
|
|
78
|
+
|
|
79
|
+
## Usage
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import { Component } from '@angular/core'
|
|
83
|
+
import { TButtonComponent } from '@soxa-ui/angular'
|
|
84
|
+
|
|
85
|
+
@Component({
|
|
86
|
+
selector: 'app-toolbar',
|
|
87
|
+
standalone: true,
|
|
88
|
+
imports: [TButtonComponent],
|
|
89
|
+
template: `<button so-button text="Save" (soActionPress)="onPress()"></button>`,
|
|
90
|
+
})
|
|
91
|
+
export class ToolbarComponent {
|
|
92
|
+
onPress(): void {
|
|
93
|
+
console.log('pressed')
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
There is no wrapper: the element you write is the component's root. Any tag works and keeps its
|
|
99
|
+
browser behavior — `<button so-button>` is a native button, `<a so-button href="/next">` is a link,
|
|
100
|
+
`<div so-button>` is a `div` that gets `role="button"` and `tabindex="0"`. The tag is read from the
|
|
101
|
+
element, so there is no `tag` input. Your `class`, `style`, attributes and event listeners stay on
|
|
102
|
+
the element. What the component writes on it — a class, an attribute, a style property — is the
|
|
103
|
+
component's, the way a host binding (`[attr.x]`, `[class.x]`) is: it writes over a value you set
|
|
104
|
+
under the same name, so `<div so-button tabindex="-1">` still gets `tabindex="0"`. Names the
|
|
105
|
+
component does not write stay yours. `rendered` set to `false` hides the element instead of
|
|
106
|
+
removing it — the element is yours — and your inline `display` wins over hiding.
|
|
107
|
+
|
|
108
|
+
Import the component you use: without `TButtonComponent` in `imports`, `<button so-button>` is a
|
|
109
|
+
plain button and Angular reports nothing; only an input binding such as `[text]` fails (NG8002).
|
|
110
|
+
|
|
111
|
+
The same component can be driven from code — pass a core instance as `ctrl`, and both sides see the
|
|
112
|
+
same state and events:
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
template: `<button so-button [ctrl]="btn" (soActionPress)="btn.disabled = true"></button>`
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
An icon from the registered pack is an `<svg>` with the icon's role — import `RoleIconDirective`
|
|
119
|
+
next to `TIconComponent`. Your own drawing is registered as a role too (`useIcons`): the icon renders
|
|
120
|
+
nothing written inside it.
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
template: `<svg so-icon so-role-icon="close" aria_label="Close"></svg>`
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
A field — Input, NumberInput, Textarea, CheckBox, Switch — is written on a `div` or, for the inline
|
|
127
|
+
CheckBox and Switch, a `span` (`<div so-input>`, `<div so-number-input>`, `<div so-textarea>`,
|
|
128
|
+
`<span so-check-box>`, `<span so-switch>`) and renders a native `<input>` (a `<textarea>`) inside,
|
|
129
|
+
so your markup is split the way Vue and React split it: `class` and `style` stay on the field,
|
|
130
|
+
while everything else you write on it goes to the `<input>` — static attributes (`autocomplete`,
|
|
131
|
+
`data-*`), bindings (`[attr.aria-describedby]`, `[aria-label]`, `[title]`) and listeners
|
|
132
|
+
(`(focus)`, `(blur)`), which is what `provideAttrs()` is for. A field is not an `<input>` itself —
|
|
133
|
+
`<input so-input>` throws, since its template would have nowhere to render. A field gets its
|
|
134
|
+
visible name from a Label around it:
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
template: `<label so-label text="Remember me"
|
|
138
|
+
><span so-check-box (soChangeValue)="remember($event)"></span
|
|
139
|
+
></label>
|
|
140
|
+
<div so-input clearable name="City" autocomplete="address-level2" (blur)="check()"></div>`
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Slots that hand data in are `<ng-template slot>` — the content next to an Input gets the field
|
|
144
|
+
itself:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
template: `<div so-input>
|
|
148
|
+
<ng-template slot="trailing" let-ctrl="ctrl">{{ ctrl.value?.length ?? 0 }}/40</ng-template>
|
|
149
|
+
</div>`
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
A number field shows its number in the locale's format — `1,234.5`, `12%` or `RUB 1,234.50` with
|
|
153
|
+
`formatOptions` — and steps it within `min` and `max` with the arrow keys and the − and + buttons,
|
|
154
|
+
which repeat while held. The form gets the number, not the text: `name` goes to a hidden `<input>`
|
|
155
|
+
with `1234.5`, while what you write on the field goes to the text `<input>`. `(soChangeValue)`
|
|
156
|
+
comes on every typed digit and step, `(soCommit)` once per finished action — leaving the field,
|
|
157
|
+
Enter, releasing a step button. The icons of the step buttons are templates,
|
|
158
|
+
`<ng-template slot="decrement-icon">` and `increment-icon`:
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
template: `<div so-number-input name="qty" [min]="0" [value]="qty()" (soCommit)="save($event.newValue)"></div>`
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
A slider and a rating are fields as well: `<span so-slider>` has a native `<input type="range">` in
|
|
165
|
+
each thumb, `<span so-rating>` one under its row of stars, and the form gets their values under
|
|
166
|
+
`name`. A number is one thumb, an array is a thumb per item (`[value]="[20, 80]"`). The pointer
|
|
167
|
+
works on the track, never on the inputs, so what you write on these two stays on the element; only
|
|
168
|
+
a rating's `id` is an input, and it goes to its range input, where `<label for>` finds it. With
|
|
169
|
+
several thumbs the slider is a group named by `aria_label`, and each thumb takes its own name from
|
|
170
|
+
`thumbLabels`. The `mark` and `thumb` slots of a slider and the `icon` slot of a rating are
|
|
171
|
+
templates with the data of their mark, thumb or star:
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
template: `<label so-label text="Volume" position="top"><span so-slider name="volume" [value]="40"></span></label>
|
|
175
|
+
<span so-slider [value]="[20, 80]" aria_label="Price" [thumbLabels]="['From', 'To']" tooltip="always">
|
|
176
|
+
<ng-template slot="thumb" let-value="value">{{ value }} ₽</ng-template>
|
|
177
|
+
</span>
|
|
178
|
+
<span so-rating name="score" [value]="3.5" [step]="0.5" aria_label="Score" (soCommit)="rate($event.newValue)"></span>`
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
A collection — RadioGroup, ListBox, Tabs, Accordion — is an owner element with its items inside
|
|
182
|
+
(`<div so-radio-group>`, `<div so-list-box>`, `<div so-tabs>`, `<div so-accordion>`). The parts are
|
|
183
|
+
components of their own, written as separate elements, since Angular has no dot syntax:
|
|
184
|
+
`<div so-list-box-item>`, `<div so-tabs-item>`, `<div so-accordion-item>`,
|
|
185
|
+
`<label so-radio-group-item>` and the tab panel `<div so-tabs-content>`. Items written in the markup
|
|
186
|
+
join the collection in document order, on the server too; without them the owner renders its
|
|
187
|
+
`items`. Inputs of the collection — `items`, `engine`, `mode`, an item's `selected` or `active` — are
|
|
188
|
+
inputs of the same element:
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
template: `<div so-list-box mode="multiple" aria-label="Cities" (soChangeValue)="pick($event.newValue)">
|
|
192
|
+
<div so-list-box-item value="a" text="Moscow"></div>
|
|
193
|
+
<div so-list-box-item value="b" text="Kazan" selected></div>
|
|
194
|
+
</div>
|
|
195
|
+
<div so-tabs>
|
|
196
|
+
<div so-tabs-item value="a" text="General" active></div>
|
|
197
|
+
<div so-tabs-item value="b" text="Mail"></div>
|
|
198
|
+
<div so-tabs-content slot="content" value="a">…</div>
|
|
199
|
+
<div so-tabs-content slot="content" value="b">…</div>
|
|
200
|
+
</div>`
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
A list box and tabs split your markup like a field: `class` and `style` stay on the element — a
|
|
204
|
+
frame around the list, next to the header and footer of a list box or the panels of tabs — while
|
|
205
|
+
everything else you write on it (`aria-label`, `id`, `tabindex`, `data-*`, `[attr.x]`, `(keydown)`)
|
|
206
|
+
goes to the list itself, `[role=listbox]` or `[role=tablist]`, which it names. An item splits your
|
|
207
|
+
markup the same way: `class` and `style` stay on the item, the rest (`title`, `id`, `(click)`) goes
|
|
208
|
+
to its row — or to the `<input>` of a radio. Animation and transition events (`(transitionend)`)
|
|
209
|
+
stay on the element you wrote: that is where they happen. Inside
|
|
210
|
+
a Label, write the radio on an inline tag, `<span so-radio-group-item>`: a `label` inside a `label`
|
|
211
|
+
is not allowed. Items rendered from `items` get their slots from the owner — `item`, `item-leading`
|
|
212
|
+
and the rest receive the item along with their scope:
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
template: `<div so-radio-group [items]="options">
|
|
216
|
+
<ng-template slot="item" let-item="item" let-active="active">{{ item.value }}</ng-template>
|
|
217
|
+
</div>`
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
The `[so-attributes]` directive on such a component goes the same way as the rest of your markup:
|
|
221
|
+
the set lands on the `<input>`, the row or the list, on top of what the component puts there
|
|
222
|
+
itself.
|
|
223
|
+
|
|
224
|
+
A slot by which a part decides whether to render its own default — the close icon of a tab, the
|
|
225
|
+
arrows of an accordion section, the edges of the tab list — is an `<ng-template slot>` only, and a
|
|
226
|
+
template given to the owner renders in every item in place of its own. A tab panel that is not
|
|
227
|
+
active stays in place hidden, its content not inserted; a markup tab closed with its close button
|
|
228
|
+
is hidden the same way — the element is yours.
|
|
229
|
+
|
|
230
|
+
A tab panel takes its content as is or as a template, `<ng-template slot="default">`, and a given
|
|
231
|
+
template is drawn instead. Write heavy content — a table, a calendar, a form — as a template: the
|
|
232
|
+
panel creates it when its tab is chosen and removes it when the tab is left, so an inactive panel
|
|
233
|
+
holds nothing. Content written as is Angular creates right away, with the template that wrote it:
|
|
234
|
+
the components of every panel live as long as the tabs do, an inactive panel only keeps them out of
|
|
235
|
+
the document.
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
template: `<div so-tabs>
|
|
239
|
+
<div so-tabs-item value="a" text="General" active></div>
|
|
240
|
+
<div so-tabs-item value="b" text="History"></div>
|
|
241
|
+
<div so-tabs-content slot="content" value="a">…</div>
|
|
242
|
+
<div so-tabs-content slot="content" value="b">
|
|
243
|
+
<ng-template slot="default"><div so-table [items]="rows" [columns]="columns"></div></ng-template>
|
|
244
|
+
</div>
|
|
245
|
+
</div>`
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
A tag set, `<div so-tags>`, is the row itself: your markup stays on it, next to its role. Tags are
|
|
249
|
+
`<div so-tags-item>` parts or come from `items`; with `mode` they become selectable, and with
|
|
250
|
+
`closable` every tag gets a close button. To lay the shown tags out yourself — in a scroller strip,
|
|
251
|
+
or a row with the rest in a popover — give the set a `default` template: its context carries
|
|
252
|
+
`shown`, and every tag is rendered in it exactly once.
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
template: `<div so-tags closable [items]="cities" (soChangeValue)="pick($event.newValue)"></div>
|
|
256
|
+
<div so-tags overflow="scroll" [items]="cities">
|
|
257
|
+
<ng-template slot="default" let-shown="shown">
|
|
258
|
+
<div so-scroller>
|
|
259
|
+
@for (tag of shown; track tag.uid) {
|
|
260
|
+
<div so-tags-item [ctrl]="tag"></div>
|
|
261
|
+
}
|
|
262
|
+
</div>
|
|
263
|
+
</ng-template>
|
|
264
|
+
</div>`
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
A select, `<div so-select>`, is a field with the list in a panel anchored to the element, moved into
|
|
268
|
+
`body` like a Frame. Options are `<div so-select-item>` parts, grouped with
|
|
269
|
+
`<div so-select-group label="…">`, or come from `items`; with `mode="multiple"` the chosen ones
|
|
270
|
+
show as tags in the field, `editable` lets you type, and `editableMode="filter"` narrows the list.
|
|
271
|
+
It splits your markup like a field: `class` and `style` stay on the element, while everything else
|
|
272
|
+
— `aria-label`, `id`, `[attr.x]`, `(keyup)` — goes to the `<input>` of its field. The `field`,
|
|
273
|
+
`tags`, `clear`, `arrow-icon` and item slots are templates; `leading`, `trailing` and `empty` are
|
|
274
|
+
elements by the `slot` attribute. Options join the collection on the server too, so the server
|
|
275
|
+
renders the field with the chosen text; the panel exists only in the browser.
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
template: `<div so-select placeholder="City" aria-label="City" (soChangeValue)="pick($event.newValue)">
|
|
279
|
+
<div so-select-item value="msk" text="Moscow"></div>
|
|
280
|
+
<div so-select-group label="Volga">
|
|
281
|
+
<div so-select-item value="kzn" text="Kazan"></div>
|
|
282
|
+
</div>
|
|
283
|
+
</div>`
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
A date field, `<div so-date-input>`, is a group of parts in the format of the subtree's locale —
|
|
287
|
+
day, month and year, with `kind="datetime"` also the time; the form gets the date as a string. A
|
|
288
|
+
calendar, `<div so-calendar>`, draws its month grids itself — a day is a cell,
|
|
289
|
+
`<td so-calendar-item>` — with one pair of paging buttons and a month and year picker under each
|
|
290
|
+
month title. `<div so-date-picker>` is a date field, or two for a range, with a calendar in a panel
|
|
291
|
+
anchored to the element and moved into `body` like a Frame. None of them has a native field to pass
|
|
292
|
+
your markup to, so it all stays on the element you write them on, the `id` of a date field and a
|
|
293
|
+
date picker too. The slots are templates: the calendar's `item` (`let-item`, `let-text`),
|
|
294
|
+
`prev-icon`, `next-icon` and `footer`; the date field's `leading`, `clear` and `trailing`; the date
|
|
295
|
+
picker's `clear`, `trigger-icon` and `item`.
|
|
296
|
+
|
|
297
|
+
```ts
|
|
298
|
+
template: `<div so-date-picker mode="range" aria_label="Trip" (soChangeValue)="trip.set($event.newValue)"></div>
|
|
299
|
+
<div so-calendar [months]="months">
|
|
300
|
+
<ng-template slot="item" let-text="text"><b>{{ text }}</b></ng-template>
|
|
301
|
+
</div>`
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Pagination draws its pages itself — from `total` and `pageSize`, or from a list of `pages` — and
|
|
305
|
+
has no content of its own: a page is an inner part, `<li so-pagination-item>` with a button in it,
|
|
306
|
+
and the `item` slot gives every page your template with the page and its text. A scroller lays its
|
|
307
|
+
content out in its viewport, between the two buttons; a role for the row and what goes with it
|
|
308
|
+
comes to the viewport as `viewportAria`. The arrow icons of both are templates, `prev-icon` and
|
|
309
|
+
`next-icon`:
|
|
310
|
+
|
|
311
|
+
```ts
|
|
312
|
+
template: `<nav so-pagination [total]="200" [value]="page()" aria_label="Result pages" (soChangeValue)="page.set($event.newValue)"></nav>
|
|
313
|
+
<div so-scroller>
|
|
314
|
+
<button so-button text="First"></button>
|
|
315
|
+
<button so-button text="Second"></button>
|
|
316
|
+
</div>`
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
A table takes its columns and rows as data only — `columns` and `items`, or an `engine` — and draws
|
|
320
|
+
its headers (`th[so-table-column]`) and rows (`tr[so-table-row]`) itself: sorting, row selection
|
|
321
|
+
with "select all", column resizing and reordering, a grid mode and a sticky head. The element you
|
|
322
|
+
write it on, `<div so-table>`, is the table's scroll viewport, with the `<table>` inside it, and it
|
|
323
|
+
splits your markup like a field: `class`, `style` and hiding stay on the viewport, while everything
|
|
324
|
+
else you write on it (`aria-label`, `id`, `data-*`, bindings and listeners) goes to the `<table>`,
|
|
325
|
+
which it names, next to the classes and ARIA of the table. A header and a cell are filled through
|
|
326
|
+
templates — a template given to the table renders in every header or cell — and an empty table
|
|
327
|
+
through the `empty` slot:
|
|
328
|
+
|
|
329
|
+
```ts
|
|
330
|
+
template: `<div so-table [items]="rows" [columns]="columns" mode="multiple" aria-label="Staff" (soColumnResize)="save($event)">
|
|
331
|
+
<ng-template slot="cell" let-column="column" let-value="value">…</ng-template>
|
|
332
|
+
<p slot="empty">No data</p>
|
|
333
|
+
</div>`
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Without a `cell` template a value is rendered as the cell's text from the core (`cell.text`) — an
|
|
337
|
+
object as JSON; the `soText` pipe (`TextPipe`) gives your own template the same text. A long list renders only
|
|
338
|
+
what is visible inside `<ng-container so-virtual>` — the same with a table; the window renders no
|
|
339
|
+
element of its own, so the layout around the list stays as it is, and `enabled` turns the window
|
|
340
|
+
off:
|
|
341
|
+
|
|
342
|
+
```ts
|
|
343
|
+
template: `<ng-container so-virtual><div so-list-box [items]="cities"></div></ng-container>`
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
A panel over the page is a Frame. The element you write is the panel, and it is that element which
|
|
347
|
+
moves into `target` (`body` by default) once the page has rendered in the browser — with your
|
|
348
|
+
`class`, attributes and event listeners on it. Its content keeps the context of the place it is
|
|
349
|
+
written in, such as the language of the surrounding `so-locale-provider`. Like a CDK overlay, the
|
|
350
|
+
panel exists only in the browser: on the server, and while `target` matches nothing, there is no
|
|
351
|
+
panel — the element you wrote stays in place, empty and hidden. `contained` keeps the panel in
|
|
352
|
+
place, positioned in its nearest positioned ancestor.
|
|
353
|
+
|
|
354
|
+
```ts
|
|
355
|
+
template: `<div so-frame [visible]="open()" [x]="10" [y]="20" class="app-menu">…</div>`
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
Angular inserts and moves nodes relative to the ones it rendered, so a Frame must not be the first
|
|
359
|
+
node of an `@for` item that gets items inserted before it or reordered — Angular fails with
|
|
360
|
+
NG05106. Wrap it in an element: `@for (item of items; track item.id) { <section><div so-frame>…`.
|
|
361
|
+
The panel follows its place. When Angular takes the place off the page while the component lives
|
|
362
|
+
on — a Frame inside a conditional `<ng-content>` of another component, or in a detached view — the
|
|
363
|
+
panel leaves the page with it, and when the place comes back, the panel is back in `target`.
|
|
364
|
+
|
|
365
|
+
A popover and a tooltip are written on your element, `<span so-popover>` and `<span so-tooltip>`,
|
|
366
|
+
which wraps the trigger and anchors their panel — a Frame of their own, moved into `body`. The
|
|
367
|
+
trigger is a template, `<ng-template slot="trigger">`: its context carries what links the trigger
|
|
368
|
+
to the panel — `triggerAria` (`aria-expanded`, `aria-controls` or the tooltip's `aria-describedby`)
|
|
369
|
+
and, for a popover, `triggerDataset` with the pressed look — and you put it on your own element
|
|
370
|
+
with the `[so-attributes]` directive. Import `SlotDirective` and `AttributesDirective` along with
|
|
371
|
+
the component. The trigger stays the same node as the panel opens and closes, so focus stays on
|
|
372
|
+
it. The content of a popover is its panel's content, next to a close button (`closable`, the
|
|
373
|
+
`close-icon` template); the content of a tooltip is its text. A popover takes its content as is
|
|
374
|
+
or as a template, `<ng-template slot="default">`, and a given template is drawn instead. Write it
|
|
375
|
+
as a template when it should not be created right away: the popover creates the template itself,
|
|
376
|
+
and with `lazyMount` its components come with the first open of the panel. Content written as is
|
|
377
|
+
Angular creates right away, with the template that wrote it, and `lazyMount` only keeps it out of
|
|
378
|
+
the document until the first open. `rendered` set to `false` hides the panel along with your
|
|
379
|
+
element. Like a Frame, the panel exists only in the browser: on the server it is an empty hidden
|
|
380
|
+
element inside yours, and the trigger already links to it.
|
|
381
|
+
|
|
382
|
+
```ts
|
|
383
|
+
template: `<span so-popover aria_label="Filters" lazyMount (soChangeOpen)="opened.set($event.newValue)">
|
|
384
|
+
<ng-template slot="trigger" let-triggerAria="triggerAria" let-triggerDataset="triggerDataset">
|
|
385
|
+
<button so-button text="Filters" [so-attributes]="{ ...triggerAria, ...triggerDataset }"></button>
|
|
386
|
+
</ng-template>
|
|
387
|
+
<ng-template slot="default">…</ng-template>
|
|
388
|
+
</span>
|
|
389
|
+
<span so-tooltip>
|
|
390
|
+
<ng-template slot="trigger" let-triggerAria="triggerAria">
|
|
391
|
+
<button so-button text="Save" [so-attributes]="triggerAria"></button>
|
|
392
|
+
</ng-template>
|
|
393
|
+
Save a draft
|
|
394
|
+
</span>`
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
A menu is written on your element too, `<span so-menu>`, with its trigger template as in a popover
|
|
398
|
+
— a button, or with `openOn="contextmenu"` the area it is called over by a right click, a long
|
|
399
|
+
touch or Shift+F10, and then the panel opens at that point. The items are parts on your own
|
|
400
|
+
elements: `<div so-menu-item>` (an action — `(soChoose)` on the menu tells which one was chosen),
|
|
401
|
+
`<div so-menu-checkbox-item>`, `<div so-menu-radio-item>` inside `<div so-menu-radio-group>`
|
|
402
|
+
(the group's `[value]` and `(soChangeValue)`), `<div so-menu-group label="…">`,
|
|
403
|
+
`<div so-menu-separator>`, `<div so-menu-submenu>` with items of its own and
|
|
404
|
+
`<div so-menu-popover-item>` with any content in its own side panel — as in a popover, a
|
|
405
|
+
`<ng-template slot="default">` with `lazyMount` is created with the first open of the panel, and
|
|
406
|
+
the row text is the `header` template. A checkbox or a radio item is
|
|
407
|
+
checked with `[selected]` and `(soChangeSelected)`. The items of a menu and of a submenu are written
|
|
408
|
+
as is or as a template, `<ng-template slot="default">`, and a given template is drawn instead.
|
|
409
|
+
Write them as a template when they should not be created right away — say, a menu in every row of a
|
|
410
|
+
table: the menu creates the template itself, with the first open of its panel, and the items stay
|
|
411
|
+
until the menu is removed. Items written as is Angular creates with the menu, and the menu only
|
|
412
|
+
keeps them out of the document until the first open. Without items in the markup, the menu draws them
|
|
413
|
+
from `[items]`: an item with nested `items` is a submenu, groups come from `_.group` or `groupBy`,
|
|
414
|
+
and the item slots are templates — `item`, `item-leading`, `item-trailing`, `item-header` and
|
|
415
|
+
`item-arrow-icon`, with the item in the context; a submenu from data gets them under the same names.
|
|
416
|
+
The `item-content` template is the panel of a submenu from data: write menu items in it, and they
|
|
417
|
+
belong to the submenu it is drawn in, not to the menu you wrote it in. Given, it replaces the items
|
|
418
|
+
from data in every submenu from data, even where it draws nothing. A group, a radio group or a
|
|
419
|
+
submenu written inside the template keeps its own items.
|
|
420
|
+
|
|
421
|
+
```ts
|
|
422
|
+
template: `<span so-menu (soChoose)="run($event.value)">
|
|
423
|
+
<ng-template slot="trigger" let-triggerAria="triggerAria" let-triggerDataset="triggerDataset">
|
|
424
|
+
<button so-button text="Actions" [so-attributes]="{ ...triggerAria, ...triggerDataset }"></button>
|
|
425
|
+
</ng-template>
|
|
426
|
+
<div so-menu-item value="edit" text="Edit"><span slot="trailing">Ctrl+E</span></div>
|
|
427
|
+
<div so-menu-submenu value="share" text="Share">
|
|
428
|
+
<ng-template slot="default">
|
|
429
|
+
<div so-menu-item value="link" text="Link"></div>
|
|
430
|
+
<div so-menu-item value="mail" text="Mail"></div>
|
|
431
|
+
</ng-template>
|
|
432
|
+
</div>
|
|
433
|
+
<div so-menu-popover-item value="due" text="Due date" lazyMount>
|
|
434
|
+
<ng-template slot="default"><div so-calendar></div></ng-template>
|
|
435
|
+
</div>
|
|
436
|
+
<div so-menu-separator></div>
|
|
437
|
+
<div so-menu-item value="remove" text="Delete"></div>
|
|
438
|
+
</span>`
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
A modal window and a sliding panel are written on your element, `<div so-dialog>` and
|
|
442
|
+
`<div so-drawer>`, and that element is the panel: it moves into `target` (`body` by default) with
|
|
443
|
+
your `class`, attributes and listeners, while a drawer with `contained` stays in place, positioned
|
|
444
|
+
in its nearest positioned ancestor. The backdrop is placed right before the panel by the component
|
|
445
|
+
itself — in the target and in place — and hides with it. Whether the panel is open is `visible`:
|
|
446
|
+
bind it and listen to `(soChangeVisible)`. The close button, Escape, a press on the backdrop and a
|
|
447
|
+
drawer's swipe ask the panel to close, and `(soCloseBefore)` can refuse with
|
|
448
|
+
`$event.preventDefault()`. The title and the footer are slots by the `slot` attribute
|
|
449
|
+
(`<span slot="title">`, `<button so-button slot="footer">`), the rest of the content is the body;
|
|
450
|
+
the icons of the header buttons are templates — `close-icon`, and for a dialog with `maximizable`
|
|
451
|
+
also `maximize-icon` and `restore-icon`. Like a Frame, the panel exists only in the browser: on the
|
|
452
|
+
server it is your element, empty and hidden, and inside `@for` it needs a wrapping element.
|
|
453
|
+
|
|
454
|
+
```ts
|
|
455
|
+
template: `<button so-button text="Settings" (click)="open.set(true)"></button>
|
|
456
|
+
<div so-dialog [visible]="open()" (soChangeVisible)="open.set($event.newValue)">
|
|
457
|
+
<span slot="title">Settings</span>
|
|
458
|
+
…
|
|
459
|
+
<button so-button slot="footer" text="Done" (click)="open.set(false)"></button>
|
|
460
|
+
</div>`
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
Toasts are shown from code. `<div so-toasts>` wraps the application and has no box of its own: the
|
|
464
|
+
application inside it stays in place, and the region with the toasts moves to `target` (`body` by
|
|
465
|
+
default) in the browser — with everything you wrote on the `div`: `class`, `style`, attributes,
|
|
466
|
+
`[class.x]`, `[style.x]`, `[attr.x]` and listeners. A component under the manager takes the queue
|
|
467
|
+
with `useToasts()` in a field or its constructor, and `show` takes a text or options and returns the
|
|
468
|
+
toast — the core instance you can change or close. Content of your own (`render`) is a component
|
|
469
|
+
class with a `toast` input. The `item`, `item-icon`, `item-content` and `item-close-icon` slots are
|
|
470
|
+
templates with the toast in their context. Like the other layers, the region exists only in the
|
|
471
|
+
browser; the application inside the manager renders on the server and hydrates as usual.
|
|
472
|
+
|
|
473
|
+
```ts
|
|
474
|
+
@Component({
|
|
475
|
+
selector: 'app-undo',
|
|
476
|
+
imports: [TButtonComponent],
|
|
477
|
+
template: `<button so-button text="Undo" (click)="toast().close()"></button>`,
|
|
478
|
+
})
|
|
479
|
+
export class Undo {
|
|
480
|
+
readonly toast = input.required<IToastsItem>()
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
@Component({
|
|
484
|
+
selector: 'app-save',
|
|
485
|
+
imports: [TButtonComponent],
|
|
486
|
+
template: `<button so-button text="Save" (click)="save()"></button>`,
|
|
487
|
+
})
|
|
488
|
+
export class Save {
|
|
489
|
+
private readonly toasts = useToasts()
|
|
490
|
+
|
|
491
|
+
save(): void {
|
|
492
|
+
this.toasts.show({ text: 'Draft saved', type: 'success', render: Undo })
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// In the application template:
|
|
497
|
+
// <div so-toasts placement="bottom-end"><router-outlet /></div>
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
The language and the strings the library renders itself come from the nearest
|
|
501
|
+
`LocaleProviderDirective` above — it can sit on an `ng-container`, and changing `locale` updates
|
|
502
|
+
the components without re-creating them:
|
|
503
|
+
|
|
504
|
+
```ts
|
|
505
|
+
template: `<ng-container so-locale-provider [locale]="locale()"><router-outlet /></ng-container>`
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
The application plugs in a theme and an icon pack once, in its entry point:
|
|
509
|
+
|
|
510
|
+
```ts
|
|
511
|
+
import { useIcons, useTheme } from '@soxa-ui/setup'
|
|
512
|
+
import * as material from '@soxa-ui/icons-material'
|
|
513
|
+
import oren from '@soxa-ui/theme-oren/setup'
|
|
514
|
+
|
|
515
|
+
useIcons(material)
|
|
516
|
+
useTheme(oren)
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
The theme's stylesheet goes into the global styles, `src/styles.css` — the file listed in `styles`
|
|
520
|
+
of `angular.json`:
|
|
521
|
+
|
|
522
|
+
```css
|
|
523
|
+
@import '@soxa-ui/theme-oren';
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
Not an `import` in `main.ts`: Angular CLI puts CSS imported from a module into a chunk the page
|
|
527
|
+
never loads, and the components stay unstyled. The values of the theme, such as
|
|
528
|
+
`variant="accent"`, reach the template type check with `@soxa-ui/theme-oren/setup`.
|
|
529
|
+
|
|
530
|
+
The `initial` budget of Angular CLI counts the raw bytes of the initial JavaScript and the global
|
|
531
|
+
styles, and a project made by `ng new` fails the build above 1 MB. `@soxa-ui/theme-oren` holds the
|
|
532
|
+
CSS of every component — 361 kB raw, 36 kB gzip — and a page with a Button, an Input, a Select, a
|
|
533
|
+
Dialog, a Table and a DatePicker adds 677 kB of JavaScript (169 kB gzip): over the threshold. Import
|
|
534
|
+
the stylesheets of the components your pages use instead — each one with the shared part of the
|
|
535
|
+
theme:
|
|
536
|
+
|
|
537
|
+
```css
|
|
538
|
+
@import '@soxa-ui/theme-oren/button.css';
|
|
539
|
+
@import '@soxa-ui/theme-oren/dialog.css';
|
|
540
|
+
@import '@soxa-ui/theme-oren/input.css';
|
|
541
|
+
@import '@soxa-ui/theme-oren/select.css';
|
|
542
|
+
@import '@soxa-ui/theme-oren/table.css';
|
|
543
|
+
@import '@soxa-ui/theme-oren/date-picker.css';
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
The same page then loads 205 kB of CSS, 882 kB in all, within the budget (the theme's README,
|
|
547
|
+
[Only the components you use](https://github.com/youra-h/soxa-ui/tree/main/packages/themes/oren#only-the-components-you-use)).
|
|
548
|
+
|
|
549
|
+
## Documentation
|
|
550
|
+
|
|
551
|
+
- [Soxa UI README](https://github.com/youra-h/soxa-ui#readme) — the component set and what each
|
|
552
|
+
adapter implements so far.
|
|
553
|
+
- [Architecture overview](https://github.com/youra-h/soxa-ui/blob/main/docs/architecture.md) —
|
|
554
|
+
layers, descriptors, plugins, collections, adapters.
|
|
555
|
+
|
|
556
|
+
## Public API
|
|
557
|
+
|
|
558
|
+
The package entry exports application components, their consumer types, the locale provider,
|
|
559
|
+
and the supported icon and toast helpers. Component props, slots, events, refs and core models
|
|
560
|
+
passed through `ctrl` remain the supported ways to customize the UI. Models come from
|
|
561
|
+
`@soxa-ui/core`; theme, icon and external-plugin registration comes from `@soxa-ui/setup`.
|
|
562
|
+
|
|
563
|
+
Assembly functions, adapter profiles, bindings, generated metadata and rendering templates
|
|
564
|
+
are private implementation details. They are no longer exported from the package entry, and
|
|
565
|
+
there is no public `/internal` entry. Code that imported them must use the component contract
|
|
566
|
+
instead. Building a new adapter component from Soxa's assembly helpers is not a supported
|
|
567
|
+
public SDK.
|
|
568
|
+
|
|
569
|
+
`SlotDirective`, `AttributesDirective`, `RoleIconDirective`, `TextPipe`, `LocaleProviderDirective` and `provideAttrs` are application
|
|
570
|
+
API. The `ɵSubtreeDirective` and `ɵValueAccessorDirective` exports are required by Angular AOT
|
|
571
|
+
and are private; applications must not import them.
|
|
572
|
+
|
|
573
|
+
## License
|
|
574
|
+
|
|
575
|
+
[MIT](https://github.com/youra-h/soxa-ui/blob/main/LICENSE)
|
|
576
|
+
|
|
577
|
+
## Two-way bindings
|
|
578
|
+
|
|
579
|
+
Writable inputs with change triggers expose a typed `<inputName>Change` output without `so`:
|
|
580
|
+
|
|
581
|
+
```html
|
|
582
|
+
<div so-input [(value)]="text"></div>
|
|
583
|
+
<label so-check-box [(value)]="checked"></label>
|
|
584
|
+
<div so-list-box [(value)]="selection"></div>
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
Model outputs emit the current property value. Ordinary events keep their `so` prefix and event
|
|
588
|
+
payload: `(valueChange)="text = $event"` receives the value, while `(soChangeValue)` receives
|
|
589
|
+
`{ oldValue, newValue }`. Namespaced plugin inputs follow the same rule, for example
|
|
590
|
+
`[(aria_label)]="label"`. Read-only properties have no model output.
|
|
591
|
+
|
|
592
|
+
## Angular Forms
|
|
593
|
+
|
|
594
|
+
All components with a writable `value` input support Angular Forms automatically. Install
|
|
595
|
+
`@angular/forms` at the same version as `@angular/core`. Import `FormsModule` for `ngModel`,
|
|
596
|
+
or `ReactiveFormsModule` for `formControl` and `formControlName`, alongside the Soxa components:
|
|
597
|
+
|
|
598
|
+
```html
|
|
599
|
+
<div so-input [(ngModel)]="name"></div>
|
|
600
|
+
<label so-check-box [formControl]="accepted"></label>
|
|
601
|
+
<form [formGroup]="profile">
|
|
602
|
+
<div so-input formControlName="name"></div>
|
|
603
|
+
</form>
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
`writeValue` updates the component without echoing a form change. User changes update the
|
|
607
|
+
form, form disabled state reaches the component, and leaving the control marks it touched.
|
|
608
|
+
Focus moves between nodes inside the control do not mark it touched. Angular's
|
|
609
|
+
`updateOn: 'blur'` and `updateOn: 'submit'` keep their usual behavior.
|
|
610
|
+
|
|
611
|
+
Angular's `null` reset value clears the component to the descriptor default (for example,
|
|
612
|
+
undefined for Input, false for CheckBox, or zero for Slider); the form retains `null`.
|
|
613
|
+
A non-nullable FormControl resets to its own initial value. Use one form binding per control;
|
|
614
|
+
`[(value)]` remains available for components managed without Angular Forms.
|