# @soleil-se/config-svelte

A collection of Svelte components and utilities for WebApp, RESTApp and Widget configurations.

## Installation

Install locally in your app:

```sh
npm install @soleil-se/config-svelte
```

Read here for usage with [TypeScript](/packages/config-svelte/typescript).

## Components

### UI

* [Panel](/packages/config-svelte/components/panel)
* [Modal](/packages/config-svelte/components/modal)

### Sitevision

Wrappers for Sitevision components.

* [DropdownSelector](/packages/config-svelte/components/dropdownselector)
* [ListSelector](/packages/config-svelte/components/listselector)
* [NodeSelector](/packages/config-svelte/components/nodeselector)
* [NumberSpinner](/packages/config-svelte/components/numberspinner)
* [TagSelector](/packages/config-svelte/components/tagselector)
* [CustomSelector](/packages/config-svelte/components/customselector)

### Input

Regular inputs

* [Checkbox](/packages/config-svelte/components/checkbox)
* [CheckboxGroup](/packages/config-svelte/components/checkboxgroup)
* [InputField](/packages/config-svelte/components/inputfield)
* [RadioGroup](/packages/config-svelte/components/radiogroup)
* [SelectField](/packages/config-svelte/components/selectfield)

### Custom

Custom components

* [TextList](/packages/config-svelte/components/textlist)
* [ImageSelector](/packages/config-svelte/components/imageselector)
* [CustomList](/packages/config-svelte/components/customlist)
* [RepositoryNodeSelector](/packages/config-svelte/components/repositorynodeselector)
* [LinkSelector](/packages/config-svelte/components/linkselector) Deprecated

## Structure

Only standard config:

* MyApp

  * config

    * App.svelte
    * config.js

  * src/

    * …

  * …

With global config:

* MyApp

  * config

    * App.svelte
    * config.js

  * config\_global

    * App.svelte
    * config.js

  * src/

    * …

  * …

With server side code:

* MyApp

  * config

    * App.svelte
    * config.js
    * index.js

  * src/

    * …

  * …

## Examples

Using Sitevision default functions with name attributes as usual.

* Svelte 5

  **config.js**

  ```js
  import { createConfigApp } from '@soleil-se/config-svelte';
  import { mount } from 'svelte';


  import App from './App.svelte';


  createConfigApp(({ target, props }) => mount(App, { target, props }));
  ```

  **App.svelte**

  ```svelte
  <script>
    import { Panel, InputField } from '@soleil-se/config-svelte';
  </script>


  <Panel heading="Inställningar">
    <InputField name="text" label="Text" />
  </Panel>
  ```

* Svelte 4

  **config.js**

  ```js
  import { createConfigApp } from '@soleil-se/config-svelte';


  import App from './App.svelte';


  createConfigApp(({ target, props }) => new App({ target, props }));
  ```

  **App.svelte**

  ```svelte
  <script>
    import { Panel, InputField } from '@soleil-se/config-svelte';
  </script>


  <Panel heading="Inställningar">
    <InputField name="text" label="Text" />
  </Panel>
  ```

* Deprecated (Svelte 4)

  **config.js**

  ```javascript
  import { createConfigApp } from '@soleil-se/config-svelte';


  import App from './App.svelte';


  createConfigApp(App);
  ```

  **App.svelte**

  ```svelte
  <script>
    import { Panel, InputField } from '@soleil-se/config-svelte';
  </script>


  <Panel heading="Inställningar">
    <InputField name="text" label="Text" />
  </Panel>
  ```

Advanced**

> **NOT RECOMENDED**
>
> Usually the standard configuration is enough for 99% of use cases and the advanced configuration will be deprecated in future versions.

Binding values. Sometimes you need full control over the data that is saved to the server, but usually the default functions are enough. Import a utility function that saves values to the server.

* Svelte 5

  **config.js**

  ```js
  import { createConfigApp } from '@soleil-se/config-svelte';
  import { mount } from 'svelte';


  import App from './App.svelte';


  createConfigApp(({ target, props }) => mount(App, { target, props }));
  ```

* Svelte 4

  **config.js**

  ```javascript
  import { createConfigApp } from '@soleil-se/config-svelte';


  import App from './App.svelte';


  createConfigApp(({ target, props }) => new App({ target, props }));
  ```

* Deprecated (Svelte 4)

  **config.js**

  ```javascript
  import { createConfigApp } from '@soleil-se/config-svelte';


  import App from './App.svelte';


  createConfigApp(App);
  ```

**App.svelte**

```svelte
<script>
  import { Panel, InputField } from '@soleil-se/config-svelte';
  import { onSave } from '@soleil-se/config-svelte/utils';


  // Values are available as a prop on root component
  export let values;


  // Set default values if needed
  values = {
    text: '',
    ...values
  };


  onSave(() => values);
</script>


<Panel heading="Inställningar">
  <InputField bind:value={values.text} label="Text" />
</Panel>
```

## Saved values

* Svelte 5

  The values object is available as a prop and `CONFIG_VALUES` global variable.

  **App.svelte**

  ```svelte
  <script>
    import { Panel, InputField } from '@soleil-se/config-svelte';


  let { values } = $props();


  console.log(values, '===', window.CONFIG_VALUES);


  </script>


  <Panel heading="Inställningar">
    <InputField name="text" label="Text" />
  </Panel>
  ```

* Svelte 4

  The values object is available as a prop and `CONFIG_VALUES` global variable.

  **App.svelte**

  ```svelte
  <script>
    import { Panel, InputField } from '@soleil-se/config-svelte';


  export let values;


  console.log(values, '===', window.CONFIG_VALUES);


  </script>


  <Panel heading="Inställningar">
    <InputField name="text" label="Text" />
  </Panel>
  ```

[Using saved values with TypeScript](/packages/config-svelte/typescript/#saved-values)

## Utils

### i18n

Since 1.6.0

Simple i18n utility for translating the configuration.

**i18n.js**

```js
import { createI18n } from '@soleil-se/config-svelte/utils';


export default createI18n({
  sv: {
    label: 'Välj sida',
    hello: 'Hej {name}!',
  },
  no: {
    label: 'Velg side',
    hello: 'Hallo {name}!',
  },
  en: {
    label: 'Select page',
    hello: 'Hello {name}!',
  },
});
```

**App.svelte**

```svelte
<script>
  import { Panel, NodeSelector } from '@soleil-se/config-svelte';
  import i18n from './i18n.js';


  const hello = i18n('hello', { name: 'Foo' });
</script>


<Panel>
  <NodeSelector name="page" type="page-selector" label={i18n('label')} />
</Panel>
```

### generateId

Generates a unique ID inside the configuration context. Prefixed with `input` per default;

```js
import { generateId } from '@soleil-se/config-svelte/utils';


const id = generateId(); // input_pcp61fn1u
const idCustomPrefix = generateId('foo'); // foo_pcp61fn1u
```

### setupComponent

Triggers the `setup-component` event on the element with passed ID.\
[Read more on developer.sitevision.se](https://developer.sitevision.se/docs/webapps/webapps-2/configuration/config.js#h-AddingSitevisioncomponentsdynamically)

```js
import { setupComponent } from '@soleil-se/config-svelte/utils';


setupComponent('myId');
```

### getAppProps

Since 1.8.0

Get props passed to the app when using server data.

```js
import { getAppProps } from '@soleil-se/config-svelte/utils';


const appProps = getAppProps();
const { myProp } = getAppProps();
```

### getAppContext

Since 1.17.0

Get app context value or object that is created for app when rendering with [createAppContext](#context).

```js
import { getAppContext } from '@soleil-se/config-svelte/utils';


const appContext = getAppContext();
const { siteName } = getAppContext();
const siteName = getAppContext('siteName');
```

### fetchRestApi

Since 1.31.0

It’s not uncommon to need to fetch data from the REST-API in a configuration, for example to populate a dropdown. This utility function is a wrapper around the native `fetch` that prefixes the URL, handles data format and the response.

| Param            | Type                    | Default     | Description                                                                                                                          |
| ---------------- | ----------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| uri              | `String`                |             | URI path for the REST-API resource, e.g. `${nodeId}/properties`                                                                      |
| \[options]       | `Object`                | `{}`        | Options object, two custom options, rest is standard [fetch options](https://developer.mozilla.org/en-US/docs/Web/API/fetch#options) |
| \[options.data]  | `Object`                | `{}`        | Data to be passed as JSON query parameter                                                                                            |
| \[options.model] | `'offline' \| 'online'` | `'offline'` | Selects which REST-API model path to use                                                                                             |

**Returns**: `Promise<unknown>` - Promise containing parsed JSON data.\
**Throws** `FetchError` - Error object with `status` property, plus any additional JSON properties returned by the response.

```js
import { fetchRestApi } from '@soleil-se/config-svelte/utils';


// GET – fetch node properties
const data = await fetchRestApi(`${nodeId}/properties`);


// GET – fetch node specific properties
const data = await fetchRestApi(`${nodeId}/properties`, {
  data: { properties: ['displayName', 'URL'] },
});


// GET – fetch online (published) data
const data = await fetchRestApi(`${nodeId}/properties`, { model: 'online' });
```

Use inside a Svelte component with error handling:

* Svelte 5

  **App.svelte**

  ```svelte
  <script>
    import { Panel, NodeSelector, SelectField } from '@soleil-se/config-svelte';
    import { fetchRestApi } from '@soleil-se/config-svelte/utils';


  let node = $state(undefined);
  let options = $state([]);
  let fetchError = $state(null);


  async function onNodeChange() {
  fetchError = null;
  if (!node) return;
  try {
  const response = await fetchRestApi(`${node}/nodes`);
  options = (response ?? []).map(({ id, name }) => ({ value: id, label: name }));
  } catch (error) {
  console.error('Error fetching data:', error);
  fetchError = error;
  }
  }


  </script>


  <Panel heading="Inställningar">
    {#if fetchError}
      <p class="alert alert-danger" role="alert">Kunde inte ladda data ({fetchError.status}).</p>
    {/if}
    <NodeSelector name="node" label="Välj nod" on:change={onNodeChange} bind:value={node} />
    <SelectField name="child" label="Välj barn" {options} />
  </Panel>
  ```

* Svelte 4

  **App.svelte**

  ```svelte
  <script>
    import { Panel, NodeSelector, SelectField } from '@soleil-se/config-svelte';
    import { fetchRestApi } from '@soleil-se/config-svelte/utils';


  let node = undefined;
  let options = [];
  let fetchError = null;


  async function onNodeChange() {
  fetchError = null;
  if (!node) return;
  try {
  const response = await fetchRestApi(`${node}/nodes`);
  options = (response ?? []).map(({ id, name }) => ({ value: id, label: name }));
  } catch (error) {
  console.error('Error fetching data:', error);
  fetchError = error;
  }
  }


  </script>


  <Panel heading="Inställningar">
    {#if fetchError}
      <p class="alert alert-danger" role="alert">Kunde inte ladda data ({fetchError.status}).</p>
    {/if}
    <NodeSelector name="node" label="Välj nod" on:change={onNodeChange} bind:value={node} />
    <SelectField name="child" label="Välj barn" {options} />
  </Panel>
  ```

### onSave

Runs the supplied callback when config is saved.\
The callback should return the values to be saved as an Object.

> **Caution**
>
> Only **one** `onSave` call should be present in the app.

```js
import { onSave } from '@soleil-se/config-svelte/utils';


onSave(() => ({ foo: 'bar' }));
```

### pluckPrefix

Plucks object properties with keys prefixed with ‘link’ and removes the prefix. `{ linkType, linkValue, linkNewWindow }` will be converted to `{ type, value, newWindow }`. If no object is passed the saved config data from `CONFIG_VALUES` will be used.

Sitevision needs a flat object structure to manage ID

when exporting and importing a site.

```js
import { pluckPrefix } from '@soleil-se/config-svelte/utils';


const link1 = pluckPrefix('link'); // Will pluck from window.CONFIG_VALUES
const link2 = pluckPrefix('link', {
  linkType: 'internal',
  linkValue: '4.xxxx',
  linkNewWindow: false,
  willNotBePlucked: 'Wrong prefix',
});
```

### addPrefix

Prefixes keys in an object `{ type, value, newWindow }` will be converted to `{ linkType, linkValue, linkNewWindow }`.

Sitevision needs a flat object structure to manage ID

when exporting and importing a site.

```js
import { addPrefix } from '@soleil-se/config-svelte/utils';


const link = { type: 'internal', value: '4.xxxx', newWindow: false };
const prefixed = addPrefix('link', link);
```

## Server

### Props

If you need to pass data from a server context to the app you can use the utility function `createAppProps`. Data will be avalilable as props in the root component and with [getAppProps](#getappprops).

* 1.30.0

  **index.js**

  ```js
  import router from '@sitevision/api/common/router';
  import { createAppProps } from '@soleil-se/config-svelte/server';


  router.get('/', (req, res) => {
  const props = {
  foo: 'bar',
  fizz: 'buzz',
  };
  createAppProps(props, res);
  });
  ```

* 1.8.0 (Deprecated)

  **index.js**

  ```js
  import router from '@sitevision/api/common/router';
  import { createAppProps } from '@soleil-se/config-svelte/server';


  router.get('/', (req, res) => {
    const props = {
      foo: 'bar',
      fizz: 'buzz',
    };
    res.send(createAppProps(props));
  });
  ```

* 1.0.0 (Deprecated)

  **index.js**

  ```js
  import router from '@sitevision/api/common/router';
  import { createAppData } from '@soleil-se/config-svelte/server';


  router.get('/', (req, res) => {
  const props = {
  foo: 'bar',
  fizz: 'buzz',
  };
  res.send(createAppData(props));
  });
  ```

- Svelte 5

  **App.svelte**

  ```svelte
  <script>
    let { foo, fizz } = $props();
  </script>


  <p>{foo}</p>
  <p>{fizz}</p>
  ```

- Svelte 4

  **App.svelte**

  ```svelte
  <script>
    export let foo;
    export let fizz;
  </script>


  <p>{foo}</p>
  <p>{fizz}</p>
  ```

[Using props with TypeScript](/packages/config-svelte/typescript/#props)

> **Caution**
>
> If you’re using an older build system, for example `@soleil/sv-gulp-build`, you need to manually add an `index.html` file with a `div` where the app can be mounted for the props to work.
>
> **index.html**
>
> ```html
> <div id="app_root"></div>
> ```

### Context

Since 1.17.0

Contains information about app context.

* `pageId` - ID of current page.
* `portletId` - ID of current portlet.
* `siteId` - ID of site.
* `siteName` - Name of site.
* [@sitevision/api/server/appInfo](https://developer.sitevision.se/docs/webapps/webapps-2/sdk/appinfo)

Is needed for some components to function.

* [RepositoryNodeSelector](/packages/config-svelte/components/repositorynodeselector)

**index.js**

```js
import router from '@sitevision/api/common/router';
import { createAppContext } from '@soleil-se/config-svelte/server';


router.get('/', (req, res) => {
  createAppContext(res);
});
```

**App.svelte**

```svelte
<script>
  import { getAppContext } from '@soleil-se/config-svelte/utils';


  const { siteName } = getAppContext()
</script>


<h1>{siteName}</h1>
```

## Global configuration

For global configuration to work you need an `index.html` file in the global folder with a `div` where the app can be mounted. This file is added automatically when using `@soleil-se/app-build`.

> **Caution**
>
> If you’re using an older build system, for example `@soleil/sv-gulp-build`, you need to manually add an `index.html` file with a `div` where the app can be mounted for the props to work.
>
> **index.html**
>
> ```html
> <div id="app_root"></div>
> ```

## Validation

All components are compatible with [@soleil-se/config-validate](/packages/config-validate/).