Wiki source code of Live Data Extension Points
Last modified by Eleni Cojocariu on 2026/09/11 15:51
Show last authors
| author | version | line-number | content |
|---|---|---|---|
| 1 | Live Data extends on both sides of its JSON configuration: on the server by contributing a [[Component>>doc:extensions:Extension.Component Module]], in the browser by registering a component on the widget. A configuration naming the new hint is enough to reach it. | ||
| 2 | |||
| 3 | |=Extension point|=Kind|=Looked up under|=Contributes | ||
| 4 | |##LiveDataSource##|Java Component|The value of the ##source## Macro parameter|A new origin for the entries and the property descriptors | ||
| 5 | |##LiveTableNewRowNamingStrategy##|Java Component|The value of the ##newRowNamingStrategy## source parameter|A way of naming the pages created for new entries by the Live Table source | ||
| 6 | |##registerPanel##|JavaScript|N/A|A new panel in the Live Data menu | ||
| 7 | |##componentStore.register##|JavaScript|A kind and a name|A new displayer, filter or layout | ||
| 8 | |||
| 9 | == Adding a Source == | ||
| 10 | |||
| 11 | A source is what puts a set of entries within reach of page authors: register a Component under a hint, and that hint becomes a value they can pass as the ##source## Macro parameter. The Component itself is small, and hands the real work to two stores. | ||
| 12 | |||
| 13 | {{code language="java"}} | ||
| 14 | @Role | ||
| 15 | public interface LiveDataSource | ||
| 16 | { | ||
| 17 | LiveDataEntryStore getEntries(); | ||
| 18 | |||
| 19 | LiveDataPropertyDescriptorStore getProperties(); | ||
| 20 | } | ||
| 21 | {{/code}} | ||
| 22 | |||
| 23 | === Reading and Writing the Entries === | ||
| 24 | |||
| 25 | ##LiveDataEntryStore## is where the data comes from. Only the two read methods have to be written; every other method has a default implementation, and the two that make the Live Data writable throw ##UnsupportedOperationException## by default. A store that keeps those defaults gives a Live Data the reader can browse but not change. Each method also throws ##LiveDataException##. | ||
| 26 | |||
| 27 | |=Method|=Returns|=Write it | ||
| 28 | |##get(LiveDataQuery)##|The entries matching the query, and how many there are in total|Always | ||
| 29 | |##get(Object entryId)##|One entry, by its id|Always | ||
| 30 | |##get(Object entryId, String property)##|One value of one entry|Only to avoid fetching the whole entry, which is what the default does | ||
| 31 | |##save(Map)##|The id of the entry that was stored|To let the reader create entries, and to let them [[edit values in place>>doc:documentation.xs.user.livedata.edit-value-place.WebHome]], which the default ##update## goes through | ||
| 32 | |##update(Object, String, Object)##|The value the property had before|Only to replace the default, which reads the entry, sets the property and saves it | ||
| 33 | |##remove(Object entryId)##|The entry that was removed|To let the reader delete entries | ||
| 34 | |||
| 35 | === Declaring the Properties === | ||
| 36 | |||
| 37 | ##LiveDataPropertyDescriptorStore## decides what the widget knows about each property: its type, whether it can be sorted, filtered or edited, and which displayer and filter it uses. What this store returns becomes the ##meta.propertyDescriptors## of the [[Live Data Configuration>>doc:documentation.xs.dev.livedata.configuration.WebHome]], so it is what a reader sees in the [[Properties, Sort and Filter panels>>doc:documentation.xs.user.livedata.panels.WebHome]]. | ||
| 38 | |||
| 39 | |=Method|=Returns|=Write it | ||
| 40 | |##get()##|Every property the entries may have|Always | ||
| 41 | |##get(String propertyId)##|One descriptor|Only to replace the default, which picks it out of the list above | ||
| 42 | |##save(LiveDataPropertyDescriptor)##|Whether the descriptor was stored|To let properties be redefined at runtime | ||
| 43 | |##remove(String propertyId)##|The descriptor that was removed|To let properties be dropped at runtime | ||
| 44 | |||
| 45 | == Naming the Pages Created for New Entries == | ||
| 46 | |||
| 47 | {{version since="18.7.0RC1"}} | ||
| 48 | For the live table source, new row creation requires a page naming strategy. For that, there is a specific interface: | ||
| 49 | |||
| 50 | {{code language="java"}} | ||
| 51 | /** | ||
| 52 | * Strategy to generate the document reference of a new livetable entry. | ||
| 53 | * | ||
| 54 | * @since 18.7.0RC1 | ||
| 55 | */ | ||
| 56 | @Role | ||
| 57 | @Unstable | ||
| 58 | public interface LiveTableNewRowNamingStrategy | ||
| 59 | { | ||
| 60 | /** | ||
| 61 | * Generates a document reference for a new livetable entry. | ||
| 62 | * | ||
| 63 | * @param parameters the livedata source parameters | ||
| 64 | * @return the generated document reference | ||
| 65 | * @throws LiveDataException if the reference cannot be generated | ||
| 66 | * @throws XWikiException if there is a wiki-level error | ||
| 67 | */ | ||
| 68 | DocumentReference generate(Map<String, Object> parameters) throws LiveDataException, XWikiException; | ||
| 69 | |||
| 70 | /** | ||
| 71 | * Checks if the current user is allowed to create a new entry based on the provided source parameters. | ||
| 72 | * | ||
| 73 | * @param parameters the live data source parameters | ||
| 74 | * @return whether the current user is allowed to create a new entry with this strategy | ||
| 75 | */ | ||
| 76 | boolean isCreationAllowed(Map<String, Object> parameters); | ||
| 77 | } | ||
| 78 | {{/code}} | ||
| 79 | |||
| 80 | New strategies can be added by creating new named components implementing this interface, and naming them in the ##newRowNamingStrategy## source parameter of a Live Data. | ||
| 81 | {{/version}} | ||
| 82 | |||
| 83 | == Adding a Panel == | ||
| 84 | |||
| 85 | A panel is registered on an instance with ##registerPanel##, from a listener of the ##xwiki:livedata:instanceCreated## event. The reader then toggles it from the Live Data menu, like the built-in panels. This registers a "Hello World" panel: | ||
| 86 | |||
| 87 | {{code language="js"}} | ||
| 88 | document.addEventListener('xwiki:livedata:instanceCreated', function(e) { | ||
| 89 | const panel = { | ||
| 90 | id: 'myExtension', | ||
| 91 | name: 'My Extension', | ||
| 92 | title: 'Hello World', | ||
| 93 | icon: 'camera', | ||
| 94 | container: document.createElement('div'), | ||
| 95 | component: 'LiveDataAdvancedPanelExtension', | ||
| 96 | order: 4000 | ||
| 97 | }; | ||
| 98 | |||
| 99 | panel.container.textContent = 'Hello World!'; | ||
| 100 | |||
| 101 | e.detail.livedata.registerPanel(panel); | ||
| 102 | }); | ||
| 103 | {{/code}} | ||
| 104 | |||
| 105 | |=Property|=What it holds | ||
| 106 | |##id##|A name unique among all the panels of the instance | ||
| 107 | |##name##|The text displayed in the Live Data menu | ||
| 108 | |##title##|The text displayed in the title of the panel | ||
| 109 | |##icon##|The icon displayed both in the menu and in the panel title | ||
| 110 | |##order##|The display order of the panel. The built-in Properties, Sort and Filter panels are at 1000, 2000 and 3000 | ||
| 111 | |##container##|A DOM node, attached to the panel body while the panel is open and detached when it is collapsed | ||
| 112 | |##component##|The Vue component rendering the panel. Use ##LiveDataAdvancedPanelExtension##, the only supported one | ||
| 113 | |||
| 114 | Every property must be set when the panel is registered. All of them except ##order## can be changed afterwards, and the change is reflected in the interface, which is how a counter in the name or the title is kept up to date. | ||
| 115 | |||
| 116 | == Registering a Displayer, a Filter or a Layout == | ||
| 117 | |||
| 118 | Vue 3 no longer allows loading a component globally with ##Vue.component##. Live Data provides a component store instead, on which displayers, filters and layouts are registered by name. | ||
| 119 | |||
| 120 | ##componentStore.register(string, string, () => Promise<VueComponent>)## | ||
| 121 | |||
| 122 | * **kind: string**: any of ##filter##, ##layout##, or ##displayer##, defines the type of loaded component | ||
| 123 | * **name: string**: the name of the registered component | ||
| 124 | * **loader: () => Promise<VueComponent>**: a function returning a ##VueComponent## wrapped in a promise. This allows for the lazy loading of components, only when effectively loaded (see ##load## below) | ||
| 125 | |||
| 126 | ##componentStore.load(string, string): Promise<VueComponent>## | ||
| 127 | |||
| 128 | * **kind: string**: the kind of component to load | ||
| 129 | * **name: string**: the name of the component to load | ||
| 130 | * **returns**: a Promise resolving to the component. When nothing is registered under that kind and name yet, the Promise stays pending and is resolved as soon as a matching component is registered | ||
| 131 | |||
| 132 | The store is imported from the ##@xwiki/platform-livedata-componentstore## module, mapped in the [[importmap>>doc:documentation.xs.dev.livedata.webjar.WebHome]] of the Live Data webjar: | ||
| 133 | |||
| 134 | {{code language="javascript"}} | ||
| 135 | import { componentStore } from "@xwiki/platform-livedata-componentstore"; | ||
| 136 | |||
| 137 | // Dynamically register a new "toggle" displayer component. | ||
| 138 | componentStore.register("displayer", "toggle", async () => { | ||
| 139 | return (await import("./components/DisplayerToggle.vue")).default; | ||
| 140 | }); | ||
| 141 | {{/code}} |