Osy#betaRun it on Osyrin: osyrin.com

Reference / UI

[Slot] and contributions — a place on a page that other kits, and the app, add to

[Slot] public abstract component X(params); · [Contribution("Label", Icon = Icons.Y, Order = n)] component C(params) : X { … } · X(args) · X(args, only: c) · Contributions<X>() · use Kit { without C; } · [Replaces(C)]

A slot is a place on a page others add to. A kit marks one (`[Slot] public abstract component QuotePanel(Quote quote);`), and every component that writes `: QuotePanel` and takes exactly its parameters — in any kit, or the app — is drawn where a page draws `QuotePanel(quote)`, in one order, each only for a reader its `[Authorize]` admits. The app has the last word: `without` leaves one out, `[Replaces]` swaps one, and a component of the same name takes one's place.

preview3 examples compiled by CIuicompositionkitslots

Summary#

A slot is a place on a page that other kits — and the app — add to. The kit that owns the page declares it once, draws it where it belongs, and never learns who fills it:

[Slot] public abstract component QuotePanel(Quote quote);       // the kit: "others may add to a quote's page here"

[Contribution("Approvals", Icon = Icons.Check, Order = 20)] [Authorize(ApprovesQuotes)]
component QuoteApprovals(Quote quote) : QuotePanel { render { … } }   // any kit, or the app

QuotePanel(quote);                                               // the page: every contribution, in order

It is a lowering, not a lookup: the compiler writes the slot out as its contributions, so a page that draws a slot costs what the same components written by hand cost — no request of its own, nothing looked up when the page runs.

Signature#

[Slot] public abstract component X(<params>);                    // the slot — no body; its parameters are the contract
[Slot] public abstract component RecordPanel<T>(T record) where T : entity;   // a slot for ANY record (Osysharp.Ui ships two)

[Contribution("Words", Icon = Icons.Y, Order = 20)]              // all three optional: words default to its name in words,
component C(<exactly X's params>) : X { render { … } }           //   Order to 100
[Order(20)]                                                      // the same place, said on its own
component Files<T>(T record) : RecordPanel<T> { … }              // for every record type
component QuoteOnly(Quote record) : RecordPanel<Quote> { … }     // for one

X(args);                                                         // draw every contribution, in order
X(args, only: c);                                                // draw one — `c` a SlotContribution
Contributions<X>()                       → List<SlotContribution>   // Key, Label, Icon, Order — what this reader is shown

use Acme.Kit@0 { without C; }                                    // app.osy: leave a package's contribution out
[Replaces(C)] component Mine(<X's params>) : X { … }             // the app's own, in C's place

Description#

In what order are they drawn?#

One order for the whole composition: Order (or [Order]) first, lower first, 100 when none is said; then the package order — a kit before the kits built on it, the app last; then the full name. It never depends on file order or on which kit compiled first, so two compiles of one composition draw the same page. Kits leave gaps (10, 20, …) so an app can put its own between them.

Who sees a contribution?#

A contribution marked [Authorize(P)] is drawn only for a reader P admits, and Contributions<X>() lists it only for them; with no [Authorize] it is drawn for everyone who sees the page. Everything a contribution READS is read as its reader, like any component's — so one contribution on a shared page shows each reader their own rows.

How does a page lay them out as tabs?#

Draw the slot with only: and the list beside it. Contributions<X>() is the reader's list, in order — each with the Key that only: matches, its Label and its Icon.

The list is written out where you call it — no request — so a [Render(SSR)] page that lists contributions is painted by the server with the list already in it, as the browser then draws it. (A contribution with an [Authorize] makes the list depend on the reader; such a page is painted in the browser instead, which already knows who is reading.)

What can the app change?#

  • Leave one out — use Acme.Kit@0 { without QuoteHistory; } in app.osy. A without that names nothing is refused with the package's whole list. A class closed for a subtype is named with it: without WorkSource<PurchaseRequest>;.
  • Swap one — [Replaces(QuoteHistory)] on the app's own contribution to the same slot: drawn in its place, with its words unless the app's say others. Only the app replaces — a kit cannot change what another kit adds.
  • Take one over by name — an app component with a contribution's NAME is drawn instead of the kit's, everywhere.
  • Restyle them — contributions paint with theme tokens, so the app's theme repaints them (theme tokens).

What if a replacement drops a slot?#

A component that takes another's place — an app component of a kit component's name, a [Replaces], or the app's design of a kit's look contract — keeps the slots the one it replaces drew. Dropping a slot that has contributions is refused, naming each contribution that would have nowhere to land:

'MyFrame' takes the place of the package's design Acme.Kit.KitBareFrame, which draws the slot Greeting — and 'MyFrame' does not, so its 2 contributions would have nowhere to land: KitA(Acme.Kit) andAppE(this app). DrawGreeting(…)in 'MyFrame', or leave them out withwithouton the package'suse line.

What does every record's page offer?#

Osysharp.Ui declares RecordPanel<T> and RecordBadge<T>. A kit that works on any record (its files, its activity) contributes once with an open contribution; one for a single type writes the type. A page draws RecordPanel(record).

What is refused?#

A contribution that does not take exactly its slot's parameters (names, types, order) — named with the shape it must take; [Contribution] on a component whose base is not a slot — listing the slots in sight; a slot with a body; a [Replaces] in a package, or naming nothing; Contributions<X>() for something that is not a slot.

Examples#

[Principal] entity User { [Required, MaxLength(200)] string Email; security { allow read when IsAuthenticated; } }
[Role] enum AppRole { Member, Approver }
[RoleGrant] entity RoleGrant { [Required] User Grantee; [Required] AppRole Level; security { allow read when IsAuthenticated; } }
policy Approves => RoleGrant.Any(g => g.Grantee == user && g.Level == AppRole.Approver);

entity Quote {
  [Required, MaxLength(100)] string Title;
  security { allow read, create when IsAuthenticated; }
}

/// A section on a quote's page — others add to it.
[Slot] abstract component QuotePanel(Quote quote);

[Contribution("Lines", Order = 10)]
component QuoteLines(Quote quote) : QuotePanel { render { Text("Lines of " + quote.Title); } }

[Contribution("Approvals", Icon = Icons.Check, Order = 20)] [Authorize(Approves)]
component QuoteApprovals(Quote quote) : QuotePanel { render { Text("Approve " + quote.Title); } }

[Contribution("History")]
component QuoteHistory(Quote quote) : QuotePanel { render { Text("History of " + quote.Title); } }

[Route("/q/{id}")]
[Render(CSR)]
component QuotePage(Guid id) {
  live var q = Quote.FirstOrDefault(x => x.Id == id);
  SlotContribution? shown = null;
  action Show(SlotContribution? c) { shown = c; }
  render {
    if (q != null) {
      Stack(gap: 3) {
        Tabs {
          Tab("All", selected: shown == null, onPress: () => Show(null));
          foreach (var c in Contributions<QuotePanel>()) {
            Tab(c.Label, selected: shown != null && shown.Key == c.Key, onPress: () => Show(c));
          }
        }
        QuotePanel(q, only: shown);
      }
    }
  }
}
[Contribution("Files", Order = 50)]
component AnyRecordFiles<T>(T record) : RecordPanel<T> { render { Text("Files"); } }

[Contribution("Pricing")]
component QuotePricing(Quote record) : RecordPanel<Quote> { render { Text("Pricing for " + record.Title); } }

[Route("/record/{id}")]
[Render(CSR)]
component QuoteRecordPage(Guid id) {
  live var q = Quote.FirstOrDefault(x => x.Id == id);
  render { if (q != null) { RecordPanel(q); } }
}
[Replaces(QuoteHistory)]
component QuoteTrail(Quote quote) : QuotePanel { render { Text("Every change to " + quote.Title); } }

See also#

Related

WorkspaceShell, WorkspaceFrame and [Nav] — a kit's pages in your app's shell and navigation

A kit's working pages (a knowledge base's desk, its review queue) name `[Layout(WorkspaceFrame)]`, and the settings…

Looks — a site's whole design, picked per version

A LOOK is a site's whole design — its frame, its pages, its theme and fonts, its design of every block type — declared…

[Composable] — presentational components in public pages

Mark a presentational, composition-only component `[Composable]` so a public page can compose it without marking it…

page authorization (policies)

A `policy` names a reusable authorization predicate over the current user — e.g. `policy Admins => UserRole.Any(r =>…

App.Implementations<T>()

One new instance of every class in the app's composition — its own and every package's — that implements the interface…

[Action] — what an app offers to be done, by name

Mark a function `[Action]` and the app offers it to be done BY NAME — by a rule, a macro, a tool — with everything a…