# Concorde — full documentation (plain text) > Mirror for AI indexers. Each page is a section below. > HTML URLs use /crawl/**/*.html — never .md under /crawl/. ## Topic hubs - Concorde decorators reference: https://concorde.supersoniks.org/crawl/hubs/decorators.html (hubs/decorators) - Fetching remote data in Concorde: https://concorde.supersoniks.org/crawl/hubs/remote-data.html (hubs/remote-data) - Concorde form components: https://concorde.supersoniks.org/crawl/hubs/form-components.html (hubs/form-components) --- ## Date - URL: https://concorde.supersoniks.org/crawl/core/components/functional/date/date.html - Doc ID: core/components/functional/date/date - Keywords: Concorde, supersoniks, core/components/functional/date/date, date, Date, functional component, sonic-date, web component Date No attribute Nothing is displayed. Now Displays the current date. Date Displays a date from a timestamp. Datestring Displays a date from a string. Startdate / enddate Displays a period of time from startdate to enddate. When by itself startdate will act as date if a startdate is not specified it will be set by the current date. if the enddate is anterior to start date date it will be used as the startdate. Startdatestring / enddatestring Wordingbilletperiodevalidite Weekday narrow : short (default) : long : hidden : Day numeric : 2-digit (default) : Month numeric : 2-digit : narrow : short (default) : long : Year numeric (default) : 2-digit : Hour numeric : 2-digit (default) : hidden : Minute numeric : 2-digit (default) : hidden : Language ISO 639-1 Language Codes fr : en : zh : ja : Timezone For the list of supported timeZones, run Intl.supportedValuesOf('timeZone') in the console Europe/London : Europe/Paris : America/Tijuana : Era narrow : short : long : .renderIf Deprecated True statement : 2" now> False statement : DesignMode --- ## Fetch - URL: https://concorde.supersoniks.org/crawl/core/components/functional/fetch/fetch.html - Doc ID: core/components/functional/fetch/fetch - Keywords: Concorde, supersoniks, core/components/functional/fetch/fetch, fetch, Fetch, functional component, sonic-fetch, web component, serviceURL="/docs-mock-api", {my:{data:{a:1,b:2}}}, key="my.data", {a:1 , b:2} Fetch > New apps: prefer @get for a typed GET on a component, or List / Queue with fetch for collections. Use Local API demos (serviceURL="/docs-mock-api") to try examples offline. The sonic-fetch component requests and stores API data. It extends the Fetcher and Subscriber mixins. Basic usage In order to work properly the sonic-fetch component needs at least the following attributes. - serviceURL : A base service url. This attribute can be inherited from an ancestor. ex : /docs-mock-api - endPoint : the specific location where requests for information are sent (see the api docs). ex : api/users | api/users?page=2 | api/users/2 - dataProvider (Required) : An ID that is used as a reference to the object storing the data returned by the API. This attribute can be inherited from an ancestor. Hover to see the data DataProvider as an endPoint If no endPoint is specified it will be filled by the dataProvider ID instead Hover to see the data HeadersDataProvider Deprecated Key When the key attribute is present, only a sub-part of the data received is injected into the dataProvider. We can use the dot syntax to target what we want to keep. For example if the data is {my:{data:{a:1,b:2}}} and the key is key="my.data", the data available in the dataProvider will be {a:1 , b:2} dataProvider object data.id data.firstname data.lastname data.email Text mode if the mime type of the content returned by the service begins with text/, then the dataProvider has a key named "text" which contains the text returned by the service. Hover to see the data NoLoader The noLoader attribute disables display of the default loader Basic fetch with noLoader attribute --- ## if - URL: https://concorde.supersoniks.org/crawl/core/components/functional/if/if.html - Doc ID: core/components/functional/if/if - Keywords: Concorde, supersoniks, core/components/functional/if/if, if, functional component, sonic-if, web component, .condition, @subscribe, formDataProvider, docs-lit-demo if The sonic-if component shows its slot when .condition is true. In Lit, bind .condition from store-driven state (@subscribe on a formDataProvider field) — live preview and TypeScript source are the same file (docs-lit-demo): Plain HTML without Lit: HTML integration. --- ## List - URL: https://concorde.supersoniks.org/crawl/core/components/functional/list/list.html - Doc ID: core/components/functional/list/list - Keywords: Concorde, supersoniks, core/components/functional/list/list, list, List, functional component, sonic-list, web component, serviceURL="/docs-mock-api", dataProvider="api/users", key="data", props, dataProvider, fetch, serviceURL, key, items List > Try offline: serviceURL="/docs-mock-api" and dataProvider="api/users" with key="data" — see Local API demos. Recommended patterns: Data flow. The sonic-list component renders one row per entry in props (array from fetch or set on the element). List extends Subscriber and Fetcher: Subscriber — props + dataProvider Fetcher — optional fetch + serviceURL / key (see Fetch) Row renderer (items) — recommended From a Lit parent, pass a function on the items property (ListItems). Each row is wrapped in a sonic-subscriber with dataProvider="…/list-item/n" (hover rows with debug). private items = ({ firstname, lastname, email, avatar }) => html ${firstname} ${lastname} ${email} ; html ; Use .items=${fn} (property binding): Lit passes functions only as properties, not as HTML attributes — @property({ type: Function }) does not change that. Same for .noItems, .separator, .skeleton. The callback receives each row object (replacing data-bind / in a ). Live demo + TypeScript source (one file, no Markdown copy): Implementation: src/docs/example/docs-users-list.ts — row markup in the items callback (item.firstname, …), same idea as replacing data-bind / in a . Alternating row layouts Use metadata (even, odd, firstChild, …) or fields on each item (e.g. tpl with templateKey): Separator and empty list Fetch + extractValues HTML children (integration without Lit) For plain HTML hosts, you can still declare children (and data-value for templateKey, separator, no-item). That path is for HTML integration — not used in Concorde doc live demos. Each list row still gets dataProvider="[list]/list-item/[index]". Additionnal tips If the request returns an object, it is wrapped in an array (unless extractValues is set). Call invalidate() on the list publisher to reload fetch data. Each row publisher exposes parent pointing at the list publisher. --- ## Mix - URL: https://concorde.supersoniks.org/crawl/core/components/functional/mix/mix.html - Doc ID: core/components/functional/mix/mix - Keywords: Concorde, supersoniks, core/components/functional/mix/mix, mix, Mix, functional component, sonic-mix, web component Mix Mix allows you to mix several subsets of dataProvider in a new key/value structure which is itself associated with a new dataProvider. The data update is then bidirectional. Dot notation is supported to extract a sub-part of the data. For example, if we declare dataproviders as follows : We can rearrange the data as follows Value of dataToMixA.foo.bar and mixedData.orThat : Value of dataToMixB.baz and mixedData.either.baz : Then we can change values in both dataProviders programaticaly this way, they will stay in sync SonicPublisherManager.get("mixedData").either.baz=6; SonicPublisherManager.get("dataToMixB").baz=8; SonicPublisherManager.get("dataToMixA").foo.bar=8; SonicPublisherManager.get("mixedData").orThat=6; Or by using a form element --- ## Queue - URL: https://concorde.supersoniks.org/crawl/core/components/functional/queue/queue.html - Doc ID: core/components/functional/queue/queue - Keywords: Concorde, supersoniks, core/components/functional/queue/queue, queue, Queue, functional component, sonic-queue, web component, serviceURL="/docs-mock-api", .items, dataProvider, …/list-item/0, …/1, dataProviderExpression, $offset, $limit, lazyload, dataFilterProvider Queue > Try offline: serviceURL="/docs-mock-api" — see Local API demos. Row rendering: Data flow (.items property binding). sonic-queue loads data in batches. Each batch is an internal List with its own dataProvider (…/list-item/0, …/1, …). | Mechanism | Role | |-----------|------| | dataProviderExpression | API path template; $offset and $limit are replaced per batch | | lazyload | Load the next batch when the user scrolls near the end | | dataFilterProvider | Publisher id of a form (formDataProvider); field values are merged into the request query string | | filteredFields | Optional list of form field names to exclude from the query (space-separated) — omit when every field should be sent | | .items, .noItems, .separator, .skeleton | Lit callbacks forwarded to each batch list (use the dot — functions are properties, not attributes) | Lazy load — $offset and $limit When the expression contains $offset and $limit, the queue: 1. Fetches the first batch with offset=0 (or the initial offset attribute) and perpage=$limit. 2. On scroll, appends a batch with offset increased by the previous batch size. 3. Stops when a batch returns fewer rows than limit (or none). The doc mock implements this on GET /docs-mock-api/api/users?offset=…&perpage=… (paginateUsers in src/docs/mock-api/router.ts). Wrap the queue in a fixed height with overflow-y-auto so lazy load triggers when scrolling inside the box (same layout as the TS starter demo-queue-templates). Try scrolling after load — batches of 4 users. Search e.g. George, Bluth, or zzz for empty results. Expression example html ; Without $offset in the expression, the queue behaves like a single list (one batch), e.g. geo communes below. Filter — dataFilterProvider + q One search field (name="q") on formDataProvider="filter". The queue listens via dataFilterProvider="filter" and, on change: 1. Resets loaded batches. 2. Appends each non-empty form field to the query (here only q — no filteredFields needed). 3. Fetches again from offset 0. Use filteredFields only when the form has extra fields that must not be sent to the API (e.g. filteredFields="rememberMe internalId"). The mock API filters before pagination — same haystack logic as the starter (filterDocsUsers in src/docs/mock-api/fixtures.ts, used by paginateUsers in router.ts / Service Worker): const haystack = ${firstname} ${lastname} ${email}.toLowerCase(); return haystack.includes(q); Simple batch (no lazy scroll) Geo communes: expression uses $limit only (no $offset) — one request, one batch. HTML children Optional for hosts without Lit — see HTML integration. --- ## Router - URL: https://concorde.supersoniks.org/crawl/core/components/functional/router/router.html - Doc ID: core/components/functional/router/router - Keywords: Concorde, supersoniks, core/components/functional/router/router, router, Router, functional component, sonic-router, web component, document.location, .routes, .items, fallback, , pathname + hash, #home, #…/router.md/router, #…/router#home, href Router sonic-router watches document.location (pathname + hash) and renders the matching view. From a Lit parent, pass a .routes map (property binding — same rule as .items on list): keys are path patterns, values are render functions. Use fallback when nothing matches. Legacy HTML remains for hosts without Lit — HTML integration. Static routes (no parameters) Route keys are matched against pathname + hash. A simple hash route #home matches when the location contains that segment. On the doc site, the page URL is already a hash (#…/router.md/router). Demos append a second hash for in-page routes (#…/router#home), use href + autoActive="strict" on sonic-button for the active state, and history.replaceState on click so markdown is not reloaded (setDocsDemoSubHash in src/docs/docs-location.ts). @state() private routes = { "#home": () => html Home , "#about": () => html About , fallback: () => html Not found , }; html Home ; | Key | When it runs | |-----|----------------| | "#home", "#about", … | RegExp or url-pattern test succeeds on current location | | fallback | No other route matched (not an attribute — a key on the same object) | Routes with parameters Two styles (see router.demo.ts and docs-router-params-demo): Url-pattern (:name) Key uses :param segments. The render function receives an object { param: string }. "#couleur/:id": ({ id }) => html Colour id: ${id} , RegExp (capturing groups) Key is a RegExp string with (\d+) / (\w+) groups. The render function receives an array of captured strings (in order). "#products/(\\d+)/(\\w+)": ([productId, slug]) => html Product ${productId}, slug ${slug} , With parameters you usually render data directly in Lit (${id}). The old + dataProviderExpression pattern scoped a dataProvider for data-bind / fetch children — prefer @subscribe or explicit props when using .routes. .routes binding Always use .routes=${…} in Lit templates: route handlers are functions and must be set as properties, not HTML attributes. Optional attributes | Attribute | Role | |-----------|------| | basePath | Prefix for pattern matching (default allows optional leading segments) | | fallBackRoute | If set and no template/route matches, navigates to this URL (redirect). Distinct from routes.fallback which only renders content | sonic-redirect Separate component: redirect when data appears on a publisher (login steps, wizards). Not part of the .routes map. Enter data Delete the data and do a history.back() Example: combine with .routes and submit for login / logout / profile steps. Package demo sonic-router-demo (router.demo.ts) — home, user profile (#user/:id/:slug), products RegExp, fallback 404. --- ## SDUI - URL: https://concorde.supersoniks.org/crawl/core/components/functional/sdui/sdui.html - Doc ID: core/components/functional/sdui/sdui - Keywords: Concorde, supersoniks, core/components/functional/sdui/sdui, sdui, SDUI, functional component, sonic-sdui, web component, Server Driven User Interfaces, props, contents SDUI What is SDUI? SDUI stands for Server Driven User Interfaces. Basically, it generates a user interface based on a JSON SDUI Descriptor. Subscriber extension: SDUI extends the mixin Subscriber. This means that you must set a dataProvider with an id pointing to the publisher that will hold your SDUI Descriptor. This also means that it has a reactive property named props that you can set with the JSON SDUI Descriptor in order to configure it. Fetcher extension: SDUI extends the mixin Fetcher as Fetch and List. In this case, it will parse the JSON coming from the request. The JSON must comply with the SDUI descriptor format. As a fetcher, invalidating the publisher or updating the endpoint will trigger a new fetch and update accordingly. Rendering: It has no shadowdom, and its display style is set to contents, so an empty SDUI has the fewest impact on the layout. SDUI descriptor TypeScript definition: export type SDUIDescriptor = { js?: Array<string>; css?: Array<string>; library?: Record<string, SDUINode>; nodes?: Array<SDUINode>; }; Meaning of the keys: js: An array of URLs pointing to JavaScript files to load. css: An array of URLs pointing to CSS files to load. library: A record of SDUINode definitions intended to be used as a model for other nodes. The keys of the record can be used as the libraryKey of the node. This node will then specialize the model with its own descriptor. nodes: An array of SDUINode descriptors. Each SDUINode will result in an HTMLElement added to the root of the component. SDUINode Descriptor An SDUINode represents an HTMLElement. TypeScript definition: export type SDUINode = { markup?: string; tagName?: string; attributes?: Record<string, string>; nodes?: Array<SDUINode>; innerHTML?: string; prefix?: string; suffix?: string; libraryKey?: string; contentElementSelector?: string; parentElementSelector?: string; }; Summary of properties: markup: Use it if you prefer to define your node entirely using an HTML string. tagName: The tag name of the HTML element. attributes: A string key/value pair storing each attribute name/value of the HTML element created. nodes: An array of SDUINode descriptors. The children of the HTMLElement are created this way. innerHTML: The inner HTML of the current node is defined here if needed. prefix: Use it if you want to wrap the component with some HTML string in conjunction with the suffix. suffix: Use it if you want to wrap the component with some HTML string in conjunction with the prefix. libraryKey: An identifier that points to an SDUINode to be used as a model for this node. contentElementSelector: For SDUINodes defined in the library only. It is a CSS selector that defines where child nodes are added. parentElementSelector: If the current node, as a future child, has a parentElementSelector attribute, it is added to the node corresponding to the associated CSS selector rather than directly to the element. Markup Use it if you prefer to define your node entirely using an HTML string. foo bar !" } ]}' > Tag name Here we use the field tagName of SDUINode to create an element with tag name sonic-input. Attributes A string key/value pair storing each attribute name/value of the HTML element created. As no tag name is defined, a div element is created. Here we define the style attribute to create a tiny red square. Nodes Children of the current node can be added using the field nodes recursively. InnerHTML As no tag name is defined, a div element is created, and then we use innerHTML to add content to it. foo bar !" }] }' > Prefix and suffix Use them if you want to wrap the component with some HTML string. Library key and contentElementSelector The libraryKey of SDUINode is an identifier that points to an SDUINode description in the library to be used as a model for this node. Note that the SDUI has a default library containing some basic component definitions useful for form declaration (see default-library.json). This example uses the library key submit which points to a sonic-submit containing a button also coming from the library. The element named button in the library is a sonic-button of type success containing a check icon as a prefix. Since the submit library element contains an attribute contentElementSelector with the value sonic-button, the "injected content" is put inside the button. We used innerHTML for the sake of simplicity, but you can use nodes to add any complex content. parentElementSelector This special field lets you inject the content at any place in the HTML flow already set inside the parent SDUI component by using a CSS selector. ⚠️ Note that it doesn't work with top-level nodes. Fetch example: Transformers: The transformers let you transform the structure of the SDUI Descriptor into a new one before parsing and rendering it. To enable it, you must fill the transformation attribute of the component with a URL pointing to a Transform descriptor. export type SDUITransformDescription = { library?: Record<string, SDUINode>; transforms: Array<SDUITransformAction>; }; The library: Each key of the library will be merged with the current library in the SDUI Descriptor, by replacing or creating items library key by key. The transform actions: Each transform represents an action that will be done on the SDUI descriptor before the creation of the entire UI. export type SDUITransformAction = { action: SDUITransformActionName; patterns?: Array<SDUINode>; after?: SDUINode; before?: SDUINode; in?: SDUINode; ui?: SDUINode; }; The action that will be done is a verb: export type SDUITransformActionName = "remap" | "unwrap" | "wrap" | "delete" | "insert" | "move"; remap: The properties (tagName, innerHTML, etc.) of the targeted SDUINode will be created/changed according to their presence in the ui (SDUINode) property of the transformAction. unwrap: Replaces the targeted SDUINode in place with its children. wrap: Groups all SDUINodes targeted by the patterns into the new SDUINode described in the ui prop of the transformAction. delete: Deletes the targeted item. insert: Inserts a new SDUINode described by the ui prop into the flow. The position of insertion depends on the presence of the attributes before, after, in, which is a pattern to target an existing SDUINode. Misc Tricks: sduiKey This is an HTML attribute to set on the component if needed. In this case, the component will not treat its props value directly as an SDUI descriptor. Internally, it will basically extract the SDUIDescriptor by doing something like this: const sduiDescriptor = this.props[this.sduikey]. messageKey This is an HTML attribute to set on the component if needed. In this case, the component will automatically create a sonic-toast-message-subscriber component. The data found in props[messageKey] will be treated as the data expected by the sonic-toast-message-subscriber. So, by respecting the format of data supported by this component, you will be able to show multiple toasts in the result of a request. Example of JSON with messageKey=messages: { "messages": [{ "content": "The product has been added to your cart", "status": "success", "type": "public" }] } library This is an HTML attribute to set on the component if needed. You can set a URL pointing to a JSON describing a library with the same structure as the library in the SDUIDescriptor. The component will simply override its library with the given one. Playground Here is a little playground to let you test some simple tricks. --- ## States - URL: https://concorde.supersoniks.org/crawl/core/components/functional/states/states.html - Doc ID: core/components/functional/states/states - Keywords: Concorde, supersoniks, core/components/functional/states/states, states, States, functional component, sonic-states, web component States ### sonic-states displays different states depending on the value of a sub-property of its dataProvider (data-path attribute in dot notation): It loops over its child templates and tests if the regexp contained in the data-value attribute matches the value of the property If so, the content of the corresponding template is displayed as a state. If the attribute dataProviderExpression is provided the content is surrounded by a div: The "dataProvider" attribute of this div is the result of calling the replace function on the property value with the regexp and dataproviderExpression as parameters. The subscribers/fetch... of the template will refer to this dataProvider You can also use url-pattern expressions for the route parameters, see the examples Examples With ma.property= 2 : RegExp data-value = (\d+) and dataproviderExpression = /user/$1 the dataProvider attribute will be "/user/2" url-pattern data-value = :id and dataproviderExpression = /user/:id the dataProvider attribute will be "/user/2 Basic Home About Work Contact Home Regexp When using capturing groups () the stored values are accessible via the dataProviderExpression e.g., data-value="#couleur (\d+) " => dataProviderExpression="api/unknown/ $1 " Url-pattern Same as RegExp but using url patterns e.g., data-value="#couleur :id " => dataProviderExpression="api/unknown/ :id " --- ## Submit - URL: https://concorde.supersoniks.org/crawl/core/components/functional/submit/submit.html - Doc ID: core/components/functional/submit/submit - Keywords: Concorde, supersoniks, core/components/functional/submit/submit, submit, Submit, functional component, sonic-submit, web component, , src/docs/example/docs-submit-demos.ts, serviceURL="/docs-mock-api", serviceURL, dataProvider, endPoint, onClick, onEnterKey, formDataProvider, sonic-input Submit > Live demos: + src/docs/example/docs-submit-demos.ts. Use Local API demos (serviceURL="/docs-mock-api"). Overview sonic-submit sends the formDataProvider publisher to a REST endpoint (same configuration model as fetch: serviceURL on an ancestor, path via dataProvider or endPoint). | Trigger | Attribute | |---------|-----------| | Click inside the slot | onClick | | Enter in a focused field inside the slot | onEnterKey | | Data / API | Attribute / property | Notes | |------------|----------------------|--------| | Form fields | formDataProvider on ancestor | Filled by sonic-input, sonic-select, etc. via name | | Result after call | submitResultDataProvider on ancestor | Whole result object (after optional submit-result-key) | | Slice of JSON result | submit-result-key on sonic-submit | Dot path, e.g. data when the API returns { data: { … } } | | HTTP verb | method on sonic-submit | post (default), put, patch, delete, get — not inherited from sonic-scope | | Path override | endPoint on sonic-submit | Wins over ancestor dataProvider | | JSON vs multipart | sendAsFormData on sonic-submit | FormData body, still expects JSON response | | Reset publishers | clearedDataOnSuccess on ancestor | Space-separated DataProviderKey.path values; runs when a result object is returned (including error payloads with messages) | | Browser form POST | native on sonic-submit | See Native HTML form | | Result event | — | Bubbles submit CustomEvent with detail = result | | Credential Management API | usernameKey, passwordKey on sonic-submit | After a successful HTTP response, stores login if those keys exist in the payload (defaults: username, password) | | ALTCHA / captcha | needsCaptchaValidation on form/header publisher | Defers send until token is set — see Captcha | While a REST submit runs, the slot is wrapped with data-disabled (faded, no pointer events). With native, validation and loader still run, then the browser performs a normal form submission. Form and result handling POST to mock api/register; result publisher submit-example-result. Native HTML form With native, Concorde does not call fetch. After form validation it: 1. Copies formDataProvider values into matching / / inside the closest (creates hidden inputs if needed). 2. Programmatically clicks a hidden native type="submit" control (name / value on sonic-submit if you need them). Use a real ancestor. For SPAs, a named target (e.g. iframe) avoids leaving the doc page. sendAsFormData Sends fields as multipart/form-data instead of application/json (response is still parsed as JSON). submit-result-key When the API wraps the payload (e.g. { data: { id, token } }), set submit-result-key="data" on sonic-submit so submitResultDataProvider receives only the inner object. Mock endpoint: POST /docs-mock-api/api/register/nested. clearedDataOnSuccess Lists one or more publisher ids (space-separated). After the API returns a result object, each is set to {} — useful to reset the form publisher. Custom submit event Listen for the bubbling submit event; event.detail is the same result written to submitResultDataProvider (if configured). endPoint vs dataProvider Ancestor dataProvider is the default path; endPoint on sonic-submit overrides it for that button only. method="get" Appends publisher fields as a query string on the endpoint, then performs a GET. The mock route api/register/echo returns the query for inspection. dot notation (formDataProvider shape) You can use dot notation in name on form controls; the publisher stores nested objects: Stored shape: { email: { value: "eve.holt@reqres.in" }, details: { password: { value: "pistol" } } } Related - Captcha — submit waits for captchaToken when validation is required. - HTML integration — legacy data-bind result blocks; prefer submitResultDataProvider + @subscribe in Lit demos. --- ## Subscriber - URL: https://concorde.supersoniks.org/crawl/core/components/functional/subscriber/subscriber.html - Doc ID: core/components/functional/subscriber/subscriber - Keywords: Concorde, supersoniks, core/components/functional/subscriber/subscriber, subscriber, Subscriber, functional component, sonic-subscriber, web component Subscriber La mixin Subscriber permet lier un composant à un publisher. La liaison à un publisher se fait via l'attribut dataProvider du composant qui représente ce que l'on obtient en appellant PublisherManager.get(dataProvider). Les propriétés du composant sont automatiquement remplies avec les propriétés du même nom dans les données du publisher. Le composant est automatiquement mis à jour lorsque les données du publisher sont mises à jour. DataProvider Id of the dataProvider that the component will subscribe to custom dataProvider id default id : pageHTML Props Data object (json) that will be passed to the dataProvider Hover to see the data Debug Helper, display the data held by the dataProvider in a floating div see the dataProvider'data NoAutofill Docs coming soon PropertyMap Docs coming soon Permet de mapper un nom de propriété de donnée source vers une propriété du subscriber à la volée BindPublisher Docs coming soon On peut utiliser cette fonction pour lier un publisher spécifique au composant si besoin. instanceCounter Docs coming soon Publisher Docs coming soon Args Docs coming soon NoShadowDom Par défaut on crée un shadow dom mais on peut demander à ne pas en avoir via cette propriété et un attribut associé. Cela se fait à l'initialisation uniquement et n'est pas modifiable lors de la vie du composant. Other attributes gathered from subscriber.stories.ts styles ?? title ?? props ?? onAssign ?? defferedDebug ?? --- ## Value - URL: https://concorde.supersoniks.org/crawl/core/components/functional/value/value.html - Doc ID: core/components/functional/value/value - Keywords: Concorde, supersoniks, core/components/functional/value/value, value, Value, functional component, sonic-value, web component Value Simply shows a value from a data provider. You can target sub data value using dot syntax. The value reacts to changes. Below is a form that helps you to set a value for the field "preference" in the data target with id "value-example" which one do you prefer ? Dogs Cats Then you can see live value of the preference using this code : I prefer If you have more complex data like this You can target it with the dot syntax --- ## Concorde - URL: https://concorde.supersoniks.org/crawl/core/components/ui/alert-messages/alert-messages.html - Doc ID: core/components/ui/alert-messages/alert-messages - Keywords: Concorde, supersoniks, core/components/ui/alert-messages/alert-messages, alert-messages, UI component, sonic-alert-messages, web component --- ## Alert - URL: https://concorde.supersoniks.org/crawl/core/components/ui/alert/alert.html - Doc ID: core/components/ui/alert/alert - Keywords: Concorde, supersoniks, core/components/ui/alert/alert, alert, Alert, UI component, sonic-alert, web component Alert Status This address is already in use. This address is already in use. This address is already in use. This address is already in use. This address is already in use. Sizes This address is already in use. Lorem ipsum dolor sit amet. This address is already in use. Lorem ipsum dolor sit amet. This address is already in use. Lorem ipsum dolor sit amet. This address is already in use. Lorem ipsum dolor sit amet. This address is already in use. Lorem ipsum dolor sit amet. This address is already in use. Lorem ipsum dolor sit amet. This address is already in use. Lorem ipsum dolor sit amet. Alert with link You have 2 unread messages See messages dismiss Background This address is already in use. This address is already in use. This address is already in use. This address is already in use. This address is already in use. Dismissible This address is already in use. This address is already in use. This address is already in use. This address is already in use. This address is already in use. Dismiss forever This address is already in use. --- ## Badge - URL: https://concorde.supersoniks.org/crawl/core/components/ui/badge/badge.html - Doc ID: core/components/ui/badge/badge - Keywords: Concorde, supersoniks, core/components/ui/badge/badge, badge, Badge, UI component, sonic-badge, web component Badge Type Default Primary Neutral Warning Info Success Danger Contrast Outline Default Primary Neutral Warning Info Success Danger Contrast Ghost Default Primary Neutral Warning Info Success Danger Contrast Size 2Xs badge Xs badge Sm badge Md badge Lg badge Xl badge 2Xl badge Empty badges Ellipsis Lorem ipsum dolor sit consectetur adipiscing elit. Lorem ipsum dolor sit consectetur adipiscing elit. Lorem ipsum dolor sit consectetur adipiscing elit. Lorem ipsum dolor sit consectetur adipiscing elit. Badge in button Messages 5 Mon compte 12 33 --- ## Button - URL: https://concorde.supersoniks.org/crawl/core/components/ui/button/button.html - Doc ID: core/components/ui/button/button - Keywords: Concorde, supersoniks, core/components/ui/button/button, button, Button, UI component, sonic-button, web component Button Type Default Primary Neutral Warning Info Success Danger Custom Outline Default Primary Neutral Warning Info Success Danger Custom Variant Default Outline Ghost Link Unstyled Size 2Xs button Xs button Sm button Md button Default button Lg button Xl button 2Xl button Shape 😂 🚀 Loading The length of the button depends on the size of its content Loading button Swap Révélez ... moi ! Reset Set A value Reset current formDataProvider Reset formDataProvider by name GoBack Retour classique Retour url prédéfinie Button with icon Button with icon Button group Choose a subscription 😎 Standard ⭐ Premium 💎 Diamond Aria-label Button with aria label Button link with aria label Custom styles & active states with tailwind subscribe ⭐ Premium --- ## Captcha - URL: https://concorde.supersoniks.org/crawl/core/components/ui/captcha/captcha.html - Doc ID: core/components/ui/captcha/captcha - Keywords: Concorde, supersoniks, core/components/ui/captcha/captcha, captcha, Captcha, UI component, sonic-captcha, web component Captcha Submit with captcha --- ## Card - URL: https://concorde.supersoniks.org/crawl/core/components/ui/card/card.html - Doc ID: core/components/ui/card/card - Keywords: Concorde, supersoniks, core/components/ui/card/card, card, Card, UI component, sonic-card, web component Card Type Base Light Primary Warning Danger Success Info Neutral Invert Header Description rendu via le composant : "sonic-card-header-description" Suffix Header custom Tout le contenu excepté l'icône ✈ passe par le slot du header ✈ Main Main area Lorem ipsum dolor sit, amet consectetur adipisicing elit. Iure id dolor debitis deleniti eligendi natus dolorem a commodi sunt dicta? Ipsa asperiores magni consequuntur dolor voluptatibus. Maxime, nemo? Facere, odio. Footer Footer area Debitis deleniti eligendi natus dolorem aufdh. 🛫 ✈ ✈ ✈ ✈ ✈ 🛬 Example of use Main - Lorem ipsum dolor sit, amet consectetur adipisicing elit. Iure id dolor debitis deleniti eligendi natus dolorem a commodi sunt dicta? Ipsa asperiores magni consequuntur dolor voluptatibus. Maxime, nemo? Facere, odio. Footer - Amet consectetur adipisicing --- ## Divider - URL: https://concorde.supersoniks.org/crawl/core/components/ui/divider/divider.html - Doc ID: core/components/ui/divider/divider - Keywords: Concorde, supersoniks, core/components/ui/divider/divider, divider, Divider, UI component, sonic-divider, web component Divider Default Align Size Custom Lorem ipsum dolor sit amet +25 --- ## Checkbox - URL: https://concorde.supersoniks.org/crawl/core/components/ui/form/checkbox/checkbox.html - Doc ID: core/components/ui/form/checkbox/checkbox - Keywords: Concorde, supersoniks, core/components/ui/form/checkbox/checkbox, checkbox, Checkbox, UI component, form, sonic-checkbox, web component, blacklistFlags, ?blacklistFlags=racist,sexist,…, flags, filterDocsJokes, src/docs/mock-api/fixtures.ts Checkbox Size 2xs checkbox xs checkbox sm checkbox default checkbox lg checkbox xl checkbox 2xl checkbox Checked Already checked checkbox Disabled Disabled checkbox Disabled and checked Disabled but already checked checkbox Checkbox with link Checkbox with link Check all checkbox All Foo Bar Enabling unsetOnDisconect ensures that outdated or irrelevant values are not retained in the form state once their associated inputs are no longer rendered. Baz Example of use — blacklist + queue Checked blacklistFlags values are sent to the mock API (?blacklistFlags=racist,sexist,…), which excludes jokes whose flags match (see filterDocsJokes in src/docs/mock-api/fixtures.ts). --- ## Fieldset - URL: https://concorde.supersoniks.org/crawl/core/components/ui/form/fieldset/fieldset.html - Doc ID: core/components/ui/form/fieldset/fieldset - Keywords: Concorde, supersoniks, core/components/ui/form/fieldset/fieldset, fieldset, Fieldset, UI component, form, sonic-fieldset, web component Fieldset Variant Default Ghost Shadow Legend Custom legend via slot Description via composant : sonic-legend-description Short hand Example of use Informations personnelles Description du fieldset lorem ipsum dolor Mentions légales Conditions générales d'utilisation - Sélectionner - Je préfère ne pas le dire Homme Femme Coordonnées S'inscrire Continuer sans m'inscrire Submitted informations... --- ## Form-actions - URL: https://concorde.supersoniks.org/crawl/core/components/ui/form/form-actions/form-actions.html - Doc ID: core/components/ui/form/form-actions/form-actions - Keywords: Concorde, supersoniks, core/components/ui/form/form-actions/form-actions, form-actions, Form-actions, UI component, form, sonic-form-actions, web component Form-actions Left by default Submit Cancel Justify Submit Cancel Direction Submit Cancel Center Left seats Right seats --- ## Form-layout - URL: https://concorde.supersoniks.org/crawl/core/components/ui/form/form-layout/form-layout.html - Doc ID: core/components/ui/form/form-layout/form-layout - Keywords: Concorde, supersoniks, core/components/ui/form/form-layout/form-layout, form-layout, Form-layout, UI component, form, sonic-form-layout, web component Form-layout - Sélectionner - Je préfère ne pas le dire Homme Femme Activer une superbe option Inscription à la newsletter J'ai lu et accepte les Conditions générales de ventes Valider --- ## Input-autocomplete - URL: https://concorde.supersoniks.org/crawl/core/components/ui/form/input-autocomplete/input-autocomplete.html - Doc ID: core/components/ui/form/input-autocomplete/input-autocomplete - Keywords: Concorde, supersoniks, core/components/ui/form/input-autocomplete/input-autocomplete, input-autocomplete, Input-autocomplete, UI component, form, sonic-input-autocomplete, web component, items, docs-queue-geo-demo, src/docs/example/docs-queue-demos.ts, searchParam, name, value Input-autocomplete The input-autocomplete component brings input and queue together in order to create a suggest behavior. This is why this component is partially configured as a form input and as a queue. Please note that only basic text input params and methods are implemented at this time. You should also be sure to understand the behavior of a queue in order to take full advantage of the input-autocomplete component. Here are some of the features of the input-autocomplete component: Doc examples below use HTML binding on suggestion rows. The Lit equivalent uses an items callback with the row object (see docs-queue-geo-demo in src/docs/example/docs-queue-demos.ts). Embedding without Lit: HTML integration. It provides a suggest behavior, where the user can type a few letters and the component will suggest possible matches. It can be used with a variety of data providers, such as an API. It is fully customizable, so you can change the look and feel of the component to match your needs. If you are looking for a component to provide a suggest behavior, the input-autocomplete component is a good option. Simple Example In this example, the input will use its name as the search parameter when calling the service responsible for autocompletion. The template is used to render the list items of results. The list items are responsible for making a selection. This time, we added buttons with the same name as the input while keeping the same data provider. The result is that when you select an item, the input takes the value of the selected item, completing the classic suggest behavior. Value different from search parameter In this example, the search parameter is separated from the name of the input. This means that the input will use the name "nom" as the search parameter, but the form data provider will be filled with the data named "siren" from the selected list item. To do this, we need to use the searchParam attribute on the input element. This attribute specifies the name of the search parameter that will be used. We also need to use the name and value attribute on the list items. This attribute specifies the value of the data provider that will be used for the selected list item. By using these attributes, we can separate the search parameter from the name of the input and still fill the form data provider with the data from the selected list item. Select autocomplete / value different from search parameter This example is the same as the previous one, except that the select attribute is used. This attribute changes the look and feel of the component slightly. Now the text is less free because, you must select either something from the list given by the service, or nothing else. The following code shows how to use the select attribute in an autocomplete input: keyboard navigation At the moment you can enable keyboard up/down by adding an attribut "data-keyboard-nav" on the component and its listItems --- ## Input - URL: https://concorde.supersoniks.org/crawl/core/components/ui/form/input/input.html - Doc ID: core/components/ui/form/input/input - Keywords: Concorde, supersoniks, core/components/ui/form/input/input, input, Input, UI component, form, sonic-input, web component Input Native attibutes list Label Placeholder Value Required Description Type Liste des types Text Size List Prefix & suffix InlineContent 4 max Example of use — search + queue Filter with formDataProvider + name="contains" (mock API: substring on joke text). Caption and rows via Lit (@subscribe, .items on sonic-queue): azdazd PLOP --> --- ## Radio - URL: https://concorde.supersoniks.org/crawl/core/components/ui/form/radio/radio.html - Doc ID: core/components/ui/form/radio/radio - Keywords: Concorde, supersoniks, core/components/ui/form/radio/radio, radio, Radio, UI component, form, sonic-radio, web component, blacklistFlags Radio Size 2xs radio xs radio sm radio default radio lg radio xl radio 2xl radio Checked Already checked radio Disabled Disabled radio Disabled and checked Disabled but already checked radio radio with link radio with link Example of use — blacklist + queue Same mock filter as Checkbox (blacklistFlags query param). Radio sends one flag at a time. --- ## Select - URL: https://concorde.supersoniks.org/crawl/core/components/ui/form/select/select.html - Doc ID: core/components/ui/form/select/select - Keywords: Concorde, supersoniks, core/components/ui/form/select/select, select, Select, UI component, form, sonic-select, web component, name="lang", ?lang=fr|en, filterDocsJokes, contains Select Options No Selection One Two (set by "tag") Three Size 2xs xs sm md default lg xl 2xl Multiple Choose a number One Two Three Disabled Choose a number One Two Three Example of use — language filter + queue name="lang" is sent as ?lang=fr|en to the doc mock API (filterDocsJokes). Same pattern as Input (contains): --- ## Switch - URL: https://concorde.supersoniks.org/crawl/core/components/ui/form/switch/switch.html - Doc ID: core/components/ui/form/switch/switch - Keywords: Concorde, supersoniks, core/components/ui/form/switch/switch, switch, Switch, UI component, form, sonic-switch, web component Switch Size 2xs switch xs switch sm switch default switch lg switch xl switch 2xl switch Checked Already checked switch Disabled Disabled switch Disabled and checked Disabled but already checked switch switch with link switch with link Example of use — blacklist + queue Same mock filter as Checkbox; switches can enable several flags (comma-separated in the query). --- ## Textarea - URL: https://concorde.supersoniks.org/crawl/core/components/ui/form/textarea/textarea.html - Doc ID: core/components/ui/form/textarea/textarea - Keywords: Concorde, supersoniks, core/components/ui/form/textarea/textarea, textarea, Textarea, UI component, form, sonic-textarea, web component Textarea Native attibutes list Label Placeholder Value Required Rows Description Size --- ## Group - URL: https://concorde.supersoniks.org/crawl/core/components/ui/group/group.html - Doc ID: core/components/ui/group/group - Keywords: Concorde, supersoniks, core/components/ui/group/group, group, Group, UI component, sonic-group, web component Group Radio group 😎 Standard ⭐ Premium 💎 Diamond Button group 🗼Air France 🌴 Air Tahiti 💣 Air Corsica Input actions prefix Input actions suffix --- ## Icon - URL: https://concorde.supersoniks.org/crawl/core/components/ui/icon/icon.html - Doc ID: core/components/ui/icon/icon - Keywords: Concorde, supersoniks, core/components/ui/icon/icon, icon, Icon, UI component, sonic-icon, web component Icon Custom Icons located in the following folder : /svg/$prefix/$name.svg regular solid default prefix External heroicons iconoir material design fontAwesome regular fontAwesome solid feathers lucide Size 2xs xs sm default lg xl 2xl 3xl --- ## Image - URL: https://concorde.supersoniks.org/crawl/core/components/ui/image/image.html - Doc ID: core/components/ui/image/image - Keywords: Concorde, supersoniks, core/components/ui/image/image, image, Image, UI component, sonic-image, web component Image Rounded Ratio Object position Cover --- ## Link - URL: https://concorde.supersoniks.org/crawl/core/components/ui/link/link.html - Doc ID: core/components/ui/link/link - Keywords: Concorde, supersoniks, core/components/ui/link/link, link, Link, UI component, sonic-link, web component Link Href Basic link with href attibute Target Link with target attibute AutoActive autoActive partial (default) AutoActive Strict vs partial autoActive strict autoActive strict AutoActive disabled autoActive disabled --- ## Loader - URL: https://concorde.supersoniks.org/crawl/core/components/ui/loader/loader.html - Doc ID: core/components/ui/loader/loader - Keywords: Concorde, supersoniks, core/components/ui/loader/loader, loader, Loader, UI component, sonic-loader, web component Loader Inline mode CurrentColor Align Fixed mode Loading button A loading button Hide / show programmatically import {Loader} from "@supersoniks/concorde/core/components/ui/loader/loader"; // Show Loader.show({ mode:"fixed" // optional : fixed | inline container : document.querySelector('#foo') // optional, default : parent sonic-theme || document.body }); // Hide Loader.hide(); --- ## Menu - URL: https://concorde.supersoniks.org/crawl/core/components/ui/menu/menu.html - Doc ID: core/components/ui/menu/menu - Keywords: Concorde, supersoniks, core/components/ui/menu/menu, menu, Menu, UI component, sonic-menu, web component Menu Direction Row Profile History Orders Log out Column Profile History Orders Log out Size XS Profile History Orders Small Profile History Orders Medium Profile History Orders Large Profile History Orders XL Profile History Orders Shadow None Small Medium Large Align Home Profile Home Profile Home Profile More Home Profile History Orders Settings Messages Log out Scrollable Home Profile History Orders Home Profile History Orders Home Profile History Orders Settings Messages Settings Messages Log out --- ## Concorde - URL: https://concorde.supersoniks.org/crawl/core/components/ui/modal/modal.html - Doc ID: core/components/ui/modal/modal - Keywords: Concorde, supersoniks, core/components/ui/modal/modal, modal, UI component, sonic-modal, web component Modal Simple modal Lorem ipsum dolor sit amet +33 Donec sed vestibulum augue. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed eget quam eu mi luctus faucibus. Click don't close Confirm Long text Long text Infos et tarifs Horaires d’ouverture Du mercredi au samedi de 13h30 à 19h et le dimanche de 13h30 à 18h + horaires spécifiques pour événements ou festivals  Billetterie  La participation à certains événements et activités est soumise à un système de billetterie avec achats ou réservations disponibles en ligne ou au guichet.   Les tarifs (Tarif plein / Tarif Réduit / Tarif Spécifique) sont indiqués pour chaque événement et peuvent varier selon la nature de l’événement. Les tarifs sont identiques en ligne et au guichet. Il est néanmoins conseillé de réserver les billets en avance pour s’assurer une place. L’accès au Skatepark, Fablab et Halle C sont soumis à adhésions. Les adhésions sont gratuites et nominatives. Elle donnent accès aux espaces dans la limite des places disponibles. + d’infos billetterie@laconditionpublique.com +33 (0)328334833  Accès 14, place Faidherbe 59100 Roubaix Métro / Tram : Arrêt Eurotéléport, Ligne 2 Bus / V’Lille : Arrêt Condition Publique  Accès handicapés : nous contacter avant votre venue au 03 28 33 48 33 ou par mail billetterie@laconditionpublique.com Force action Force action to close Do you want some cookies ? Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed eget quam eu mi luctus faucibus. Nope Hell yes ! Custom dimensions Custom width Fullscreen Fullscreen --- ## Pop - URL: https://concorde.supersoniks.org/crawl/core/components/ui/pop/pop.html - Doc ID: core/components/ui/pop/pop - Keywords: Concorde, supersoniks, core/components/ui/pop/pop, pop, Pop, UI component, sonic-pop, web component Pop Placement Left Notifications Right Notifications Top Notifications Bottom Notifications Shadow None Notifications Small Notifications Medium Notifications Large Notifications noToggle No toggle on click Notifications Default toggle on click Notifications manual Manual doesn't open on click Hourra ! Click to open pop --- ## Progress bar - URL: https://concorde.supersoniks.org/crawl/core/components/ui/progress/progress.html - Doc ID: core/components/ui/progress/progress - Keywords: Concorde, supersoniks, core/components/ui/progress/progress, progress, Progress bar, UI component, sonic-progress, web component Progress bar Displays an indicator showing the completion progress of a task. Indeterminate Remaining 80% Used 20% left Sizes 25% 25% 25% 25% 25% 25% 25% Type 50% 50% 50% 50% 50% Invert 50% --- ## Table - URL: https://concorde.supersoniks.org/crawl/core/components/ui/table/table.html - Doc ID: core/components/ui/table/table - Keywords: Concorde, supersoniks, core/components/ui/table/table, table, Table, UI component, sonic-table, web component Table Basic usage Id Name Email 1 George Bluth george.bluth@reqres.in 2 Janet Weaver janet.weaver@reqres.in 3 Emma Wong emma.wong@reqres.in List / fetch Id Name Email $lastname "> Size 2xs xs sm md default lg xl 2xl Bordered Id Name Email 1 George Bluth george.bluth@reqres.in 2 Janet Weaver janet.weaver@reqres.in 3 Emma Wong emma.wong@reqres.in MaxHeight Id Name Email $lastname "> Type Values available : primary, info, success, warning, danger Attribute type set on sonic-th Attribute type set on sonic-td Attribute type set on sonic-tr Colspan / rowspan colspan : 2 cell cell cell rowspan : 2 cell cell Cell style attributes The following attributes will be used to set the style of the component : - align - minWidth - maxWidth - width align minWidth maxWidth width Left 20rem 5rem 15rem center 20rem 5rem 15rem right 20rem 5rem - Lorem ipsum dolor 15rem - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam porta sollicitudin mollis. Curabitur sit amet nibh diam. Vivamus a pharetra mauris. Responsive Every table is responsive by default Id Name Email Comment $lastname "> hasellus suscipit vulputate lacus, in tempor turpis aliquam vel. Nunc eleifend, velit id ultrices elementum, ipsum felis porttitor dui, id laoreet diam nulla sed urna. Sticky header Company Contact Country Quantity Price Alfreds Futterkiste Maria Anders Germany 2 40€ Centro comercial Moctezuma no information yet 3 40€ Ernst Handel Roland Mendel Austria 4 40€ Island Trading Helen Bennett UK 1 40€ Laughing Bacchus Winecellars Yoshi Tannamuri Canada 0 40€ Magazzini Alimentari Riuniti Giovanni Rovelli Italy 20 40€ Alfreds Futterkiste Maria Anders Germany 2 40€ Centro comercial Moctezuma no information yet 3 40€ Ernst Handel Roland Mendel Austria 4 40€ Island Trading Helen Bennett UK 1 40€ Laughing Bacchus Winecellars Yoshi Tannamuri Canada 0 40€ Magazzini Alimentari Riuniti Giovanni Rovelli Italy 20 40€ table caption --- ## Toast - URL: https://concorde.supersoniks.org/crawl/core/components/ui/toast/toast.html - Doc ID: core/components/ui/toast/toast - Keywords: Concorde, supersoniks, core/components/ui/toast/toast, toast, Toast, UI component, sonic-toast, web component, success, error, warning, info, preserve: true, dismissForever: true, id, ghost: true Toast Utilisation de base Afficher un toast Statut Les statuts disponibles sont : success, error, warning, info ou vide (par défaut). Default Success Error Warning Info Avec titre Toast avec titre Toast d'erreur avec titre Avec contenu HTML Le contenu du toast peut contenir du HTML. HTML et un lien cliquable ', status: 'info' })"> Toast avec HTML Persistance Par défaut, les toasts disparaissent automatiquement. Avec preserve: true, le toast reste affiché jusqu'à ce qu'il soit fermé manuellement. Toast persistant Masquer définitivement Avec dismissForever: true et un id, le toast peut être masqué définitivement. Une fois fermé, il ne réapparaîtra plus même après rechargement de la page. Toast avec dismiss forever Fantôme Avec ghost: true, le toast devient semi-transparent et non-interactif. Toast ghost Méthodes de suppression Tout supprimer Supprime tous les toasts sauf ceux marqués comme ghost. Ajouter toast 1 Ajouter toast 2 Ajouter toast 3 Supprimer tous Supprimer par statut Supprime tous les toasts d'un statut spécifique. Success Error Warning Supprimer success Supprimer error Supprimer les éléments temporaires Supprime tous les toasts qui ne sont pas marqués comme preserve. Temporaire Persistant Supprimer temporaires Exemple complet Toast complet --- ## Tooltip - URL: https://concorde.supersoniks.org/crawl/core/components/ui/tooltip/tooltip.html - Doc ID: core/components/ui/tooltip/tooltip - Keywords: Concorde, supersoniks, core/components/ui/tooltip/tooltip, tooltip, Tooltip, UI component, sonic-tooltip, web component Tooltip hover me Placement By default, tooltip is centered next to its content Top Bottom Right Left More placement Top start Top end Bottom start Bottom end Right start Right end Left start Left end Focusable Focus or hover non interactive element Disabled Tooltip disabled --- ## Data flow - URL: https://concorde.supersoniks.org/crawl/docs/_core-concept/dataFlow.html - Doc ID: docs/_core-concept/dataFlow - Keywords: Concorde, supersoniks, docs/_core-concept/dataFlow, dataFlow, Data flow, concept, DataProvider, get, set, dp, DataProviderKey, sub(key), @subscribe, @state, @ancestorAttribute Data flow Recommended patterns for new Concorde apps (Lit + TypeScript). Under the hood, data lives in a DataProvider store (legacy Publisher API: Legacy: Sharing data). Quick map | Need | Use | |------|-----| | Read/write in code | get / set / dp + DataProviderKey (static paths only) | | Reactive Lit template | sub(key) or @subscribe | | Read component state from store | @subscribe + DataProviderKey + @state | | Inherit ancestor attributes | @ancestorAttribute | | Write from component state | @publish | | React to assignments | @handle | | HTTP GET | @get + Endpoint, or sonic-list / sonic-queue with fetch | | HTTP POST (body from store) | @post + Endpoint + body DataProviderKey | | HTTP PUT / PATCH (body from store) | @put / @patch — same model as @post | | Forms | formDataProvider + name on fields | | Offline doc demos | serviceURL="/docs-mock-api" — Local API demos | Skill: concorde-get-set-dp in the package ai/ folder. DataProviderKey import { DataProviderKey } from "@supersoniks/concorde/dataProviderKey"; import { dp, get, set } from "@supersoniks/concorde/utils"; const cartKey = new DataProviderKey<{ items: string[] }>("cart"); set(cartKey, { items: [] }); dp(cartKey.items).set(["a", "b"]); get(cartKey); Dynamic paths (users.${userId}) → decorators or sub() — not get("users.${id}") in imperative code. Resolution rules: Dynamic path placeholders. DataProviderKey Decorators | Decorator | Role | |-----------|------| | @subscribe | Read-only property from store — data configuration (type + key + scope) | | @publish | Push property writes to store | | @handle | Method called on assignment | | @ancestorAttribute | Copy ancestor HTML attribute onto property | | @get | HTTP GET into ApiResult<T> | | @post | HTTP POST into ApiResult<T> (body from a publisher) | | @put / @patch | HTTP PUT / PATCH into ApiResult<T> | Walkthrough: My first component HTTP and lists - @get — single GET on a component - @post — POST with body read from a DataProviderKey - @put · @patch — PUT / PATCH (same options as @post) - List — fetch + key="data" + /docs-mock-api/api/users - Queue — lazy offset=$offset&perpage=$limit + optional dataFilterProvider (form → query) Starter kit npx @supersoniks/create-concorde-ts-starter my-app Interactive routes mirror these patterns (/concepts/, /demo/). Legacy integration | Topic | Page | |-------|------| | Subscriber / Fetcher mixins on app code | Legacy: Subscriber mixin, Legacy: My first subscriber | | data-bind HTML (plain HTML hosts) | HTML integration — doc demos use Lit in src/docs/example/ | | @onAssign | @onAssign (prefer @handle) | | sonic-fetch alone | Fetch | --- ## lit + tailwind + vite - URL: https://concorde.supersoniks.org/crawl/docs/_core-concept/overview.html - Doc ID: docs/_core-concept/overview - Keywords: Concorde, supersoniks, docs/_core-concept/overview, overview, lit + tailwind + vite, concept, DataProvider, custom elements, reactive properties lit + tailwind + vite Some notes for an overview of Concorde's functioning Standard Web components A web component is simply a custom HTML element created using web standards: web component = Custom Elements + shadowDom + HTML templates Lit Library Concorde components are written using the lightweight Lit library: https://lit.dev/. A good portion of the elements documented on the Lit website are standard JavaScript concepts (e.g., mixins, tagged template literals). The library simplifies the writing of web components by automating certain tasks: - Creation of an open mode shadow DOM (the component is accessible from the outside via its shadow root property). - Rendering function based on tagged template literals - Reactive properties that trigger rendering when modified. Our use of Lit rarely goes beyond this hello world Typescript Language We write the components in TypeScript, which is then compiled into JS. Static and inferred typing provides stronger validation of the program's functioning during compilation. However, we often use the "any" type due to time constraints. Lit uses TypeScript to simplify development through metadata: - Simplified declaration of custom elements via @customElement - Generation of reactive properties via @property. ☢️Warning: - During development, we operate in interpreted mode, which is faster but allows errors that won't pass during the build. - It is important to build a distribution before pushing the code to ensure everything is fine. Compatibility with Classic JS Libraries Compatibility issues may arise when using JS libraries, particularly regarding DOM traversal. This is because these libraries are blocked from traversing the DOM due to the presence of the shadow DOM. Here are some examples of things that may cause problems: - Latest FontAwesome in SVG mode, icons are not replaced - Libraries associated with jQuery - document.getElementById - ... Topics to Explore for Further Learning 🧱 Creating components 🎨 Adding styles 🥨 Sharing data --- ## Legacy: Subscriber mixin - URL: https://concorde.supersoniks.org/crawl/docs/_core-concept/subscriber.html - Doc ID: docs/_core-concept/subscriber - Keywords: Concorde, supersoniks, docs/_core-concept/subscriber, subscriber, Legacy: Subscriber mixin, concept, DataProvider, LitElement, @subscribe, @ancestorAttribute, sonic-list, sonic-fetch, dataProvider, subDataProvider = 'address.of.my.data', props Legacy: Subscriber mixin > New app components: My first component and Data flow — extend LitElement with decorators (@subscribe, @ancestorAttribute, …). This mixin remains in core components (sonic-list, sonic-fetch, …) and legacy tutorials. The Subscriber mixin was commonly extended by Concorde core components and older destination components. Pure UI components usually don't extend it, especially those outside of form components. DataProvider Attribute: Automatic Filling of Subscriber Properties Upon being added to the DOM (connectedCallback), subscribers search for the first occurrence of the dataProvider attribute in their parent's HTML structure. The value of this attribute is the DataProvider path (see Data flow and DataProviderKey; legacy API: Sharing Data). The subscriber then subscribes to the publisher as a data template to be filled. The reactive properties of the component reflect in real-time the properties with the same name in the publisher. This functionality is commonly used in the generic components fetch, queue, and list. If the subscriber is inside another subscriber, it can subscribe to a data of its parent using the attribute subDataProvider = 'address.of.my.data' instead of the parent's data. This allows for dynamic behavior. Automated Translation In a similar manner, searching for ancestor attributes can be used to configure automatic translation for any property starting with "wording" and ending with a label identifier recognized by the wordingProvider API. Normally, this API is globally configured and not within the component. Therefore, remember to prefix translatable label identifiers present in the ticketing system, for example, with "wording". Reactive Property props The value of this property can be provided as a JSON object or any other value. This value is then assigned to the associated publisher via the dataProvider attribute. As a result, the reactive properties of all subscribers associated with the same dataProvider are filled as mentioned above. Disabling Automatic Filling of Reactive Properties It is possible to disable the automatic filling of reactive properties in a particular component. To do so, set the variable noAutoFill = true in the component's class. However, the reactive property props will still be updated. For example, sonic-subscriber and sonic-fetch have this attribute because they do not have reactive properties. ☢ CAUTION: When creating an object of type Subscriber or Fetcher, make sure to use the mixins and not directly extend the concrete fetch component (sonic-fetch) and subscriber component (sonic-subscriber). This is because the noAutofill = true attribute is set in those components. TIPS: If you disable automatic filling, you will likely make the rendering dynamic by writing expressions like this.props.my.subproperty. If props is updated, the rendering will be triggered. However, if this.props.my.subproperty is directly modified, the rendering will not be triggered. To achieve more reactivity, you can enable rendering when any subproperty is modified. Simply set the variable renderOnPropsInternalChange = true in the class that implements the corresponding mixin. Data Binding Suppose that: - You have a subscriber subscribed to a publisher via its dataProvider attribute. - The published data has the following structure: {"img": { "avatar": "my-avatar.jpg", "caption": "look at my smile" }, "email": "an.email@example.com" } - We want to keep the display of an image and its title attribute up to date, as well as a "contact" button to email the person in the photo. The content of the subscriber could be as follows: In a Lit app, use the same data with data configuration (@subscribe + DataProviderKey) on a small row component — see docs-user in src/docs/example/users.ts. For HTML-only embedding (no Lit), attribute binding is described in HTML integration and in the Legacy Subscriber mixin pages. This pattern also illustrates: - Binding to subproperties using dot syntax, as done for the src attribute of the img tag. - Using a simple expression to include the property within a string, as in creating a mailto link. - Using formatting functions, as in the title attribute of the img tag. You can find (and add) formatting functions in the core/utils/Format.ts file. - Writing for properties that are in camel case. Convert everything to lowercase and add hyphens. Note that innerHTML (or outerHTML) is a special case where no hyphens are added between each letter. Note: Data binding implies that updating the img.src data via the publisher will change the photo without any additional calls. Special Variables: - self targets the root of the data. This is useful, for example, when the data is a pure string. - parent targets the parent publisher of the current publisher, if any. For example, the subscribers of lists have the complete data of the list as their parent. - metadata is only used in the case of sonic-list at the moment. In a simple list, metadata see the (list)[] component for further details Disabling this functionality: You can disable data binding if it is not needed by calling DataBinding.disable(). --- ## @ancestorAttribute - URL: https://concorde.supersoniks.org/crawl/docs/_decorators/ancestor-attribute.html - Doc ID: docs/_decorators/ancestor-attribute - Keywords: Concorde, supersoniks, docs/_decorators/ancestor-attribute, ancestor-attribute, @ancestorAttribute, decorator, ancestorAttribute, connectedCallback, HTML.getAncestorAttributeValue, dataProvider, testAttribute, dynamic: false, formDataProvider, wordingProvider @ancestorAttribute The @ancestorAttribute decorator automatically injects the value of an ancestor's attribute into a class property at the time of connectedCallback. Principle This decorator uses HTML.getAncestorAttributeValue to traverse up the DOM tree from the current element and find the first ancestor that has the specified attribute. The value of this attribute is then assigned to the decorated property. Usage Import import { ancestorAttribute } from "@supersoniks/concorde/decorators"; Basic example The component reads dataProvider and testAttribute from its ancestor wrapper. By default (dynamic: false), values are read once at connect. import { html, LitElement } from "lit"; import { customElement } from "lit/decorators.js"; import { ancestorAttribute } from "@supersoniks/concorde/decorators"; @customElement("demo-ancestor-attribute") export class DemoAncestorAttribute extends LitElement { @ancestorAttribute("dataProvider") dataProvider: string | null = null; @ancestorAttribute("testAttribute") testAttribute: string | null = null; render() { return html dataProvider: ${this.dataProvider ?? "null"} testAttribute: ${this.testAttribute ?? "null"} ; } } Use cases This decorator is particularly useful for: - Retrieving the dataProvider from an ancestor without having to pass it explicitly - Retrieving the formDataProvider in form components - Retrieving the wordingProvider for translation - Retrieving any other attribute defined on an ancestor Behavior - The search starts from the current element and traverses up the DOM tree - The injection happens automatically at the time of connectedCallback - By default (dynamic: false), the value is read once at connect time and is not updated afterward - With { dynamic: true }, the property stays in sync when: - an ancestor changes the attribute value - a closer ancestor gains the attribute (that value wins) - the nearest ancestor loses the attribute (falls back to the next one up, or null) - When dynamic is enabled, updates also propagate to @subscribe / @bind paths that depend on the decorated property (e.g. ${dataProvider}) Dynamic mode Use { dynamic: true } when the decorated property must follow the DOM. With Lit, add @state() on the same property so template updates are visible when the observer assigns a new value. The live demo below shows only the resolved attribute value — no store, no reparenting. @customElement("demo-ancestor-attribute-dynamic") export class DemoAncestorAttributeDynamic extends LitElement { @ancestorAttribute("dataProvider", { dynamic: true }) @state() dataProvider: string | null = null; render() { return html <p>Nearest ancestor: <strong>${this.dataProvider ?? "null"}</strong></p> ; } } Try in the demo: - parent-a / parent-b — change the value on the direct parent - Remove parent attr — falls back to outer - Add closer — a wrapper with from-closer becomes the nearest ancestor - Remove closer — back to parent or outer - Remove outer attr — if parent has no attribute either, the child shows null Static mode (default) — read once at connect, no observer: @ancestorAttribute("dataProvider") dataProvider: string | null = null; Notes - This decorator works with any component that has a connectedCallback method (such as LitElement or components extending Subscriber) - The search also traverses Shadow DOM if necessary - If multiple ancestors have the attribute, the closest one will be used --- ## @autoSubscribe - URL: https://concorde.supersoniks.org/crawl/docs/_decorators/auto-subscribe.html - Doc ID: docs/_decorators/auto-subscribe - Keywords: Concorde, supersoniks, docs/_decorators/auto-subscribe, auto-subscribe, @autoSubscribe, decorator, autoSubscribe, DataProviderKey, PublisherManager, ; } render() { return html @autoSubscribe > Legacy: prefer @subscribe + DataProviderKey. Examples below may still show PublisherManager for existing codebases. The @autoSubscribe decorator automatically detects which publishers are accessed within a method and subscribes to them. When any of these publishers change, the method is automatically re-executed. Principle This decorator wraps a method to track which publishers are accessed during its execution. It then subscribes to all accessed publishers, and when any of them change, the method is re-executed. This provides automatic reactivity without manually managing subscriptions. Usage Import import { autoSubscribe } from "@supersoniks/concorde/decorators"; Basic example @customElement("demo-auto-subscribe") export class DemoAutoSubscribe extends LitElement { static styles = [tailwind]; @state() displayText: string = ""; @state() computedValue: number = 0; @autoSubscribe() updateDisplay() { const value1 = PublisherManager.get("autoValue1").get() || 0; const value2 = PublisherManager.get("autoValue2").get() || 0; this.computedValue = value1 + value2; this.displayText = ${value1} + ${value2} = ${this.computedValue}; } render() { return html ${this.displayText} this.randomizeValue("autoValue1")}> Randomize Value 1 this.randomizeValue("autoValue2")}> Randomize Value 2 ; } randomizeValue(publisherId: string) { const value = PublisherManager.get(publisherId); value.set(Math.floor(Math.random() 100)); } } Example with render method @customElement("reactive-view") export class ReactiveView extends LitElement { @autoSubscribe() render() { const data = PublisherManager.get("myData"); const config = PublisherManager.get("config"); // This render method will be automatically re-executed // when myData or config change const value = data.get()?.value || 0; const multiplier = config.get()?.multiplier || 1; return html Result: ${value multiplier} Value: ${value} Multiplier: ${multiplier} ; } } How it works 1. First execution: When the method is called, PublisherManager.collectModifiedPublisher() is used to track which publishers are accessed 2. Subscription: After execution, the decorator subscribes to all detected publishers 3. Re-execution: When any subscribed publisher changes, the method is automatically called again 4. Cleanup: On disconnectedCallback, all subscriptions are automatically removed Behavior - The method is automatically called on connectedCallback - The method is re-executed whenever any accessed publisher changes - Subscriptions are managed automatically (no manual cleanup needed) - Only publishers accessed during method execution are subscribed to - The decorator uses queueMicrotask to batch multiple updates and avoid unnecessary re-renders Use cases This decorator is particularly useful for: - Reactive rendering where the render method depends on multiple publishers - Data transformation that needs to update when source data changes - Computed properties that depend on multiple data sources - Automatic synchronization between publishers and component state Complete example import { html, LitElement } from "lit"; import { customElement } from "lit/decorators.js"; import { autoSubscribe } from "@supersoniks/concorde/decorators"; import { PublisherManager } from "@supersoniks/concorde/core/utils/PublisherProxy"; @customElement("shopping-cart") export class ShoppingCart extends LitElement { items: any[] = []; total: number = 0; discount: number = 0; @autoSubscribe() calculateTotal() { const cart = PublisherManager.get("cart"); const promo = PublisherManager.get("promo"); // Access cart items this.items = cart.items.get() || []; // Access promo code const promoCode = promo.code.get() || ""; const discountPercent = promoCode === "SAVE10" ? 0.1 : 0; // Calculate totals const subtotal = this.items.reduce((sum, item) => sum + (item.price item.quantity), 0 ); this.discount = subtotal discountPercent; this.total = subtotal - this.discount; this.requestUpdate(); } connectedCallback() { super.connectedCallback(); this.calculateTotal(); } render() { return html Shopping Cart ${this.items.map(item => html ${item.name} x${item.quantity} - ${item.price}€ )} Subtotal: ${this.total + this.discount}€ Discount: -${this.discount}€ Total: ${this.total}€ ; } } // When you update the publishers, calculateTotal is automatically called: const cart = PublisherManager.get("cart"); cart.items.set([ { name: "Product 1", price: 10, quantity: 2 }, { name: "Product 2", price: 15, quantity: 1 } ]); const promo = PublisherManager.get("promo"); promo.code.set("SAVE10"); // calculateTotal will be automatically called and the UI will update Notes - This decorator works with any component that has connectedCallback and disconnectedCallback methods (such as LitElement or components extending Subscriber) - The method is called automatically on connectedCallback - Remember to call this.requestUpdate() if you're updating component properties - The decorator uses debouncing via queueMicrotask to prevent excessive re-executions - For more information about publishers, see the documentation on Sharing data --- ## @bind - URL: https://concorde.supersoniks.org/crawl/docs/_decorators/bind.html - Doc ID: docs/_decorators/bind - Keywords: Concorde, supersoniks, docs/_decorators/bind, bind, @bind, decorator, @state(), DataProviderKey, . Use , reflect: true, publisher.set(...) @bind Binds a class property to a path in a publisher. The property updates when publisher data changes. For Lit re-renders, also add @state() on the same property. See also: @subscribe, @handle, @publish, @get, @post, @put, @patch. Principle The decorator subscribes to the DataProvider store using dot notation or a DataProviderKey. Updates flow into the decorated property (Data flow). Import import { bind } from "@supersoniks/concorde/decorators"; Example @customElement("demo-bind") export class DemoBind extends LitElement { static styles = [tailwind]; @bind("demoData.firstName") @state() firstName = ""; @bind("demoData.lastName") @state() lastName: string = ""; @bind("demoData.count") @state() count: number = 0; render() { return //...... } updateData() { set(demoDataKey, { ...get(demoDataKey), count: get(demoDataKey).count + 1 }); // see demo-bind in src/docs/example/decorators-demo-bind-demos.ts const randomIndex = Math.floor(Math.random() demoUsers.get().length); const randomUser = demoUsers.get()[randomIndex]; demoData.set({ firstName: randomUser.firstName, lastName: randomUser.lastName, count: (demoData.count.get() || 0) + 1, }); } } DataProviderKey (strict typing) @bind accepts either a string path (legacy) or a DataProviderKey . The property type must match T. Use reflect: true to push local writes back to the publisher (see below). See DataProviderKey. import { bind } from "@supersoniks/concorde/decorators"; import { DataProviderKey } from "@supersoniks/concorde/dataProviderKey"; type Data = { count: number }; const dataKey = new DataProviderKey ("data"); @bind(dataKey.count, { reflect: true }) @state() count: number = 0; Reflect (reflect: true) Two-way sync: reads from the publisher and local assignments call publisher.set(...). An internal guard avoids infinite loops. @bind("userData.profile.avatarUrl", { reflect: true }) @state() avatar: string; @customElement("demo-bind-reflect") export class DemoBindReflect extends LitElement { static styles = [tailwind]; @bind("bindReflectDemo.count", { reflect: true }) @state() withReflect: number = 0; @bind("bindReflectDemo.count") @state() withoutReflect: number = 0; render() { return html from publisher : ${sub("bindReflectDemo.count") || 0} from component with reflect : ${this.withReflect || 0} from component without reflect : ${this.withoutReflect || 0} this.withReflect++} >Increment with reflect this.withoutReflect++} >Increment without reflect ; } } Path syntax - First segment: data provider id. - Nested properties: dot notation. - Arrays: numeric index (items.0). Dynamic paths Use ${prop} or ${this.prop} inside a normal string literal (not a JS template literal with backticks). @bind re-subscribes when a reactive dependency changes. While a placeholder is null/undefined, the bind is inactive; optional { skipEmptyPlaceholder: true } also waits on "". See Dynamic path placeholders. > Properties referenced in the pattern must be reactive (@property, etc.) or you must call requestUpdate manually. Behavior - Subscribes at connectedCallback, unsubscribes at disconnectedCallback. - If the path does not exist yet, a publisher may be created with null. Notes Works with any component that has the usual DOM lifecycle (LitElement, Subscriber mixin, etc.). Shared data: Sharing data. --- ## @get - URL: https://concorde.supersoniks.org/crawl/docs/_decorators/get.html - Doc ID: docs/_decorators/get - Keywords: Concorde, supersoniks, docs/_decorators/get, get, @get, decorator, API.getDetailed, ApiResult | null, request, response, null, dataProvider(...), result, Endpoint, ApiResult @get Loads data through API.getDetailed. The decorated property is ApiResult | null: request, response (or null for dataProvider(...) resolution without HTTP), and typed result. Pass an Endpoint as the first argument. Import get and ApiResult from @supersoniks/concorde/decorators, and Endpoint from @supersoniks/concorde/utils/endpoint. Configuration - Default: HTML.getApiConfiguration(host) (ancestor serviceURL, etc.). - Second argument: DataProviderKey — config is read from the publisher at the resolved path; internal mutations trigger another GET. See API configuration for mock demos. Dynamic path ${prop} on the endpoint or config key is resolved on the host. While a placeholder is null or undefined, no GET runs. Optional skipEmptyPlaceholder: true also blocks "" (empty string only). Details: Dynamic path placeholders. Options (GetOptions) Pass as the second argument (scoped config) or third (with a configuration key). Same shape as send decorators where applicable: | Option | Description | |--------|-------------| | skipEmptyPlaceholder | Treat "" as not ready (see Dynamic path). | | refetchEveryMs | Automatic polling interval (ms). 0 or omitted = no polling. | | triggerKey | DataProviderKey — PublisherManager.get(...).invalidate() on that publisher re-runs the GET. | Manual refetch (triggerKey) Same pattern as @post: a publisher acts as a signal, independent of the response payload. import { get, type ApiResult } from "@supersoniks/concorde/decorators"; import { Endpoint } from "@supersoniks/concorde/utils/endpoint"; import { DataProviderKey } from "@supersoniks/concorde/dataProviderKey"; import { dp } from "@supersoniks/concorde/core/utils/PublisherProxy"; const geoCommunesEndpoint = new Endpoint ( "communes?limit=5&fields=nom,code", ); const geoCommunesRefreshKey = new DataProviderKey ( "docsDemoGeoCommunesRefresh", ); @get(geoCommunesEndpoint, apiConfigurationKey, { triggerKey: geoCommunesRefreshKey, }) @state() payload: ApiResult | null = null; // Same component or anywhere else: dp(geoCommunesRefreshKey).invalidate(); Prefer triggerKey over mutating the API configuration publisher when you only want to reload the same URL — no need to touch serviceURL, tokens, etc. The docs mock adds an X-Fetched-At response header on /docs-mock-api/geo/communes so live demos can show when the last fetch happened (response.headers.get("X-Fetched-At")). When the GET runs again - A referenced Lit property changes (endpoint path and/or config key contains ${...}). - set on the active configuration publisher (onInternalMutation). - triggerKey publisher invalidate(). - refetchEveryMs polling (after each successful fetch). Import import { get, type ApiResult } from "@supersoniks/concorde/decorators"; import { Endpoint } from "@supersoniks/concorde/utils/endpoint"; import { DataProviderKey } from "@supersoniks/concorde/dataProviderKey"; Minimal example Same demo service as sonic-queue (/docs-mock-api/geo/). Publisher setup lives in decorators-demo-geo.ts and decorators-demo-subscribe-publish-get-demos.ts. @get(new Endpoint ("users/${userId}")) @state() payload: ApiResult | null = null; Live demos @get with triggerKey — refresh button + X-Fetched-At timestamp in the UI: Dynamic config and endpoint path. Manual refetch is triggered from a separate component (same triggerKey publisher): Scoped @get with @publish / @subscribe on the payload (see @publish and @subscribe) — wrap under an ancestor with serviceURL="/docs-mock-api/geo/": Stale responses are ignored if the path or generation changed before the request finished. See also - Dynamic path placeholders - @post · @put · @patch — send decorators (same path rules) --- ## @handle - URL: https://concorde.supersoniks.org/crawl/docs/_decorators/handle.html - Doc ID: docs/_decorators/handle - Keywords: Concorde, supersoniks, docs/_decorators/handle, handle, @handle, decorator, DataProviderKey, @state, HandleOptions, null, undefined, @bind, @subscribe, ${user.firstName} ${user.lastName} @handle Typed callback on one or more DataProviderKey paths: invokes the decorated method when a publisher assigns a value (calculations, side effects, updating other @state properties, etc.). Unlike @subscribe, nothing is bound to the decorated member — only your method runs. @handle is typed and accepts up to 3 keys plus an optional trailing HandleOptions object. It supersedes the string-based @onAssign. By default the method is called on every assignment, even when the value is null / undefined. Use the options below to restrict that. Import import { handle, Skip } from "@supersoniks/concorde/decorators"; import { DataProviderKey } from "@supersoniks/concorde/dataProviderKey"; import { get, set } from "@supersoniks/concorde/utils"; Basic example type DemoCounterData = { count: number }; const demoDataKey = new DataProviderKey<DemoCounterData>("demoData"); @customElement("demo-handle") export class DemoHandle extends LitElement { @state() doubled = 0; @state() lastUpdate = ""; @handle(demoDataKey.count) onCountChange(count: number) { this.doubled = count 2; this.lastUpdate = new Date().toLocaleTimeString(); } incrementCount() { const data = get(demoDataKey); set(demoDataKey, { ...data, count: data.count + 1 }); } render() { return html <p>Doubled count: ${this.doubled}</p> <p><small>Last update: ${this.lastUpdate}</small></p> <sonic-button @click=${this.incrementCount}>Increment</sonic-button> ; } } Dynamic path Placeholders in DataProviderKey resolve from the host component’s properties (same rules as @bind / @subscribe). type User = { firstName: string; lastName: string; email: string }; @customElement("demo-handle-dynamic") export class DemoHandleDynamic extends LitElement { @property({ type: Number }) userIndex = 0; @state() displayName = ""; @state() lastUpdate = ""; @handle(new DataProviderKey<User, { userIndex: number }>("demoUsers.${userIndex}")) onUserAssigned(user: User) { this.displayName = ${user.firstName} ${user.lastName}; this.lastUpdate = new Date().toLocaleTimeString(); } render() { return html...; } } Multiple paths @handle accepts up to 3 keys; the method receives one strongly-typed argument per key, in order. Each assignment triggers the method, so make your method safe against partial values (or use waitForAllDefined, see below). type QueueConfig = { onInactivity: { stillHere: { show: boolean } } }; const config = new DataProviderKey<QueueConfig>("sessionQueueConfig"); const idle = new DataProviderKey<{ isIdle: boolean }>("idleStatus"); @customElement("demo-handle-multi") export class DemoHandleMulti extends LitElement { // show: boolean, isIdle: boolean — fully typed from the keys @handle(config.onInactivity.stillHere.show, idle.isIdle) onInactivity(show: boolean, isIdle: boolean) { if (show === true && isIdle === true) this.openModal(); else this.closeModal(); } } Options (HandleOptions) Pass an options object as the last argument. The names map to real situations seen in the apps. waitForAllDefined Only call the method once all watched keys are defined (non null / undefined). This reproduces the historical @onAssign semantics — use it when the logic only makes sense with every source ready (e.g. building a date from date + timeZone + direction). @handle(trip.departureDate, trip.event.timeZone, form.direction, { waitForAllDefined: true, }) updateDepartureDate(date: number, timeZone: string, direction: string) { // called only when the three values are all available this.formDate = formatDate(date, timeZone, direction); } skip Do not call the method when a received value belongs to one of the listed categories (the Skip enum). Each entry is a named category — not a value — so there is no value/pattern ambiguity (e.g. {} is Skip.EmptyObject, an explicit "empty object" category, never a value comparison). | Category | Matches | | --- | --- | | Skip.Nullish | null or undefined (a publisher always emits null, never undefined) | | Skip.EmptyString | "" | | Skip.EmptyObject | object with no keys ({}), arrays excluded | | Skip.EmptyArray | empty array ([]) | Useful when a not-yet-initialized publisher emits {} as a "loading" state. For a specific value (e.g. a particular string), guard inside the method instead. @handle(user.profile, { skip: [Skip.Nullish, Skip.EmptyObject] }) onProfile(profile: Profile) { // not called while the publisher still holds {} (not loaded yet) this.displayName = profile.firstName; } > Options can be combined, e.g. @handle(a, b, { waitForAllDefined: true, skip: [Skip.Nullish] }). For any arbitrary validation on a specific value, just guard inside the method (if (!isValid(v)) return;) — that is exactly what an accept-style predicate would do, since @handle only runs your method. skipEmptyPlaceholder On a dynamic key path ("items.${itemId}"), if true, a placeholder resolved to '' is treated as not ready (no subscription) — empty string only, not 0 or false. See Dynamic path placeholders. Highlights - Strict typing: the method receives one argument per key, in order. - Up to 3 keys; for 4+ keys (rare), keep @onAssign for now. - By default the method runs on every assignment, even with null / undefined (unlike @onAssign, which waits for all values). Opt back into that behavior with waitForAllDefined. - skip filters out values by named category (e.g. [Skip.Nullish, Skip.EmptyObject]); for arbitrary checks on a specific value, guard inside the method. See also DataProviderKey and Dynamic path placeholders. --- ## @onAssign - URL: https://concorde.supersoniks.org/crawl/docs/_decorators/on-assign.html - Doc ID: docs/_decorators/on-assign - Keywords: Concorde, supersoniks, docs/_decorators/on-assign, on-assign, @onAssign, decorator, onAssign, DataProviderKey, set, get, User n°${userNumber}, user-${userNumber}@example.com @onAssign > New apps: use @handle with DataProviderKey (Data flow). @onAssign uses untyped string paths; it remains documented for existing codebases — see Migrating to @handle below. The @onAssign decorator allows you to execute a method when one or more publishers are updated. The method is called only when all specified publishers have been assigned values. For a typed equivalent (recommended), use @handle. Principle This decorator subscribes to one or more publishers by string path (legacy). When all specified publishers have been assigned values (via set), the decorated method is called with all the values as arguments. Prefer @handle + DataProviderKey and get / set from Data flow. This is particularly useful when you need to wait for multiple data sources to be ready before executing logic. Usage Import import { onAssign } from "@supersoniks/concorde/decorators"; Basic example //... @customElement("demo-on-assign") export class DemoOnAssign extends LitElement { static styles = [tailwind]; @state() userWithSettings: any = null; @state() isReady: boolean = false; @state() lastUpdate: string = ""; @onAssign("demoUser", "demoUserSettings") handleDataReady(user: any, settings: any) { this.isReady = Object.keys(user).length > 0 && Object.keys(settings).length > 0; this.userWithSettings = { ...user, ...settings }; this.lastUpdate = new Date().toLocaleTimeString(); this.requestUpdate(); } render() { const { name, email, theme, language } = this.userWithSettings; return //... } updateData() { const user = PublisherManager.get("demoUser"); const userSettings = PublisherManager.get("demoUserSettings"); const userNumber = Math.floor(Math.random() 100); user.set({ name: User n°${userNumber}, email: user-${userNumber}@example.com, }); userSettings.set({ theme: ["light", "dark", "auto"][Math.floor(Math.random() 3)], language: ["en", "fr", "es"][Math.floor(Math.random() 3)], }); } } Example with nested paths @customElement("product-view") export class ProductView extends LitElement { product: any = null; inventory: any = null; @onAssign("store.product", "store.inventory") handleProductData(product: any, inventory: any) { this.product = product; this.inventory = inventory; this.requestUpdate(); } render() { if (!this.product) return html Loading... ; const stock = this.inventory[this.product.id] || 0; return html ${this.product.name} Price: ${this.product.price}€ Stock: ${stock} ; } } Path syntax The path uses dot notation to navigate through the publisher structure: - First segment: dataProvider identifier (e.g., "userData") - Following segments: nested properties (e.g., "store.product") - Array access: use numeric index (e.g., "data.items.0") Dynamic path driven by class properties You can now build the paths dynamically by referencing the host class properties inside the strings passed to @onAssign. Two placeholder syntaxes are supported: - ${myProperty} or ${this.myProperty} - {$myProperty} Each placeholder is replaced at runtime with the current value of the corresponding property. @onAssign automatically watches those properties and: - re-evaluates the final paths when one of them changes, - removes the previous subscriptions before attaching the new ones, - observe the changes inside willUpdate(changedProperties) so nothing touches the getters/setters. While a placeholder is null/undefined, subscriptions are detached. Optional skipEmptyPlaceholder on @handle (typed replacement). Details: Dynamic path placeholders. @customElement("demo-on-assign-dynamic") export class DemoOnAssignDynamic extends LitElement { static styles = [tailwind]; @property({ type: String }) dataProvider: "demoUsers" | "demoUsersAlt" = "demoUsers"; @property({ type: Number }) userIndex: number = 0; @state() user: any = null; @state() userSettings: any = null; @onAssign("${dataProvider}.${userIndex}", "${dataProvider}Settings.${userIndex}") handleUserDataReady(user: any, settings: any) { this.user = user; this.userSettings = settings; } updateUserIndex(e: Event) { this.userIndex = parseInt((e.target as HTMLInputElement).value); } updateDataProvider(e: Event) { this.dataProvider = (e.target as HTMLSelectElement).value as | "demoUsers" | "demoUsersAlt"; } updateCurrentUserData() { const usersPublisher = PublisherManager.get(this.dataProvider); const settingsPublisher = PublisherManager.get( ${this.dataProvider}Settings ); const userPublisher = Objects.traverse( usersPublisher, [String(this.userIndex)] ) as PublisherProxy; const settingPublisher = Objects.traverse( settingsPublisher, [String(this.userIndex)] ) as PublisherProxy; if (userPublisher && settingPublisher) { // Générer de nouvelles données aléatoires const randomNames = [ { firstName: "Alice", lastName: "Wonder" }, { firstName: "Bob", lastName: "Builder" }, { firstName: "Charlie", lastName: "Chaplin" }, ]; const randomThemes = ["light", "dark", "auto"]; const randomLanguages = ["en", "fr", "es"]; const randomName = randomNames[Math.floor(Math.random() randomNames.length)]; const randomEmail = ${randomName.firstName.toLowerCase()}.${randomName.lastName.toLowerCase()}@example.com; const randomTheme = randomThemes[Math.floor(Math.random() randomThemes.length)]; const randomLanguage = randomLanguages[Math.floor(Math.random() randomLanguages.length)]; // Mettre à jour l'utilisateur directement const currentUser = userPublisher.get() || {}; userPublisher.set({ ...currentUser, firstName: randomName.firstName, lastName: randomName.lastName, email: randomEmail, }); // Mettre à jour les settings directement settingPublisher.set({ theme: randomTheme, language: randomLanguage, }); } } render() { return html <div class="flex flex-col gap-2"> <sonic-select label="Users set" @change=${this.updateDataProvider}> <option value="demoUsers">First set of users</option> <option value="demoUsersAlt">Second set of users</option> </sonic-select> <sonic-input type="number" .value=${this.userIndex} @input=${this.updateUserIndex} min="0" max="9" label="Index" class="block" ></sonic-input> <sonic-button @click=${this.updateCurrentUserData}> Update current user data </sonic-button> <div class="flex flex-col gap-2 border p-2"> <div> <sonic-icon name="user" library="heroicons"></sonic-icon> ${this.user?.firstName} ${this.user?.lastName} </div> <div> <sonic-icon name="envelope" library="heroicons"></sonic-icon> ${this.user?.email} </div> <div> Theme: ${this.userSettings?.theme} | Language: ${this.userSettings?.language} </div> </div> </div> ; } } > ⚠️ Use classic string literals: @onAssign("${dataProvider}.${profileId}", "settings.${profileId}"). Do not use template literals (backticks), otherwise JavaScript would try to interpolate the value immediately. Additional constraints: - The hosting class must expose a willUpdate(changedProperties?: Map ) method (LitElement already provides it) so that @onAssign can listen to dependency changes. - Dependencies need to be reactive (e.g. @property() on LitElement) or you must call this.requestUpdate("myProp") manually after changing them, otherwise willUpdate will never be notified. - If you use nested expressions like ${user.id}, the first segment (user) is the one being observed: you need to reassign this.user (e.g. with a new object) so that the binding can detect the change. Behavior - The method is called only when all specified publishers have been assigned values - The method receives the values of all publishers as arguments, in the same order as specified - Subscription happens at the time of connectedCallback - Unsubscription happens automatically at the time of disconnectedCallback - If a publisher doesn't exist yet, it will be created with the value null - The method is called with this bound to the component instance Use cases This decorator is particularly useful for: - Waiting for multiple data sources before rendering or executing logic - Synchronizing data from different publishers - Initializing components that depend on multiple data providers - Handling complex data dependencies where you need all data to be ready Complete example import { html, LitElement } from "lit"; import { customElement } from "lit/decorators.js"; import { onAssign } from "@supersoniks/concorde/decorators"; import { PublisherManager } from "@supersoniks/concorde/core/utils/PublisherProxy"; @customElement("order-summary") export class OrderSummary extends LitElement { order: any = null; customer: any = null; shipping: any = null; @onAssign("orderData", "customerData", "shippingData") handleOrderReady(order: any, customer: any, shipping: any) { this.order = order; this.customer = customer; this.shipping = shipping; this.requestUpdate(); } render() { if (!this.order || !this.customer || !this.shipping) { return html Loading order details... ; } return html Order #${this.order.id} Customer: ${this.customer.name} Shipping to: ${this.shipping.address} Total: ${this.order.total}€ ; } } // Somewhere in your code, update the publishers: const orderPub = PublisherManager.get("orderData"); const customerPub = PublisherManager.get("customerData"); const shippingPub = PublisherManager.get("shippingData"); // The method will be called only when all three are set: orderPub.set({ id: "123", total: 99.99 }); customerPub.set({ name: "John Doe", email: "john@example.com" }); shippingPub.set({ address: "123 Main St" }); // handleOrderReady will be called with all three values Migrating to @handle @handle is the typed successor of @onAssign. The key behavioral difference: @onAssign waits for all values to be defined before calling the method, whereas @handle calls it on every assignment by default. Use the waitForAllDefined option to keep the old semantics. Why migrate - Typed paths: keys are DataProviderKey , so the method arguments are strongly typed (no more any). - Explicit intent: waitForAllDefined and skip replace implicit behavior. - Single API: @handle covers the mono- and multi-path cases (up to 3 keys). Equivalent semantics (waitForAllDefined) // Before @onAssign("demoUser", "demoUserSettings") handleDataReady(user: any, settings: any) { / ... / } // After — same "wait for everything" behavior, but typed const user = new DataProviderKey<User>("demoUser"); const settings = new DataProviderKey<Settings>("demoUserSettings"); @handle(user, settings, { waitForAllDefined: true }) handleDataReady(user: User, settings: Settings) { / ... / } Single path // Before @onAssign("settings.modules.logsroute.enabled") onLogRoute(value: boolean) { / ... / } // After const settings = new DataProviderKey<AppSettings>("settings"); @handle(settings.modules.logsroute.enabled) onLogRoute(value: boolean) { / ... / } 4+ paths @handle is capped at 3 keys. For the rare case of 4 or more publishers, keep @onAssign for now, or split the logic into several @handle methods that each store their value and call a shared method (guarding against partial values). Notes - This decorator works with any component that has connectedCallback and disconnectedCallback methods (such as LitElement or components extending Subscriber) - The method is called synchronously when all publishers are ready - If you need to update the UI, remember to call this.requestUpdate() inside the method - For more information about publishers, see the documentation on Sharing data --- ## @patch - URL: https://concorde.supersoniks.org/crawl/docs/_decorators/patch.html - Doc ID: docs/_decorators/patch - Keywords: Concorde, supersoniks, docs/_decorators/patch, patch, @patch, decorator, API.patchDetailed, ApiResult | null, request, response, result, Endpoint, DataProviderKey, ApiResult, @supersoniks/concorde/decorators @patch Sends data through API.patchDetailed. Same model as @post: decorated property is ApiResult | null (request, response, result). Pass an Endpoint and a DataProviderKey for the request body. Import patch and ApiResult from @supersoniks/concorde/decorators. Configuration Same as @post / @get: scoped HTML.getApiConfiguration(host) or DataProviderKey as third argument. Options (PatchOptions) Same shape as PostOptions on @post (ApiSendOptions): refetchEveryMs, skipIfBodyMissing, autoPostOnBodyMutation, triggerKey, skipEmptyPlaceholder. See Dynamic path placeholders for ${sessionId} and empty-string behaviour. Import import { patch, type ApiResult } from "@supersoniks/concorde/decorators"; import { Endpoint } from "@supersoniks/concorde/utils/endpoint"; import { DataProviderKey } from "@supersoniks/concorde/dataProviderKey"; Example const patchBodyKey = new DataProviderKey<Partial<Resource>>("resourcePatch"); @patch( new Endpoint<Resource, { resourceId: string }>("resources/${resourceId}"), patchBodyKey, { skipEmptyPlaceholder: true }, ) @state() payload?: ApiResult<Resource> | null; See also - @post — POST (live demos) - @put — full replacement (PUT) - Dynamic path placeholders --- ## @post - URL: https://concorde.supersoniks.org/crawl/docs/_decorators/post.html - Doc ID: docs/_decorators/post - Keywords: Concorde, supersoniks, docs/_decorators/post, post, @post, decorator, API.postDetailed, ApiResult | null, request, response, result, Endpoint, DataProviderKey, ApiResult, @supersoniks/concorde/decorators @post Sends data through API.postDetailed. The decorated property is ApiResult | null: request, response, and typed result. Pass an Endpoint as the first argument and a DataProviderKey for the request body as the second. Import post and ApiResult from @supersoniks/concorde/decorators, and Endpoint from @supersoniks/concorde/utils/endpoint. Configuration Same as @get: scoped HTML.getApiConfiguration(host) by default, or DataProviderKey as third argument. See API configuration for mock demos. Optional PostOptions (third or fourth argument) | Option | Description | |--------|-------------| | refetchEveryMs | Re-post on an interval (ms). | | skipIfBodyMissing | Skip when body publisher is null/undefined (default: true). | | autoPostOnBodyMutation | Re-post when the body publisher mutates (default: true). false = manual via triggerKey.invalidate(). | | skipEmptyPlaceholder | If true, a placeholder resolved to '' blocks the request (empty string only — not 0 or false). See Dynamic paths. | | triggerKey | DataProviderKey — invalidate() re-runs the POST with the current body. | When the POST runs again - Body publisher onInternalMutation when autoPostOnBodyMutation is true (default). Plusieurs mutations synchrones dans la même frame sont regroupées en un seul POST via requestAnimationFrame. - Endpoint path dependency changes (${sessionId} on the host). - Configuration publisher mutation. - triggerKey publisher invalidate(). - refetchEveryMs timer (if set). Import import { post, publish, type ApiResult } from "@supersoniks/concorde/decorators"; import { Endpoint } from "@supersoniks/concorde/utils/endpoint"; import { DataProviderKey } from "@supersoniks/concorde/dataProviderKey"; Minimal example Mock service: POST /docs-mock-api/api/register (same route as sonic-submit demos). Publisher setup lives in decorators-demo-post.ts. const syncRequest = new DataProviderKey<SyncRequest>("syncRequest"); const syncTrigger = new DataProviderKey<number>("syncTrigger"); @post( new Endpoint<SyncResponse, { sessionId: string }>("sessions/${sessionId}/sync"), syncRequest, { triggerKey: syncTrigger }, ) @state() payload?: ApiResult<SyncResponse> | null; Live demos Dynamic endpoint path — changing sessionId on the host re-runs the POST (POST /docs-mock-api/api/sessions/{id}/sync): @post + @publish on the same property (see @publish): Stale responses are ignored if the path or generation changed before the request finished. Note: plusieurs composants @post sur la même page qui partagent le même bodyKey enverront chacun une requête à chaque mutation du body — utiliser une clé par composant (voir les deux démos live ci-dessus). Path placeholders (${sessionId}, …): Dynamic path placeholders. See also - @put · @patch — same decorator model, PUT / PATCH via putDetailed / patchDetailed --- ## @publish - URL: https://concorde.supersoniks.org/crawl/docs/_decorators/publish.html - Doc ID: docs/_decorators/publish - Keywords: Concorde, supersoniks, docs/_decorators/publish, publish, @publish, decorator, DataProviderKey, @bind, @subscribe, skipEmptyPlaceholder @publish Write-only binding: assigning to the property publishes to the DataProviderKey path. No read subscription (inverse of @subscribe). Similar to the “reflect” half of @bind without listening to the publisher. Import import { publish } from "@supersoniks/concorde/decorators"; import { sub } from "@supersoniks/concorde/directives"; import { DataProviderKey } from "@supersoniks/concorde/dataProviderKey"; Example type PublishDemoData = { email: string; message: string }; const publishDemoKey = new DataProviderKey ("publishDemo"); // @customElement("demo-publish") export class DemoPublish extends LitElement { @publish(publishDemoKey.email) @state() email = ""; // @publish(publishDemoKey.message) @state() message = ""; // render() { return html (this.email = (e.target as HTMLInputElement).value)} label="Email" > ${sub(publishDemoKey.email)} ; } } Dynamic paths use the same placeholder rules as @bind / @subscribe. Resolution and skipEmptyPlaceholder: Dynamic path placeholders. --- ## @put - URL: https://concorde.supersoniks.org/crawl/docs/_decorators/put.html - Doc ID: docs/_decorators/put - Keywords: Concorde, supersoniks, docs/_decorators/put, put, @put, decorator, API.putDetailed, ApiResult | null, request, response, result, Endpoint, DataProviderKey, ApiResult, @supersoniks/concorde/decorators @put Sends data through API.putDetailed. Same model as @post: decorated property is ApiResult | null (request, response, result). Pass an Endpoint and a DataProviderKey for the request body. Import put and ApiResult from @supersoniks/concorde/decorators. Configuration Same as @post / @get: scoped HTML.getApiConfiguration(host) or DataProviderKey as third argument. Options (PutOptions) Same shape as PostOptions on @post (ApiSendOptions): refetchEveryMs, skipIfBodyMissing, autoPostOnBodyMutation, triggerKey, skipEmptyPlaceholder. See Dynamic path placeholders for ${sessionId} and empty-string behaviour. Import import { put, type ApiResult } from "@supersoniks/concorde/decorators"; import { Endpoint } from "@supersoniks/concorde/utils/endpoint"; import { DataProviderKey } from "@supersoniks/concorde/dataProviderKey"; Example const bodyKey = new DataProviderKey<UpdatePayload>("resourceBody"); @put(new Endpoint<Resource, { resourceId: string }>("resources/${resourceId}"), bodyKey) @state() payload?: ApiResult<Resource> | null; Changing resourceId on the host re-runs the PUT when the path becomes ready — see Dynamic path placeholders. See also - @post — POST (live demos, polling, @publish) - @patch — partial update (PATCH) - Endpoint · API configuration --- ## @subscribe - URL: https://concorde.supersoniks.org/crawl/docs/_decorators/subscribe.html - Doc ID: docs/_decorators/subscribe - Keywords: Concorde, supersoniks, docs/_decorators/subscribe, subscribe, @subscribe, decorator, DataProviderKey, | Shape of the object at that path (, , , , …) | | **Key** | , — static path (, ) or dynamic (, (e.g. , ) — often filled via [ @subscribe Keeps a Lit property in sync with a read-only slice of the DataProvider store. You pass a DataProviderKey; when that path changes, the property updates and the component re-renders. Typical setup (same idea as My first component): | Piece | Role | |-------|------| | Type T | Shape of the object at that path (DocsUserData, { count: number }, …) | | Key | DataProviderKey — static path ("cart") or dynamic ("users.${userIndex}", "${dataProvider}") | | Scope on the host | Properties listed in U (e.g. dataProvider, userIndex) — often filled via @ancestorAttribute | | @subscribe(key) | Mirrors the store into @state() (or another property); read-only from the component side | For writing back to the store from component state, use @publish. In templates, the same paths work with sub(). Import import { subscribe } from "@supersoniks/concorde/decorators"; import { DataProviderKey } from "@supersoniks/concorde/dataProviderKey"; type Data = { count: number }; const dataKey = new DataProviderKey<Data>("data"); @subscribe(dataKey.count) @state() count = 0; Static path The key path is fixed. The property type must match T at that segment. const cartKey = new DataProviderKey<{ items: string[] }>("cart"); @subscribe(cartKey) @state() cart: { items: string[] } | null = null; Dynamic path and scope Placeholders ${prop} in the key string are resolved from properties on the same component. While a value is null/undefined, the subscription is inactive; optional { skipEmptyPlaceholder: true } also waits on "". Full rules: Dynamic path placeholders. Declare dynamic props in the key’s second generic so TypeScript expects them on the host: type User = { firstName: string; lastName: string; email: string }; @subscribe(new DataProviderKey<User, { userIndex: number }>("demoUsers.${userIndex}")) @state() user: User | null = null; @property({ type: Number }) userIndex = 0; When userIndex changes, @subscribe re-resolves the path and refreshes user. Row / ancestor scope List items (and wrappers like ) set which branch the child reads. Pattern from the tutorial: export const rowKey = new DataProviderKey< User, { dataProvider: string | null } >("${dataProvider}"); @ancestorAttribute("dataProvider") dataProvider: string | null = null; @subscribe(rowKey) @state() user: User | null = null; Demo See also - Data flow — overview - DataProviderKey — paths and host generics - Dynamic path placeholders — ${prop} resolution and skipEmptyPlaceholder - sub() — same paths inside html templates --- ## @awaitConnectedAncestors and @dispatchConnectedEvent - URL: https://concorde.supersoniks.org/crawl/docs/_decorators/wait-for-ancestors.html - Doc ID: docs/_decorators/wait-for-ancestors - Keywords: Concorde, supersoniks, docs/_decorators/wait-for-ancestors, wait-for-ancestors, @awaitConnectedAncestors and @dispatchConnectedEvent, decorator, awaitConnectedAncestors and @dispatchConnectedEvent, @awaitConnectedAncestors, @dispatchConnectedEvent, connectedCallback, sonic-connected, @dispatchConnectedEvent() @awaitConnectedAncestors and @dispatchConnectedEvent The @awaitConnectedAncestors and @dispatchConnectedEvent decorators delay a web component's initialization until its matching ancestors have executed their connectedCallback. This is when contextual elements (publisher, dataProvider, etc.) are configured. Principle When a child component attaches to the DOM, its ancestors may not yet be initialized (especially if custom element definitions are loaded asynchronously). The @awaitConnectedAncestors decorator delays the component's connectedCallback until all ancestors matching the provided CSS selectors have executed their connectedCallback. The @dispatchConnectedEvent decorator allows ancestors to signal they are ready by dispatching the sonic-connected event at the end of their connectedCallback. The event bubbles, so it can be listened to from anywhere (e.g. document.addEventListener(CONNECTED, handler)). Ancestors that are not web components (no hyphen in tag name) are considered connected by default and do not need to emit the event. Usage Import import { awaitConnectedAncestors, dispatchConnectedEvent, ancestorAttribute } from "@supersoniks/concorde/decorators"; Basic example An ancestor container decorated with @dispatchConnectedEvent() signals when it is ready. A child component decorated with @awaitConnectedAncestors("demo-wait-ancestor-container[dataProvider]") waits for this container to be initialized before initializing itself. Parameters are CSS selectors (element.matches()). The parent is registered via customElements.define() (vanilla JS) rather than @customElement, so it can be defined later—e.g. when the user clicks a button. This demonstrates the child waiting until the parent exists. import { html, LitElement } from "lit"; import { customElement, state } from "lit/decorators.js"; // Parent: registered later via customElements.define(), not @customElement @dispatchConnectedEvent() export class DemoWaitAncestorContainer extends LitElement { render() { return html ; } } // Child: waits for parent before initializing @customElement("demo-wait-ancestor-value") @awaitConnectedAncestors("demo-wait-ancestor-container[dataProvider]") export class DemoWaitAncestorValue extends LitElement { @ancestorAttribute("dataProvider") dataProvider: string | null = null; @state() initializedAt: string = ""; connectedCallback() { super.connectedCallback(); this.initializedAt = new Date().toISOString(); } render() { return html DataProvider from ancestor: ${this.dataProvider || "—"} Initialized at: ${this.initializedAt || "(waiting for parent…)"} ; } } // Demo section: register parent via customElements.define() when user clicks @customElement("demo-wait-ancestors-section") export class DemoWaitAncestorsSection extends LitElement { registerParent() { if (!customElements.get("demo-wait-ancestor-container")) { customElements.define("demo-wait-ancestor-container", DemoWaitAncestorContainer); } } render() { return html Register parent component ; } } Multiple ancestors The child waits for all specified ancestors. Register outer first, then inner — the child initializes only when both are ready. Ancestors already connected When the parent is defined at load and already in the DOM, the child initializes immediately (no delay). Static (both in DOM from start): Dynamic (child added on button click): CSS selector support Parameters are CSS selectors matched via element.matches() — e.g. "sonic-subscriber", "sonic-subscriber[dataProvider]", ".my-container", or multiple: "sonic-subscriber", "sonic-sdui". Behavior - Ancestor search uses CSS selectors (element.matches(selector)) — supports tag names, classes, attributes, combinators, etc. - Traversal includes shadow roots (parentNode / host) - Non-web components (no hyphen in tag name) are considered connected by default - For web component ancestors, it waits for customElements.whenDefined(tagName) and the sonic-connected event (or a timeout as fallback) - If no matching ancestor is found, the original connectedCallback is called immediately - Ancestors that do not emit sonic-connected trigger the fallback after the timeout (compatibility with existing components) Use cases These decorators are particularly useful for: - sonic-value inside a sonic-subscriber: the value waits for the subscriber to configure its publisher - Components inside sonic-sdui: wait for the SDUI to load and configure its context - Any component depending on context provided by an ancestor custom element Listening to the connected event The sonic-connected event bubbles, so you can listen to it from anywhere: import { CONNECTED } from "@supersoniks/concorde/decorators"; someConnectable.addEventListener(CONNECTED, (e) => { console.log("Component connected:", e.target); }); Notes - These decorators apply only to web components (classes extending HTMLElement) - The fallback timeout ensures compatibility with components that do not yet use @dispatchConnectedEvent - Traversal includes shadow roots - For a guarantee without relying on the timeout, decorate ancestors (Subscriber, Fetcher, etc.) with @dispatchConnectedEvent --- ## sub() - URL: https://concorde.supersoniks.org/crawl/docs/_directives/sub.html - Doc ID: docs/_directives/sub - Keywords: Concorde, supersoniks, docs/_directives/sub, sub, sub(), directive, ${prop}, @subscribe, null, "", skipEmptyPlaceholder, ${…}, get, set sub() Read-only Lit directive: displays the current DataProvider value and updates whenever it is assigned. Import import { sub } from "@supersoniks/concorde/directives"; String path render() { return html<p>Count: ${sub("myCounter.count")}</p>; } DataProviderKey (static) import { DataProviderKey } from "@supersoniks/concorde/dataProviderKey"; import { sub } from "@supersoniks/concorde/directives"; const counterKey = new DataProviderKey<{ count: number }>("myCounter"); render() { return html<p>${sub(counterKey.count)}</p>; } Dynamic DataProviderKey (${prop}) Like @subscribe: the path is resolved on the template host component; the directive re-subscribes when observed props change. Placeholder values (null, "", 0, …): Dynamic path placeholders (skipEmptyPlaceholder not available on sub() yet). import { DataProviderKey } from "@supersoniks/concorde/dataProviderKey"; import { sub } from "@supersoniks/concorde/directives"; type User = { firstName: string; lastName: string; email: string }; const userKey = new DataProviderKey<User, { userIndex: number }>( "demoUsers.${userIndex}", ); @customElement("demo-sub-dynamic") export class DemoSubDynamic extends LitElement { @property({ type: Number }) userIndex = 0; render() { return html <p>${sub(userKey.email)}</p> ; } } Demo Concatenation (forms) html<span>${sub(this.formDataProvider + ".email")}</span> sub() vs get() / set() / dp() | API | Context | Dynamic ${…} | |-----|----------|------------------| | sub() | Lit template, reactive | Yes | | get / set / dp | Imperative code | No | Do not replace sub(path) with get(path) in a template: get returns a one-time snapshot. See DataProviderKey, @subscribe, and the concorde-get-set-dp migration skill. --- ## AI agents (skills & AGENTS.md) - URL: https://concorde.supersoniks.org/crawl/docs/_getting-started/ai-agents.html - Doc ID: docs/_getting-started/ai-agents - Keywords: Concorde, supersoniks, docs/_getting-started/ai-agents, ai-agents, AI agents (skills & AGENTS.md), guide, AGENTS.md, yarn ai:sync, ai-init, yarn add @supersoniks/concorde, | | Concorde repo (contributors) | , --merge-agents, --overlay , --cursor-only AI agents (skills & AGENTS.md) Concorde ships skills, rules, and a root AGENTS.md so Cursor, JetBrains AI Assistant, and other coding agents follow framework patterns (DataProvider, decorators, short imports, scope, theme, migrations). On a starter project, run yarn ai:sync after install. On a manual / brownfield project, install Concorde first, then use ai-init below. Dedicated commands | Context | Command | |---------|---------| | create-concorde-ts-starter project | yarn ai:sync | | Any consumer app (after yarn add @supersoniks/concorde) | node nodemodules/@supersoniks/concorde/scripts/ai-init.mjs | | Concorde repo (contributors) | yarn ai-init | Starter (from project root) yarn ai:sync Manual / existing project node nodemodules/@supersoniks/concorde/scripts/ai-init.mjs Useful flags | Flag | Effect | |------|--------| | --merge-agents | Append to an existing AGENTS.md instead of replacing it | | --overlay | Copy extra rules/skills from another layer (e.g. starter kit) | | --cursor-only | Install only .cursor/skills and .cursor/rules | | --jetbrains-only | Install only .aiassistant/rules/ | What gets installed | Path | Role | |------|------| | AGENTS.md | Project-level guide for agents (Concorde conventions) | | .cursor/skills/concorde/ | Core Lit + DataProvider patterns | | .cursor/skills/concorde-imports/ | Short @supersoniks/concorde/… import paths | | .cursor/skills/concorde-scope/ | serviceURL, formDataProvider, API configuration | | .cursor/skills/concorde-theme/ | sonic-theme and --sc- tokens | | .cursor/skills/concorde-menu/ | sonic-menu navigation | | .cursor/skills/concorde-get-set-dp/ | get / set / dp and DataProviderKey migration | | .cursor/rules/concorde.mdc | Cursor rules (always-on patterns) | | .aiassistant/rules/concorde.md | JetBrains AI Assistant rules | Workflow 1. Starter project: Installation — yarn ai:sync is wired in the kit. 2. Existing / manual Vite project: Manual installation, then ai-init after yarn add @supersoniks/concorde. 3. Commit AGENTS.md, .cursor/, and .aiassistant/ if your team shares agent settings. 4. Re-run the same command when you bump @supersoniks/concorde to refresh skills from nodemodules/@supersoniks/concorde/ai/. 5. In Cursor, skills under .cursor/skills/ are picked up automatically; mention a skill in chat for focused tasks (e.g. migration → concorde-get-set-dp). Source files in the npm package: nodemodules/@supersoniks/concorde/ai/ (see also ai/README.md in the Concorde repository). --- ## Manual installation (Vite) - URL: https://concorde.supersoniks.org/crawl/docs/_getting-started/concorde-manual-install.html - Doc ID: docs/_getting-started/concorde-manual-install - Keywords: Concorde, supersoniks, docs/_getting-started/concorde-manual-install, concorde-manual-install, Manual installation (Vite), guide, tsconfig.json, AGENTS.md, --host, becomes , * , main.ts, my-element.ts Manual installation (Vite) > Legacy — for brownfield apps or custom stacks. New projects should use the starter. Brand New Vite Project Tailwind configuration is not covered here yet. Creating the Project Pure JavaScript project yarn create vite --template vanilla-js TypeScript project (it is recommended to use this approach, which installs Lit separately if needed, via "yarn add lit") yarn create vite --template vanilla-ts TypeScript + Lit project (creating new web components, for example, to extend the fetch or subscriber mixins) yarn create vite --template lit-ts Using Lit with TypeScript requires the following configuration in the "compilerOptions" section of the tsconfig.json file: "experimentalDecorators": true, "useDefineForClassFields": false Installing Concorde Navigate to the project folder created and perform the installation: yarn add @supersoniks/concorde Agent skills (AGENTS.md, Cursor / JetBrains): see AI agents — run node nodemodules/@supersoniks/concorde/scripts/ai-init.mjs after install. Development / Build Development (you can add --host after the command chain in package.json's dev script instead of typing it each time as shown below) yarn dev --host Build yarn build Shortcut Imports Shortcut imports work by default in JavaScript, but in TypeScript, they are activated by choice as they inject data into tsconfig.json. Here is the command: npx concorde enable-short-paths Shortcut imports replace the actual paths with aliases as follows: @supersoniks/concorde/core/components/functionalorui/component/component becomes @supersoniks/concorde/functionalorui/component @supersoniks/concorde/core/mixinsorutils/classname becomes @supersoniks/concorde/mixinsorutils/classname The original paths remain accessible. Shortcut imports are used for the examples later in this documentation. Usage Simple Integration in HTML Import needed component in main.ts or wherever you want to use it: import "@supersoniks/concorde/ui/button"; Then in the render function ofyour component or in the HTML of the web page that includes the compiled JS, use the component as follows: My button Using a Mixin to Create a New Lit Component For example, create a file my-element.ts at the root: import { html, LitElement } from "lit"; import { customElement, property } from "lit/decorators.js"; import Fetcher from "@supersoniks/concorde/mixins/Fetcher"; import Subscriber from "@supersoniks/concorde/mixins/Subscriber"; @customElement("my-element") export class SonicComponent extends Fetcher(Subscriber(LitElement)) { @property() email = ""; @property() firstname = ""; @property() lastname = ""; render() { return html ${this.firstname} ${this.lastname} : ${this.email} ; } } Import component main.ts or main.js or any other component that uses it to be compiled import "./my-element"; In the HTML of a JS or TS component or in the HTML of the web page containing the compiled JS: --- ## Installation - URL: https://concorde.supersoniks.org/crawl/docs/_getting-started/concorde-outside.html - Doc ID: docs/_getting-started/concorde-outside - Keywords: Concorde, supersoniks, docs/_getting-started/concorde-outside, concorde-outside, Installation, guide, yarn ai:sync Installation For almost all new projects, use create-concorde-ts-starter . You get Vite + TypeScript + Lit + Tailwind, an example component, npx concorde enable-short-paths ready to run, and yarn ai:sync for AI agent skills ( AGENTS.md , Cursor / JetBrains rules) without extra setup. Starter The following command creates a new Vite project in TypeScript mode with Tailwind and an example component to get started. npx @supersoniks/create-concorde-ts-starter "projectname" Then, from the project folder: yarn install yarn dev --host Next steps | Step | Page | |------|------| | Learn patterns | My first component | | Agent skills | AI agents (skills) — yarn ai:sync on the starter | | Brownfield / manual Vite | Legacy: Manual installation | --- ## Creating components - URL: https://concorde.supersoniks.org/crawl/docs/_getting-started/create-a-component.html - Doc ID: docs/_getting-started/create-a-component - Keywords: Concorde, supersoniks, docs/_getting-started/create-a-component, create-a-component, Creating components, guide, Subscriber, core, /components/destination/, /components/atoms, example.ts, ${this.text} Creating components > New app components: start with My first component and Data flow. The Subscriber mixin below is legacy. Where to put it? In this document, we consider the src directory of the project as the root. We describe how we organize our components as an example, however it depends on your project. In concorde each component is currently organized in the following directory structure (at least we try): - /core/components/functional/component-name/component-name.ts Generic/functional/generally unstyled component. The component usually doesn't use any other concrete components. - /core/components/ui/component-name/component-name.ts Generic but UI-oriented component. The component usually only uses generic components and other UI components. - la-billetterie/components/atoms/component-name/component-name.ts The component is intended to have a concrete usage within our ticketing system "la-billetterie". It usually only uses generic components from core and possibly other atoms. - la-billetterie/components/concrete-destination/component-name/component-name.ts The component has a specific destination: (event / cart / gift card): /components/destination/ The component uses various components, usually from the /components/atoms directory. It does not use components at the same level in the file hierarchy. Starting from a Simple Model You can copy example.ts from the source to the desired destination to start with. This file contains a web component in the form of a class that extends the Subscriber mixin, with a reactive property and a render function. import { html, LitElement } from "lit"; import { customElement, property } from "lit/decorators.js"; import Subscriber from "@supersoniks/concorde/core/mixins/Subscriber"; @customElement("sonic-example") export class SonicComponent extends Subscriber(LitElement) { @property() text = "Example"; render() { return html${this.text}; } } You can remove the dependency on Subscriber if automatic population of the component with external data is not required. For example, for a UI component: import { html, LitElement } from "lit"; import { customElement, property } from "lit/decorators.js"; @customElement("sonic-example") export class SonicComponent extends LitElement { @property() text = "Example"; render() { return html${this.text}; } } Regarding Subscriber, see: - 🔔 Subscriber - 🥨 Sharing Data Naming the Component The class name is not necessarily important. However, it is important to give it a component name prefixed with "sonic" (or a prefix of your own) using the dedicated metadata already present in the copied document. For example, a button component would be named as follows: @customElement("sonic-button") For less generic components with a specific destination, we advise to include the destination in the name. For example, for a "title" component in the "event" destination, the name would be simply: @customElement("sonic-event-title") Modifying It Creating Reactive Properties and Modifying the Render Function To do this, study the functioning of https://lit.dev and also refer to Subscriber. HTML Structure of a Component The HTML structure of a component should remain as simple as possible. Ideally, there should be only one additional level of elements in addition to slots. - The main component is already a wrapper. It receives classes to manage its layout, but it should not style internal elements. - Slots handle the visual organization of elements. - The specificity of the component is expressed in this additional level by adding a list, a functional native element (e.g., button), or another component. This leads to the creation of more components and thus raises questions about the hierarchical organization of files. However, this tends to atomize their roles. Referencing It To compile the component, it needs to be referenced somewhere through an import statement. In particular, it is important to reference it in any component that uses it. In the case where it can be directly used in a page, it should also be globally referenced, especially considering the creation of specific bundles in the future. Here's where we add imports based on the component's location inside concorde as an example - /core/components/functional/component-name/component-name.ts In /core/components/functional/functional.ts, which is referenced in core.ts and imported in index.ts. - /core/components/ui/component-name/component-name.ts In /core/components/ui/ui.ts, which is referenced in core.ts and imported in index.ts. - la-billetterie/components/atoms/component-name/component-name.ts Nowhere else but where it will be used, except for possible temporary tests, for example, in index.ts. - la-billetterie/components/concrete-destination/component-name/component-name.ts In la-billetterie/components/concrete-destination/destination-concrete.ts. If it's a new destination, you'll need to create the corresponding import .ts file and import it in la-billetterie.ts. Using It As a reminder, the component is simply integrated into the context by adding a tag with the component's name, for example: --- ## My first component - URL: https://concorde.supersoniks.org/crawl/docs/_getting-started/my-first-component.html - Doc ID: docs/_getting-started/my-first-component - Keywords: Concorde, supersoniks, docs/_getting-started/my-first-component, my-first-component, My first component, guide, @subscribe, src/docs/tailwind.ts, ${unsafeCSS(tailwindImport)}, DocsUserData, user, docsUserRowKey, "${dataProvider}", { dataProvider: string | null } My first component Build a Lit user card with Tailwind and Concorde UI, then connect it to the DataProvider store: declare the data configuration (type, key, scope), and let @subscribe keep the card in sync. > Legacy approach with the Subscriber mixin: Legacy: My first subscriber. 1. Lit component + Tailwind Export Tailwind once (e.g. src/docs/tailwind.ts in this repo): import { css, unsafeCSS } from "lit"; import tailwindImport from "./css/tailwind.css?inline"; export const tailwind = css${unsafeCSS(tailwindImport)}; User card — plain Lit properties and UI components (no store yet): import { html, LitElement } from "lit"; import { customElement, property } from "lit/decorators.js"; import { tailwind } from "../tailwind"; @customElement("docs-user") export class DocsUser extends LitElement { static styles = [tailwind]; @property({ type: String }) firstname = ""; @property({ type: String }) lastname = ""; @property({ type: String }) email = ""; @property({ type: String }) avatar = ""; render() { return html<div class="flex items-center gap-3 rounded-md p-2"> <sonic-image src=${this.avatar} rounded="md" ratio="1/1" class="w-16"></sonic-image> <div> <div>${this.firstname} <span class="font-bold">${this.lastname}</span></div> <div class="text-sm text-neutral-400">${this.email}</div> </div> </div>; } } 2. Data configuration The card does not hard-code user fields anymore. You declare what is stored, where it lives, and which ancestor scope applies — then @subscribe mirrors that object on the component. Type — shape of the data DocsUserData documents the fields you expect at this scope (first name, email, avatar, …). TypeScript checks that user matches that shape. export type DocsUserData = { firstname: string; lastname: string; email: string; avatar: string; }; Key — path in the DataProvider docsUserRowKey points at the store segment to read. The path "${dataProvider}" is resolved at runtime from a property on the component (see scope below). import { DataProviderKey } from "@supersoniks/concorde/core/utils/dataProviderKey"; export const docsUserRowKey = new DataProviderKey< DocsUserData, { dataProvider: string | null } >("${dataProvider}"); The second generic ({ dataProvider: string | null }) lists what the host must expose so "${dataProvider}" can be resolved. See DataProviderKey and Dynamic path placeholders. Scope — which store segment applies A parent sets dataProvider=${docsUserScopeAKey.path} (or a list row sets …/list-item/0). @ancestorAttribute copies that attribute onto this.dataProvider, so the key resolves to the correct branch. @ancestorAttribute("dataProvider") dataProvider: string | null = null; Subscribe — keep the card in sync @subscribe watches docsUserRowKey and updates user when the store changes. Use @state() so Lit re-renders. The template reads this.user like any other property. import { html, LitElement, nothing } from "lit"; import { customElement, state } from "lit/decorators.js"; import { ancestorAttribute, subscribe, } from "@supersoniks/concorde/core/decorators/Subscriber"; import { docsUserRowKey, type DocsUserData } from "./users"; import { tailwind } from "../tailwind"; @customElement("docs-user") export class DocsUser extends LitElement { static styles = [tailwind]; @ancestorAttribute("dataProvider") dataProvider: string | null = null; @subscribe(docsUserRowKey) @state() user: DocsUserData | null = null; render() { const u = this.user; if (!u) return nothing; return html<div class="flex items-center gap-3 rounded-md p-2"> <sonic-image src=${u.avatar} rounded="md" ratio="1/1" class="w-16"></sonic-image> <div> <div>${u.firstname} <span class="font-bold">${u.lastname}</span></div> <div class="text-sm text-neutral-400">${u.email}</div> </div> </div>; } } Live code: src/docs/example/users.ts. In this repo, imports use core/… paths (short @supersoniks/concorde/… exports apply in consumer apps only). Two scopes, one component The same docs-user is mounted twice. Each copy inherits a different nearest dataProvider — that is the row/list scope: Markup (two isolated stores; keys from docs-provider-keys.ts): <div class="grid md:grid-cols-2 gap-6"> <div dataProvider="${docsUserScopeAKey.path}"> <docs-user></docs-user> </div> <div dataProvider="${docsUserScopeBKey.path}"> <docs-user></docs-user> </div> </div> Seeded in docs-provider-keys.ts (set(docsUserScopeAKey, …)): Paul / Marie. Without @ancestorAttribute, both cards would not know which store to read. 3. Fetch users — sonic-list with .items Use a Lit parent and the items renderer (not HTML children). The callback receives each row object from fetch — render fields directly (${item.firstname}, …), like porting a template that used data-bind / . Wrapper (docs-users-list.ts): import { html, LitElement } from "lit"; import { customElement } from "lit/decorators.js"; import "../../core/components/functional/list/list"; import "./users"; @customElement("docs-users-list") export class DocsUsersList extends LitElement { private items = ({ firstname, lastname, email, avatar }) => html <div class="flex gap-3"> <sonic-image src=${avatar} ...></sonic-image> <div>${firstname} <b>${lastname}</b></div> <div class="text-sm text-neutral-400">${email}</div> </div> ; render() { return html <sonic-list fetch dataProvider=${usersListEndpoint.path} key="data" .items=${this.items}></sonic-list> ; } } See Local API demos for serviceURL="/docs-mock-api". 4. Form preview with formDataProvider Edit fields in a form; the card follows the same docsUserRowKey when the preview host sets dataProvider="userPreview". Preview Use formDataProvider + name on inputs — no manual @input handlers (Data flow). Next steps - Data flow — full map - DataProviderKey — typed paths and host constraints - @subscribe / sub() — read-only in templates - List — .items, .noItems, .skeleton --- ## Legacy: My first subscriber component - URL: https://concorde.supersoniks.org/crawl/docs/_getting-started/my-first-subscriber.html - Doc ID: docs/_getting-started/my-first-subscriber - Keywords: Concorde, supersoniks, docs/_getting-started/my-first-subscriber, my-first-subscriber, Legacy: My first subscriber component, guide, @ancestorAttribute, @subscribe, DataProviderKey, .items, ${unsafeCSS(tailwindImport)} Legacy: My first subscriber component > New projects: use My first component (@ancestorAttribute, @subscribe + DataProviderKey, .items on lists) and Data flow. This page documents the Subscriber mixin used by older apps and core components. Learn how to build a component with the Subscriber mixin, styled with Tailwind, filled from a DataProvider via automatic property mapping. Create a classic lit component import { html, LitElement, nothing } from "lit"; import { customElement, property } from "lit/decorators.js"; @customElement("docs-user") export class user extends LitElement { @property({ type: String }) firstname = ""; @property({ type: String }) lastname = ""; @property({ type: String }) avatar = ""; @property({ type: String }) email = ""; render() { return html <img src="${this.avatar}" /> <br> ${this.firstname} ${this.lastname} <br> ${this.email}; } } Style with tailwind and ui components import { css, unsafeCSS } from "lit"; import tailwindImport from "./css/tailwind.css?inline"; export const tailwind = css${unsafeCSS(tailwindImport)}; import { html, LitElement } from "lit"; import { customElement, property } from "lit/decorators.js"; import { tailwind } from "../tailwind"; import '@supersoniks/concode/ui/image' import '@supersoniks/concode/ui/button' import '@supersoniks/concode/ui/icon' @customElement("docs-user") export class user extends LitElement { static styles = [tailwind]; @property({ type: String }) firstname = ""; @property({ type: String }) lastname = ""; @property({ type: String }) avatar = ""; @property({ type: String }) email = ""; render() { return html<div class="flex items-center gap-3 p-2"> <sonic-image src=${this.avatar} rounded="md" ratio="1/1" class="w-16"></sonic-image> <div>${this.firstname} <span class="font-bold">${this.lastname}</span></div> <div class="text-sm text-neutral-400">${this.email}</div> </div>; } } Add Subscriber mixin import Subscriber from "@supersoniks/concorde/core/mixins/Subscriber"; @customElement("docs-user") export class user extends Subscriber(LitElement) { // properties auto-filled from DataProvider when names match } See Legacy: Subscriber mixin. Autofill from a dataProvider Preview before submit --- ## Legacy: Sharing data - URL: https://concorde.supersoniks.org/crawl/docs/_getting-started/pubsub.html - Doc ID: docs/_getting-started/pubsub - Keywords: Concorde, supersoniks, docs/_getting-started/pubsub, pubsub, Legacy: Sharing data, guide, get, set, DataProviderKey, PublisherManager, publisher.set, onAssign, {foo:{hello:["world"]}, bar:"baz"}, myPublisher.foo.hello, null Legacy: Sharing data > New apps: Data flow (get / set / DataProviderKey, decorators). This page documents the historical Publisher proxy API (PublisherManager, publisher.set, onAssign, …). This section describes how data is shared between graphical and non-graphical components. Graphical components should not reference each other directly — they stay decoupled via a publish/subscribe store. The Publisher Principle The publisher is a JavaScript proxy that holds data. Example: {foo:{hello:["world"]}, bar:"baz"} Accessing a property (myPublisher.foo.hello) returns another publisher for that segment. Missing properties create a publisher with internal data null, filled later. Subscribers listen via onAssign, onInternalMutation, template filling on the Subscriber mixin, etc. Updates use publisher.set(...) or nested assignment; changes propagate to subscribers. ❇️ Order of creation vs subscription theoretically does not matter. Methods set — replace internal value publisher.set({foo:{hello:["world"]}, bar:"baz"}); get — read internal value publisher.get() onAssign / offAssign — react to set publisher.a.b.onAssign(console.log); Modern equivalent: get / set / @handle — see Data flow. --- ## Introduction - URL: https://concorde.supersoniks.org/crawl/docs/_getting-started/start.html - Doc ID: docs/_getting-started/start - Keywords: Concorde, supersoniks, docs/_getting-started/start, start, Introduction, guide, get, set, formDataProvider, @ancestorAttribute, @bind, serviceURL, yarn ai:sync, AGENTS.md Introduction What is Concorde? Based on lit.dev, Concorde is a collection of web components for shared apps and websites. Build UIs without tying components to a specific host stack, while keeping visual consistency through a small set of CSS variables. Why and use case In 2022, Supersoniks needed a new ticketing stack for nearly 100 sites — mobile apps, modern sites, and legacy PHP without bundlers. One composable web-component library replaced many one-off apps. Lit was chosen for broad runtime compatibility. Stack Lit TypeScript Vite Tailwind (starter kit, not required in the core package) Functional features DataProvider store (decorators, get / set) Forms (formDataProvider) Lists, queues, lazy loading Router, states, UI kit with status variants Learn Concorde | Step | Page | |------|------| | 1 | My first component — Lit card, @ancestorAttribute, @bind, mock API | | 2 | Data flow — Core concepts: decorators and APIs | | 3 | Local API demos — offline serviceURL for this site (Misc) | | 4 | create-concorde-ts-starter — recommended project kit (Vite, Tailwind, yarn ai:sync) | | 5 | AI agents (skills) — AGENTS.md, Cursor / JetBrains rules | Legacy (Subscriber mixin, Publisher API): Legacy section at the bottom of the sidebar. Start a new project npx @supersoniks/create-concorde-ts-starter "projectname" The starter runs yarn ai:sync for you. Details: AI agents (skills). Manual Vite setup: Legacy: Manual installation. --- ## Adding styles - URL: https://concorde.supersoniks.org/crawl/docs/_getting-started/theming.html - Doc ID: docs/_getting-started/theming - Keywords: Concorde, supersoniks, docs/_getting-started/theming, theming, Adding styles, guide, , size, {reflect: true}, :host(), :host([type='primary']){...}, inline, block, display, contents Adding styles Normal Behavior No style crosses the shadow root of a component, except for inheritable properties (which have the "inherit" property possible) and CSS variables. Properties integrated via a "slot" remain stylizable in a conventional way. - During creation / from the inside: - We edit the static "styles" property of the lit component, which can also reference shared and dynamic styles. These styles are scoped and do not impact the outside. - We use CSS variables as the value of properties intended to be customized from the outside. For each variable, we define a default value to have a simple but consistent base style. - We only rewrite inheritable properties with hard values if they are truly specific to the component. - The tag as a direct child can be used as a last resort, especially if there is a need for particularly dynamic customization. Performance is reduced. - During use / from the outside: We define values for inheritable CSS properties (e.g., font-\, color...) using tools like Tailwind. We modify the value of CSS variables used by the component. Choosing Style Presets via Reactive Properties: The declaration of reactive properties is useful for selecting a particular configuration that mostly affects a set of properties. For example, a size property (xs, sm, md, xl) will affect margins, font, line heights to align them with the corresponding CSS vars, which can be customized using the methods mentioned earlier if necessary. It is recommended to use the {reflect: true} property for reactive properties that have an associated style on the :host(). For example: :host([type='primary']){...} ☢️ Caution: Passing class names via reactive properties / HTML attributes of the component should be avoided as it can quickly lead to difficult-to-manage situations. CSS "display" Property By default, the display property is inline. Therefore, be careful to define it according to the needs, as one might mistakenly expect it to be block as with a regular . ☢️ Caution: Defining the display property as contents may seem attractive at first, but: - It almost always leads to the creation of wrappers to style the content (instead of using :host). - It is no longer possible to directly add classes to the component or style it from the outside. Ultimately, the amount of code to write increases significantly. TAILWIND Functional Classes tailwind has been integrated into Concorde and is available in scoped components (with Shadow DOM). To use it, you need to import the following: import { tailwind } from "@supersoniks/concorde/la-billetterie/ui/theme/theme"; Then include the tailwind style in the static styles property of the component: static styles = [tailwind]; Finally, use it in the HTML within the render function: A paragraph with margin The colors from Concorde's theme are referenced in Tailwind's theme. Operation without Shadow DOM Usefulness This operation is particularly useful when it comes to adding behavior to a simple existing element. It may also become necessary to establish compatibility with a traditional JS library. For example, with a text input: - Trigger automatic data or filter update when typing text. - Automatic formatting. - Constraints that cannot be handled by native methods. Consequences If there is no shadow DOM (see the noShadowDom property of Subscriber): - Styling using the static "styles" property of the lit component will not be applied. - The element and its content can be styled in a traditional manner. For example, the components queue, list, and fetch do not have a shadow DOM. ℹ️ Note: Specifically in this case, it may be useful to set the display property to contents. --- ## API configuration - URL: https://concorde.supersoniks.org/crawl/docs/_misc/api-configuration.html - Doc ID: docs/_misc/api-configuration - Keywords: Concorde, supersoniks, docs/_misc/api-configuration, api-configuration, API configuration, reference, APIConfiguration, HTML.getApiConfiguration, API, wording(), @get, @post, @put, @patch, serviceURL API configuration APIConfiguration is the object built by HTML.getApiConfiguration from ancestor attributes on the DOM (or from a typed publisher — see @get / @post configuration key). It is passed to API by fetchers, sonic-submit, the wording() directive, @get, @post, @put, and @patch. > Mock service: same Local API demos Service Worker / Vite middleware. Routes used on this page are listed in the API config routes section below. Attribute map | Attribute (ancestor) | APIConfiguration field | Role | |---------------------|---------------------------|------| | serviceURL | serviceURL | Base URL (e.g. /docs-mock-api) | | token | token | Static Bearer sent on REST calls | | userName / password | userName / password | Basic auth for tokenProvider fetch only | | eventsApiToken | authToken | Bearer for tokenProvider when no Basic | | tokenProvider | tokenProvider | Path to GET a new token ({ token } JSON) | | wordingProvider | — (read by wording()) | Base path + query for label batch GET | | wordingVersionProvider | — | Publisher id; bump version → reload wordings | | credentials | credentials | fetch credentials mode | | addHTTPResponse | addHTTPResponse | Attach sonichttpresponse on result | | cache | cache | fetch cache mode | | blockUntilDone | blockUntilDone | Serialize calls per serviceURL | | keepAlive | keepAlive | fetch keepalive | Lit / TypeScript: store the same shape in a publisher and pass DataProviderKey as the second argument of @get (see DataProviderKey). Bearer token (static) Publisher docsApiConfBearerKey (set(docsApiConfBearerKey, { token: "docs-mock-valid-token", … })) — mock returns the protected payload. tokenProvider + Basic auth No static token: API.auth() calls GET /docs-mock-api/auth/token with Basic demo / secret, stores token, then calls the protected route. HTTP 498 — stale token refresh Initial token="docs-mock-stale-token" → mock responds 498 → Concorde invalidates the token, runs auth() again (same tokenProvider + Basic), retries with docs-mock-fresh-token. eventsApiToken Attribute eventsApiToken on an ancestor maps to authToken in config (used as Bearer when calling tokenProvider, instead of Basic). Wording API wordingProvider="wording/labels?lang=fr" + wording('api-config.greeting') in Lit. Mock returns label map from labels[] query params. Scoped attributes (HTML / sonic-scope) Attributes on sonic-scope (or any ancestor) are visible to descendants via getAncestorAttributeValue. API config routes | Route | Demo | |-------|------| | GET /docs-mock-api/auth/token | tokenProvider, eventsApiToken | | GET /docs-mock-api/api/config/protected | Bearer / Basic / 498 | | GET /docs-mock-api/wording/labels?labels[]=…&lang=fr | wording() | Mock tokens (doc only): docs-mock-valid-token, docs-mock-stale-token (498), docs-mock-fresh-token (after refresh). Basic: demo / secret. Events token: docs-mock-events-token. Implementation: src/docs/mock-api/api-config-mock.ts (bundled in the Service Worker with router.ts). See also - @get — APIConfiguration + Endpoint - @post — same configuration model for POST - @put · @patch — PUT / PATCH - Endpoint — typed path - Local API demos — offline serviceURL - Fetch — attribute table (legacy sonic-fetch) --- ## DataProviderKey - URL: https://concorde.supersoniks.org/crawl/docs/_misc/dataProviderKey.html - Doc ID: docs/_misc/dataProviderKey - Keywords: Concorde, supersoniks, docs/_misc/dataProviderKey, dataProviderKey, DataProviderKey, reference, toString(), path, myKey.items[0], items, Item[], .path DataProviderKey The DataProviderKey utility provides type-safe navigation through composite data structures. Each property or index access extends the path, and the final key can be retrieved via toString() or the path property. For a single HTTP path string (no dot-syntax), see Endpoint. For DataProviderKey , see API configuration. Principle DataProviderKey uses a Proxy to intercept property access and build a cumulative path string. TypeScript infers the nested type at each level, so myKey.items[0] is correctly typed as DataProviderKey when items is Item[]. In Lit demos, bind HTML attributes from a key’s .path (single source of truth), and use get / set / dp instead of PublisherManager.get("…"): import { DataProviderKey } from "@supersoniks/concorde/dataProviderKey"; import { get, set } from "@supersoniks/concorde/utils"; export const myFormKey = new DataProviderKey<{ email: string }>("myForm"); set(myFormKey, { email: "a@b.c" }); // template: formDataProvider=${myFormKey.path} Usage Import import { DataProviderKey } from "@supersoniks/concorde/dataProviderKey"; Basic example type Item = { id: string; name: string }; // type Data = { items: Item[]; count: number; }; // const myKey = new DataProviderKey<Data>("data").items[0]; // Equivalent to: new DataProviderKey<Item>("data.items.0") myKey.toString(); // "data.items.0" myKey.path; // same value // myKey is typed as DataProviderKey<Item> Object property access const key = new DataProviderKey<Data>("data"); const countKey = key.count; countKey.path; // "data.count" countKey.toString(); // "data.count" Array index access const itemsKey = new DataProviderKey<Data>("data").items; itemsKey.path; // "data.items" // itemsKey is DataProviderKey<Item[]> // const firstItem = itemsKey[0]; firstItem.path; // "data.items.0" // firstItem is DataProviderKey<Item> Dynamic paths Use placeholders ${prop} or {$prop} in the path string. The path is resolved at runtime from the component's properties. See Dynamic path placeholders for resolution rules and skipEmptyPlaceholder. The type remains declarative: type User = { name: string; email: string }; // // Path resolved from component.userIndex at runtime @subscribe(new DataProviderKey<User>("users.${userIndex}")) @state() user: User | null = null; Dynamic keys are not supported by get, set, or dp — those APIs take a snapshot at call time with no component context. For dynamic paths use decorators (@subscribe, @publish, @handle) or sub(key) in Lit templates (resolves ${…} from the host component). See sub(). get / set / dp with static keys For programmatic access, pass a DataProviderKey or a static path string. Dynamic placeholders are rejected: import { dp, get, set } from "@supersoniks/concorde/utils"; import { DataProviderKey } from "@supersoniks/concorde/dataProviderKey"; // const counterKey = new DataProviderKey<{ count: number }>("myCounter"); // set(counterKey, { count: 0 }); dp(counterKey.count).set(1); get(counterKey); // snapshot: { count: 1 } Path retrieval The final path is built by concatenating each accessed property with a dot: - new DataProviderKey ("base") → "base" - key.prop → "base.prop" - key.items[0] → "base.items.0" Use toString() or path to get the full path string: const key = new DataProviderKey<Data>("data").count; const pathString = key.toString(); // "data.count" const pathProp = key.path; // "data.count" Use cases - Type-safe bindings: paths for @bind, @subscribe, @publish, @handle - Dynamic paths: reusable keys with ${...} placeholders - Form fields: form data paths with compile-time checking Integration with @subscribe, @publish and @handle Use DataProviderKey with @subscribe (read-only), @publish (write-only), or @handle (method callback on assign). With @subscribe / @publish, the decorated property must match the key’s value type. With @handle, the method receives (value: T). import { subscribe } from "@supersoniks/concorde/decorators"; import { DataProviderKey } from "@supersoniks/concorde/dataProviderKey"; // type FormData = { email: string }; const formKey = new DataProviderKey<FormData>("formData"); // @customElement("user-form") export class UserForm extends LitElement { @subscribe(formKey.email) @state() email = ""; // render() { return html<input .value=${this.email} @input=${(e) => this.email = (e.target as HTMLInputElement).value}>; } } These decorators support dynamic paths: "base.${prop}" in the constructor. A wrong property type (e.g. number for DataProviderKey ) is a TypeScript error. Resolution rules: Dynamic path placeholders. See @handle for method callbacks. Notes - Function properties are excluded from navigation (no key.method() chaining) - Primitives have no navigable properties - The path property and toString() are equivalent for retrieving the key --- ## Local API demos (offline) - URL: https://concorde.supersoniks.org/crawl/docs/_misc/docs-mock-api.html - Doc ID: docs/_misc/docs-mock-api - Keywords: Concorde, supersoniks, docs/_misc/docs-mock-api, docs-mock-api, Local API demos (offline), reference, yarn dev, /docs-mock-api/*, registerDocsMockApiServiceWorker(), /docs-mock-api-sw.js, /docs-mock-api/..., serviceURL="/docs-mock-api", src/docs/mock-api/urls.ts, DOCS_MOCK_REQRES_SERVICE, DOCS_MOCK_GEO_SERVICE, /docs-mock-api/api/users Local API demos (offline) The Concorde doc site does not call fragile third-party APIs during yarn dev. A Service Worker and the Vite dev middleware serve /docs-mock-api/ on the same origin. How it works 1. On load, registerDocsMockApiServiceWorker() registers /docs-mock-api-sw.js. 2. Requests to /docs-mock-api/... are handled by the SW (production build) or Vite middleware (dev). 3. Live examples use serviceURL="/docs-mock-api" and relative paths. TypeScript constants: src/docs/mock-api/urls.ts (DOCSMOCKREQRESSERVICE, DOCSMOCKGEOSERVICE, …). Routes | Path | Used in | |------|---------| | /docs-mock-api/api/users | sonic-list, sonic-fetch, sonic-queue (users) | | /docs-mock-api/api/users/:id | Single user, docs-user | | POST /docs-mock-api/api/register | sonic-submit — JSON, multipart/form-data (sendAsFormData), or application/x-www-form-urlencoded (native ); response includes parsed email + token | | POST /docs-mock-api/api/register/nested | Wrapped body { data: { id, token } } for submit-result-key demo | | GET /docs-mock-api/api/register/echo | Echoes query string (method="get" on submit) | | GET /docs-mock-api/auth/token | API configuration — tokenProvider (Basic or eventsApiToken) | | GET /docs-mock-api/api/config/protected | Bearer / Basic; docs-mock-stale-token → 498 | | GET /docs-mock-api/wording/labels | Wording batch (labels[], lang) | | /docs-mock-api/geo/communes | Geo list, @get demos | | /docs-mock-api/jokes/joke/:category | JokeAPI-shaped list (key="jokes" on queue/list) | Filtres GET /jokes/joke/… | Query | Meaning | |-------|---------| | amount | Max jokes per request when not using offset | | offset + limit / perpage | Pagination for sonic-queue lazy load | | contains | Substring on joke text, setup/delivery, categories (input demo, name="contains") | | lang | fr or en — filters the local dataset (select demo, name="lang") | | blacklistFlags | Comma-separated flags to exclude (nsfw, religious, political, racist, sexist, explicit) — checkbox/radio/switch « Remove following jokes » demos; each joke has matching flags in fixtures | Pagination GET /api/users | Query | Meaning | |-------|---------| | offset + perpage | Index-based slice on the filtered set — sonic-queue (offset=$offset&perpage=$limit) | | page + perpage | 1-based page — static fetch examples (?page=2) | | limit | Alias of perpage | | q | Search on first name, last name, email (optional; sonic-queue + dataFilterProvider, field name="q") | ALTCHA (sonic-captcha) is not mocked — it still uses the Supersoniks service. In TypeScript demos, import from src/docs/mock-api/urls.ts (e.g. DOCSMOCKREQRESSERVICE on serviceURL). Source files - src/docs/mock-api/ — router, fixtures, service worker, urls.ts - scripts/docs-mock-api-vite-plugin.ts — SW build + middleware - public/docs-mock-api-sw.js — generated on yarn dev / yarn build-docs yarn dev # mock API on by default yarn build-docs # includes SW bundle --- ## Dynamic path placeholders - URL: https://concorde.supersoniks.org/crawl/docs/_misc/dynamic-path.html - Doc ID: docs/_misc/dynamic-path - Keywords: Concorde, supersoniks, docs/_misc/dynamic-path, dynamic-path, Dynamic path placeholders, reference, DataProviderKey, ${prop}, {$prop}, "users/${userId}", "api/sessions/${sessionId}/sync", "teams.${teamId}.members", resolveDynamicPath, userId, sessionId, requestAnimationFrame Dynamic path placeholders Decorators and DataProviderKey paths can include placeholders resolved on the host component at runtime: - ${prop} or {$prop} — e.g. "users/${userId}", "api/sessions/${sessionId}/sync" - Nested expressions — e.g. "teams.${teamId}.members" Resolution is done by resolveDynamicPath. The root property names (userId, sessionId, …) are watched via requestAnimationFrame (see dynamicPropertyWatch.ts). Default behaviour (ready / not ready) | Placeholder value | Path ready? | Inserted segment | Notes | |-------------------|---------------|------------------|-------| | undefined | no | — | Wait until defined | | null | no | — | Same as undefined | | "" | yes | empty string | e.g. sessions//sync — request may still run | | 0 | yes | "0" | Not treated as “missing” | | false | yes | "false" | | | 42, "alpha" | yes | "42", "alpha" | | When ready: false, decorators do not call the network (for @get / @post / @put / @patch), unsubscribe (@bind / @subscribe), or skip publisher binding (@publish / @handle). The decorated property is often left unchanged or set to undefined (HTTP decorators). When the placeholder later becomes valid, observers run again and behaviour resumes. skipEmptyPlaceholder option Some APIs should not run while an id is still "". Opt in per decorator: @get(new Endpoint ("users/${userId}"), { skipEmptyPlaceholder: true, }) | Option | Scope | |--------|--------| | skipEmptyPlaceholder?: boolean | Only empty string '' on a placeholder | | Default false | "" is inserted into the path (legacy behaviour) | | true | "" → ready: false, same as null / undefined for that segment | Does not affect 0, false, null, or undefined (nullish stays “not ready” regardless). Available on: | API | Where | |-----|--------| | @get | 2nd or 3rd argument (GetOptions) | | @post / @put / @patch | PostOptions / ApiSendOptions | | @bind / @subscribe | BindOptions | | @publish | 2nd argument PublishOptions | | @handle | HandleOptions | Per-decorator summary | Decorator | ready: false | ready: true | |-----------|----------------|---------------| | @get | No HTTP; payload → undefined | ApiResult assigned | | @post / @put / @patch | No HTTP; payload → undefined | ApiResult assigned | | @subscribe / @bind | Unsubscribe; prop keeps last value | Subscribe onAssign | | @publish | Internal publisher null; writes ignored | publisher.set on assign | | @handle | No subscription; with waitForAllDefined, method not called | Callback on assign | See also - DataProviderKey — path syntax - Endpoint — HTTP paths + host generic U - HTTP: @get · @post · @put · @patch - Store: @subscribe · @bind · @publish · @handle - Legacy strings: @onAssign (dynamic paths section) - Templates: sub() — dynamic paths in Lit (no skipEmptyPlaceholder yet) - Tutorial: My first component — "${dataProvider}" scope pattern --- ## Endpoint - URL: https://concorde.supersoniks.org/crawl/docs/_misc/endpoint.html - Doc ID: docs/_misc/endpoint - Keywords: Concorde, supersoniks, docs/_misc/endpoint, endpoint, Endpoint, reference, API.get, (default , ${…}, {$…}, null, undefined, "", and , userId Endpoint Endpoint describes a single HTTP path (or a path accepted by API.get) and carries the expected response type T. Unlike DataProviderKey, there is no dot-navigation: the path is one string. The optional second generic U (default any) describes host properties used to resolve dynamic segments in the path (${…} / {$…}), for example with @get or @post. See Dynamic path placeholders for null / undefined / "" / 0 and skipEmptyPlaceholder. Import import { Endpoint } from "@supersoniks/concorde/utils/endpoint"; Construction const users = new Endpoint<User[]>("users?limit=10"); users.path; // "users?limit=10" const one = new Endpoint<User, { userId: string }>("users/${userId}"); // userId on the host class is observed when used with @get Normalization Endpoint.normalizePath trims the string, rejects an empty path, strips leading slashes for paths relative to serviceURL, collapses duplicate slashes, and validates absolute http(s):// URLs. Publisher key for payloads getDataProviderKey() returns a typed publisher key whose path matches the endpoint path (payload typing follows ApiResult for this endpoint). Useful when pairing @get with @publish / @subscribe (see @get). Data-provider paths Endpoint.looksLikeDataProviderPath(path) returns true for strings shaped like dataProvider(id)…, which API.get can resolve without HTTP. See also - API configuration — serviceURL, token, wording (mock demos) - @get — GET decorator - @post — POST decorator (body from a DataProviderKey) - @put · @patch — PUT / PATCH decorators - DataProviderKey — typed publisher paths (dot notation) --- ## HTML integration (no Lit) - URL: https://concorde.supersoniks.org/crawl/docs/_misc/html-integration.html - Doc ID: docs/_misc/html-integration - Keywords: Concorde, supersoniks, docs/_misc/html-integration, html-integration, HTML integration (no Lit), reference, data-bind, dataProvider, formDataProvider, src/docs/example/, , src/docs/example/*.ts, @subscribe HTML integration (no Lit) Some hosts (legacy PHP pages, static HTML) embed Concorde components without a Lit build step. They can use HTML attributes such as data-bind, dataProvider, and formDataProvider on tags directly in the page. That style is not what we demonstrate in this doc site: live examples are Lit components under src/docs/example/, registered once and reused from Markdown via tags like . | Goal | Use | |------|-----| | Learn modern patterns | My first component, Data flow | | Author doc examples | + src/docs/example/.ts — see My first component | | Embed in plain HTML only | data-bind / Subscriber docs in Legacy — Subscriber mixin | New application code in TypeScript should use @subscribe, formDataProvider + name, and Lit templates — not HTML data-bind.