@csedl/hotwire-svelte-helpers 2.1.1 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,12 +12,27 @@ Dropdown with stimulus, based on floating-UI.
12
12
  ## Setup
13
13
 
14
14
  ```javascript
15
- import {HotwireSvelteHelpers} from "@csedl/hotwire-svelte-helpers/init"
15
+ import { HotwireSvelteHelpers } from "@csedl/hotwire-svelte-helpers/init"
16
16
  HotwireSvelteHelpers.debug = true
17
17
  HotwireSvelteHelpers.initializeOverlays()
18
18
  ```
19
19
 
20
- You MUST add the file `<vite-sourcee-code-dir>/config/hotwire-svelte-helpers.js`, with content like:
20
+ This package has a Server-Side Rendering (SSR) safe configuration, which means:
21
+
22
+ You must add the file `<vite-sourcee-code-dir>/config/hotwire-svelte-helpers.js`.
23
+ The package inside does a fixed `import customConfigs from '/config/hotwire-svelte-helpers.js'`.
24
+ By this, it must find that file.
25
+
26
+ Minimal content of this file:
27
+
28
+ ```javascript
29
+ export default {
30
+ }
31
+ ```
32
+
33
+ ## Configuration
34
+
35
+ The package-internal default configs, which can be overwritten hereby, are:
21
36
 
22
37
  ```javascript
23
38
  export default {
@@ -31,22 +46,48 @@ export default {
31
46
  persistTooltipOnClick: false,
32
47
  // clicking on the tooltip-label causes the tooltip-panel to persist open
33
48
  // this may mostly be helpful for development, so you may make it environment-dependent
34
- closeButtonSvg: '',
35
- })
49
+ closeButtonSvg: null,
50
+ // raw import of svg-icon,
51
+
52
+ formFieldWrapperClass: 'form-field-wrapper',
53
+ formFieldLabelWrapperClass: 'form-field-label-wrapper',
54
+ formFieldInputWrapperClass: 'form-field-input-wrapper',
55
+ }
36
56
  ```
37
57
 
38
- But, at least with content:
58
+ The code you can find in this package under `src/lib/configs.js`
59
+
60
+ You can add as many keys as you like and fetch them from anywhere by:
39
61
 
40
62
  ```javascript
41
- export default {
42
- }
63
+ import { hotwireSvelteConfig } from "@csedl/hotwire-svelte-helpers";
64
+ hotwireSvelteConfig('closeButtonSelector')
65
+ ```
66
+
67
+ ## cleanMount()
68
+
69
+ Interaction with (Svelte) components can lead to orphaned instances.
70
+ This can happen when the [unmount](https://svelte.dev/docs/svelte/imperative-component-api#unmount) event does not happen, for example when the underlaying DOM element disappears which can happen in a Hotwire Environemnt.
71
+
72
+ For this here is a double security. CleanMount() adds the instance to a global store and then executes mount().
73
+
74
+ ```javascript
75
+ import {cleanMount} from "@csedl/hotwire-svelte-helpers";
76
+ cleanMount(AnySvelteComponent, { target: cleanMountDemoTag, ...});
43
77
  ```
44
78
 
45
- The reason for this is that we need to have ssr safe configs.
79
+ Next, you must add something like
80
+
81
+ ```javascript
82
+ import { unmountAllDetached } from '@csedl/hotwire-svelte-helpers'
83
+ document.addEventListener('turbo:render', () => {
84
+ unmountAllDetached()
85
+ })
86
+ ```
46
87
 
47
- ## Example
88
+ to your application.js. This will check for detached instances and unmount them.
48
89
 
49
- There is a [online example app](https://hotwire-svelte-helpers.sedlmair.ch/)
90
+ ## Dropdown Example
50
91
 
51
92
  ```html
52
93
  <div data-controller="csedl-dropdown" data-panel-id="dropdown-panel-3h5k7l4">
package/index.js CHANGED
@@ -1,8 +1,10 @@
1
1
 
2
2
  import { cleanMount, unmountAllDetached } from './src/svelte/cleanMount.js';
3
- import { initializeDropdown, openDropdownPanel, getOrSetPanelId, debugLog, hotwireSvelteConfig } from './src/lib/utils.js'
3
+ import { initializeDropdown, openDropdownPanel, getOrSetPanelId, debugLog, getCsrfToken } from './src/lib/utils.js'
4
+ import { hotwireSvelteConfig } from './src/lib/config.js'
4
5
 
5
6
  export {
6
7
  cleanMount, unmountAllDetached, hotwireSvelteConfig,
7
- initializeDropdown, openDropdownPanel, getOrSetPanelId, debugLog
8
+ initializeDropdown, openDropdownPanel, getOrSetPanelId,
9
+ debugLog, getCsrfToken
8
10
  }
package/package.json CHANGED
@@ -1,21 +1,43 @@
1
1
  {
2
2
  "name": "@csedl/hotwire-svelte-helpers",
3
- "version": "2.1.1",
3
+ "version": "2.2.1",
4
4
  "description": "Hotwire + Svelte helpers for Rails: Stimulus floating dropdowns/toolips + Svelte global panels/modals + RTurbo-friendly utilities. Build together with the rubygem svelte-on-rails and its npm-package.",
5
5
  "main": "index.js",
6
6
  "scripts": {
7
7
  "test": "echo \"Error: no test specified\" && exit 1"
8
8
  },
9
9
  "exports": {
10
- ".": "./index.js",
11
- "./init": "./src/lib/initializers.js",
10
+ ".": {
11
+ "import": "./index.js",
12
+ "default": "./index.js"
13
+ },
14
+ "./init": {
15
+ "import": "./src/lib/initializers.js",
16
+ "default": "./src/lib/initializers.js"
17
+ },
18
+ "./DropdownButton.svelte": {
19
+ "svelte": "./src/svelte/templates/DropdownButton.svelte",
20
+ "default": "./src/svelte/templates/DropdownButton.svelte"
21
+ },
12
22
  "./DropdownPanel.svelte": {
13
23
  "svelte": "./src/svelte/templates/DropdownPanel.svelte",
14
24
  "default": "./src/svelte/templates/DropdownPanel.svelte"
15
25
  },
26
+ "./ModalButton.svelte": {
27
+ "svelte": "./src/svelte/templates/ModalButton.svelte",
28
+ "default": "./src/svelte/templates/ModalButton.svelte"
29
+ },
16
30
  "./Modal.svelte": {
17
31
  "svelte": "./src/svelte/templates/Modal.svelte",
18
32
  "default": "./src/svelte/templates/Modal.svelte"
33
+ },
34
+ "./FormInput.svelte": {
35
+ "svelte": "./src/svelte/templates/FormInput.svelte",
36
+ "default": "./src/svelte/templates/FormInput.svelte"
37
+ },
38
+ "./FormSubmitButton.svelte": {
39
+ "svelte": "./src/svelte/templates/FormSubmitButton.svelte",
40
+ "default": "./src/svelte/templates/FormSubmitButton.svelte"
19
41
  }
20
42
  },
21
43
  "repository": {
@@ -0,0 +1,25 @@
1
+ import customConfigs from '/config/hotwire-svelte-helpers.js'
2
+
3
+ export function hotwireSvelteConfig(key) {
4
+
5
+ const defaultConfig = {
6
+ closeButtonSelector: '.close-button',
7
+ dropdownContentSelector: '.content',
8
+ tooltipContentSelector: '.content',
9
+ addArrow: true,
10
+ persistTooltipOnClick: false,
11
+ closeButtonSvg: '',
12
+
13
+ formFieldWrapperClass: 'form-field-wrapper',
14
+ formFieldLabelWrapperClass: 'form-field-label-wrapper',
15
+ formFieldInputWrapperClass: 'form-field-input-wrapper',
16
+ }
17
+
18
+ const configs = {...defaultConfig, ...customConfigs}
19
+
20
+ if (!Object.keys(configs).includes(key)) {
21
+ throw new Error(`[@csedl/hotwire-svelte-helpers] unknown config key: ${key}, actual keys are:\n • ${Object.keys(configs).join("\n • ")}`)
22
+ }
23
+
24
+ return configs[key] || null
25
+ }
package/src/lib/utils.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import {positionPanelSelf, positionPanelByButton, positionPanel} from "./floating-ui-functions.js";
2
- import customConfigs from '@frontend/config/hotwire-svelte-helpers.js';
3
- import {validateOptions} from "./type-validators";
2
+ import {hotwireSvelteConfig} from "./config.js";
4
3
 
5
4
  // Fetch content from server based on panel's data-src attribute and update content
6
5
  export function openDropdownPanel(buttonElement, panelElement, options = {}) {
@@ -9,9 +8,6 @@ export function openDropdownPanel(buttonElement, panelElement, options = {}) {
9
8
  return
10
9
  }
11
10
 
12
- //validateOptions(options, {clickedElement: 'object'})
13
- //const clickedElement = options.clickedElement || buttonElement;
14
-
15
11
 
16
12
  // link button and panel, initialize
17
13
  panelElement.id = getOrSetPanelId(buttonElement);
@@ -267,28 +263,8 @@ export function getOrSetPanelId(buttonTag) {
267
263
  }
268
264
  }
269
265
 
270
- export function hotwireSvelteConfig(key) {
271
-
272
- const defaultConfig = {
273
- closeButtonSelector: '.close-button',
274
- dropdownContentSelector: '.content',
275
- tooltipContentSelector: '.content',
276
- addArrow: true,
277
- persistTooltipOnClick: false,
278
- closeButtonSvg: '',
279
- }
280
-
281
- const validations = {
282
- closeButtonSelector: 'string',
283
- dropdownContentSelector: 'string',
284
- tooltipContentSelector: 'string',
285
- addArrow: 'boolean',
286
- persistTooltipOnClick: 'boolean',
287
- closeButtonSvg: 'string',
288
- }
289
- validateOptions(customConfigs, validations)
290
-
291
- return customConfigs[key] || defaultConfig[key] || null
266
+ export function getCsrfToken() {
267
+ return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '';
292
268
  }
293
269
 
294
270
  function generateRandomHex(n) {
@@ -1,6 +1,7 @@
1
1
  import {Controller} from "@hotwired/stimulus"
2
2
  import {positionPanelByButton} from "../lib/floating-ui-functions.js";
3
- import {hotwireSvelteConfig, debugLog, findPanelOrThrow, openDropdownPanel} from "../lib/utils.js";
3
+ import { debugLog, findPanelOrThrow, openDropdownPanel} from "../lib/utils.js";
4
+ import {hotwireSvelteConfig} from "../lib/config.js";
4
5
 
5
6
  export default class extends Controller {
6
7
 
@@ -0,0 +1,47 @@
1
+ <script>
2
+ import DropdownPanel from "./DropdownPanel.svelte";
3
+ import { unmount } from 'svelte'
4
+ import { cleanMount } from "../cleanMount.js";
5
+
6
+ let { panelContent, panelTitle, panelClass, label, svgIcon } = $props();
7
+
8
+ let buttonElement, panelInstance;
9
+
10
+
11
+ export function closeDropdown() {
12
+ if (panelInstance) {
13
+ unmount(panelInstance);
14
+ }
15
+ buttonElement.classList.remove('has-open-panel');
16
+ };
17
+
18
+ function openEditPanel() {
19
+
20
+ if (buttonElement.classList.contains('has-open-panel')) return;
21
+
22
+ panelInstance = cleanMount(
23
+ DropdownPanel, {
24
+ target: document.getElementById('dropdown-panels-box'),
25
+ props: {
26
+ title: panelTitle,
27
+ closeFunction: closeDropdown,
28
+ buttonElement: buttonElement,
29
+ panelClass: panelClass,
30
+ content: panelContent,
31
+ }
32
+ }
33
+ );
34
+ }
35
+ </script>
36
+
37
+ <span class="dropdown-button" onclick={ openEditPanel } bind:this={ buttonElement } role="button" tabindex="0" onkeyup={()=>{}}>
38
+ {#if svgIcon}
39
+ {@html svgIcon}
40
+ {/if}
41
+ {#if label}
42
+ {label}
43
+ {/if}
44
+ {#if !svgIcon && !label}
45
+ ??
46
+ {/if}
47
+ </span>
@@ -1,5 +1,5 @@
1
1
  <script>
2
- import { openDropdownPanel, hotwireSvelteConfig } from "@csedl/hotwire-svelte-helpers";
2
+ import {openDropdownPanel, hotwireSvelteConfig} from "@csedl/hotwire-svelte-helpers";
3
3
  import {onMount} from "svelte";
4
4
 
5
5
  let {
@@ -23,9 +23,15 @@
23
23
  }
24
24
 
25
25
  function closeSvg() {
26
- const path = hotwireSvelteConfig('closeButtonSvg');
27
- const svg = (path ? path : '?')
28
- return svg;
26
+ const value = hotwireSvelteConfig('closeButtonSvg');
27
+
28
+ if (!value) return '?';
29
+
30
+ if (typeof value === 'function') {
31
+ return value();
32
+ }
33
+
34
+ return String(value);
29
35
  }
30
36
 
31
37
  </script>
@@ -34,15 +40,22 @@
34
40
 
35
41
 
36
42
  <div class="header">
37
- <span class="title">
38
- {title || 'missing title'}
39
- </span>
40
- <button class="close-btn"
41
- onclick={closeFunction}>{@html closeSvg()}</button>
43
+ <span class="title">
44
+ {title || 'missing title'}
45
+ </span>
46
+ <span class="buttons">
47
+ <button class={ hotwireSvelteConfig('closeButtonSelector').replace('.', '') || 'close-btn' }
48
+ onclick={closeFunction}>{@html closeSvg()}
49
+ </button>
50
+ </span>
42
51
  </div>
43
52
 
44
53
  <div class="content">
45
- {@render content()}
54
+ {#if content}
55
+ {@render content()}
56
+ {:else}
57
+ <div style="color: red;">Missing required `content` snippet for dropdown panel.</div>
58
+ {/if}
46
59
  </div>
47
60
 
48
61
  </div>
@@ -0,0 +1,91 @@
1
+ <script>
2
+
3
+ import { hotwireSvelteConfig } from "../../lib/config.js";
4
+
5
+ let {
6
+ context,
7
+ columnName,
8
+ errors,
9
+ onChangeFunction,
10
+ type,
11
+ autocomplete = 'on'
12
+ } = $props();
13
+
14
+ let model = $derived(context._schema.self.modelKey)
15
+ let columnSchema = $derived(context._schema[columnName])
16
+ let value = $derived(context[columnName])
17
+ let inputType = $derived.by(() => {
18
+ const t = {
19
+ string: 'text',
20
+ text: 'textarea',
21
+ integer: 'number',
22
+ decimal: 'number',
23
+ boolean: 'checkbox',
24
+ }[columnSchema.dbType]
25
+ return (type ? type : t);
26
+ })
27
+
28
+ let fieldErrors = $derived.by(() => {
29
+ const modelErrors = errors?.[model] ?? {};
30
+ const fieldErrors = modelErrors?.[columnSchema.key];
31
+ if (!fieldErrors) return [];
32
+
33
+ console.log('fieldErrors', fieldErrors)
34
+
35
+ return fieldErrors;
36
+ })
37
+
38
+ let fieldErrorsClass = $derived.by(() => {
39
+ return fieldErrors.length > 0 ? 'error' : '';
40
+ })
41
+
42
+ function label() {
43
+ let req = (columnSchema.required) ? '* ' : ''
44
+ return req + columnSchema.label
45
+ }
46
+
47
+ </script>
48
+
49
+
50
+ <div class="{hotwireSvelteConfig('formFieldWrapperClass')} {model}_{columnName} {inputType} {fieldErrorsClass}">
51
+ <div class={hotwireSvelteConfig('formFieldLabelWrapperClass')}>
52
+ <label for="{model}_{columnName}">{label()}</label>
53
+ </div>
54
+ <div class={hotwireSvelteConfig('formFieldInputWrapperClass')}>
55
+
56
+ {#if columnSchema.enum}
57
+ <select
58
+ id="{model}_{columnName}"
59
+ name="{model}[{columnSchema.key}]"
60
+ value="{value}"
61
+ onchange="{onChangeFunction}"
62
+ class="input-field">
63
+ {#each Object.entries(columnSchema.enum) as [key, label]}
64
+ <option value="{key}">{label}</option>
65
+ {/each}
66
+ </select>
67
+ {:else if inputType === 'textarea'}
68
+ <textarea
69
+ id="{model}_{columnName}"
70
+ name="{model}[{columnSchema.key}]"
71
+ value="{value}"
72
+ class="input-field"/>
73
+ {:else}
74
+ <input
75
+ id="{model}_{columnName}"
76
+ type="{inputType}"
77
+ name="{model}[{columnSchema.key}]"
78
+ value="{value}"
79
+ class="input-field"
80
+ autocomplete={autocomplete}
81
+ >
82
+ {/if}
83
+
84
+
85
+ </div>
86
+
87
+ {#each fieldErrors as error}
88
+ <small class="error">{error}</small>
89
+ {/each}
90
+
91
+ </div>
@@ -0,0 +1,6 @@
1
+ <script>
2
+ let { context } = $props();
3
+ let label = $derived(context.id ? context._translations.helpers.submit.update : context._translations.helpers.submit.create);
4
+ </script>
5
+
6
+ <button class="button" type="submit">{label}</button>
@@ -5,12 +5,8 @@
5
5
  let {
6
6
  title,
7
7
  panelClass,
8
- SvelteComponent,
9
- svelteComponentProps,
10
8
  closeFunction,
11
- closeButtonSvg,
12
9
  content,
13
- text,
14
10
  actionButtonLabel,
15
11
  actionButtonFunction,
16
12
  } = $props()
@@ -45,25 +41,23 @@
45
41
  <span class="title">
46
42
  {title || 'missing title'}
47
43
  </span>
48
- <button class="close-btn"
49
- onclick={closeFunction}>{@html closeSvg()}</button>
44
+ <span class="buttons">
45
+ <button class={ hotwireSvelteConfig('closeButtonSelector').replace('.', '') || 'close-btn' }
46
+ onclick={closeFunction}>{@html closeSvg()}
47
+ </button>
48
+ </span>
50
49
  </div>
51
50
 
52
51
  <div class="content">
53
52
 
54
- {#if content}
55
- {@render content()}
56
- {/if}
53
+ <div class="content">
54
+ {#if content}
55
+ {@render content()}
56
+ {:else}
57
+ <div style="color: red;">Missing required `content` snippet for dropdown panel.</div>
58
+ {/if}
59
+ </div>
57
60
 
58
- {#if SvelteComponent}
59
- <SvelteComponent {...svelteComponentProps}/>
60
- {/if}
61
-
62
- {#if text}
63
- {#each text as paragraph}
64
- <p>{paragraph}</p>
65
- {/each}
66
- {/if}
67
61
  </div>
68
62
  <div class="footer">
69
63
  {#if actionButtonLabel}
@@ -0,0 +1,38 @@
1
+ <script>
2
+ import Modal from './Modal.svelte';
3
+ import {unmount} from 'svelte'
4
+ import {cleanMount} from "../cleanMount.js";
5
+
6
+ let {panelTitle, panelContent, label, svgIcon} = $props()
7
+ let modalInstance;
8
+
9
+ export function closeModal() {
10
+ if (modalInstance) {
11
+ unmount(modalInstance);
12
+ }
13
+ };
14
+
15
+ function openModal() {
16
+
17
+ modalInstance = cleanMount(Modal, {
18
+ target: document.getElementById('dropdown-panels-box'),
19
+ props: {
20
+ title: panelTitle,
21
+ content: panelContent,
22
+ closeFunction: closeModal
23
+ }
24
+ });
25
+ }
26
+ </script>
27
+
28
+ <span role="button" tabindex="0" onkeyup={()=>{}} onclick={openModal} class="modal-button">
29
+ {#if svgIcon}
30
+ {@html svgIcon}
31
+ {/if}
32
+ {#if label}
33
+ {label}
34
+ {/if}
35
+ {#if !svgIcon && !label}
36
+ ??
37
+ {/if}
38
+ </span>