|
3 | 3 | import Fa from 'svelte-fa'; |
4 | 4 | import { faXmark, faSave } from '@fortawesome/free-solid-svg-icons'; |
5 | 5 |
|
6 | | - import { createEventDispatcher, onMount } from 'svelte'; |
| 6 | + import { createEventDispatcher, onMount, tick } from 'svelte'; |
7 | 7 |
|
8 | 8 | import suite from './externalLink'; |
9 | 9 | import { |
|
22 | 22 | export let link: externalLinkType; |
23 | 23 | $: link; |
24 | 24 |
|
| 25 | + // local editable copy to guarantee reactivity on nested property changes |
| 26 | + let localLink: externalLinkType = link ? { ...link } : ({} as externalLinkType); |
| 27 | + // keep local copy in sync when parent provides a new object |
| 28 | + $: if (link && link.id !== localLink.id) localLink = { ...link }; |
| 29 | + |
| 30 | +
|
25 | 31 | let help: boolean = true; |
26 | 32 | let loaded = false; |
27 | 33 |
|
28 | 34 | // validation |
29 | 35 | let res = suite.get(); |
30 | | - $: isValid = res.isValid(); |
| 36 | +
|
| 37 | + $: isValid = res.isValid() |
| 38 | + $: isChanged = false |
| 39 | +
|
31 | 40 |
|
32 | 41 | // data |
33 | 42 | import { helpInfoList } from './help'; |
|
42 | 51 | const dispatch = createEventDispatcher(); |
43 | 52 |
|
44 | 53 | onMount(() => { |
45 | | - console.log('🚀 ~ file: ExternalLink.svelte:28 ~ linksList:', linksList); |
46 | | - helpStore.setHelpItemList(helpInfoList); |
| 54 | + console.log('🚀 ~ file: ExternalLink.svelte:28 ~ linksList:', linksList); |
| 55 | + helpStore.setHelpItemList(helpInfoList); |
47 | 56 |
|
48 | | - // reset & reload validation |
49 | | - suite.reset(); |
| 57 | + // reset & reload validation |
| 58 | + suite.reset(); |
50 | 59 |
|
51 | | - // set types = |
| 60 | + console.log('🚀 ~ file: ExternalLink.svelte:45 ~ onMount ~ types:', types); |
52 | 61 |
|
53 | | - console.log('🚀 ~ file: ExternalLink.svelte:45 ~ onMount ~ types:', types); |
| 62 | + if (link.id == 0) { |
| 63 | + suite.reset(); |
| 64 | + } else { |
| 65 | + (async () => { |
| 66 | + await tick(); |
| 67 | + // full validation: DO NOT pass an empty string as fieldName |
| 68 | + res = suite({ name: localLink.name, uri: localLink.uri, type: localLink.type, id: localLink.id }, undefined); |
| 69 | + console.log('Validation result init:', res.isValid(), res.getErrors(), res.getState && res.getState()); |
| 70 | + })(); |
| 71 | + } |
| 72 | + |
| 73 | + //updateAutoCompleteList(); |
| 74 | + |
| 75 | + loaded = true; |
| 76 | + }); |
54 | 77 |
|
55 | | - setTimeout(async () => { |
56 | | - if (link.id > 0) { |
57 | | - res = suite(link, ''); |
58 | | - } // run validation only if start with an existing |
59 | | - }, 10); |
| 78 | + function onChangeFn(e: Event) { |
| 79 | + // wait for DOM / bindings to update, then validate |
| 80 | + const target = e.target as EventTarget & { id?: string } | null; |
| 81 | + const id = target?.id ?? ''; |
| 82 | + // ensure Svelte notices nested changes |
| 83 | + localLink = { ...localLink }; |
| 84 | + console.log('onChangeFn ', id); |
| 85 | + console.log('localLink ', localLink); |
| 86 | + const uri = localLink.uri; |
| 87 | + |
| 88 | + (async () => { |
| 89 | + await tick(); |
| 90 | + //res = suite({ name: localLink.name, uri: localLink.uri, type: localLink.type, id: localLink.id }); |
| 91 | + res = suite({ name: localLink.name, uri: localLink.uri, type: localLink.type, id: localLink.id }, id || undefined); |
| 92 | + |
| 93 | + })(); |
| 94 | + console.log('Changed ', e); |
| 95 | + console.log('Validation result:', res.isValid(), res.getErrors()); |
60 | 96 |
|
61 | | - //updateAutoCompleteList(); |
62 | | -
|
63 | | - loaded = true; |
64 | | - }); |
65 | | -
|
66 | | - function onChangeFn(e) { |
67 | | - // add some delay so the entityTemplate is updated |
68 | | - // otherwise the values are old |
69 | | - setTimeout(async () => { |
70 | | - res = suite(link, e.target.id); |
71 | | - }, 100); |
72 | 97 | } |
73 | 98 |
|
74 | 99 | function cancel() { |
|
77 | 102 | } |
78 | 103 |
|
79 | 104 | async function submit() { |
80 | | - var s = (await (link.id == 0)) ? create(link) : update(link); |
| 105 | + // use localLink for create/update |
| 106 | + var s = (await (localLink.id == 0)) ? create(localLink) : update(localLink); |
81 | 107 |
|
82 | 108 | if ((await s).status === 200) { |
83 | | - dispatch('success'); |
| 109 | + // sync back to parent prop via event |
| 110 | + dispatch('success', { link: localLink }); |
84 | 111 | } else { |
85 | 112 | dispatch('fail'); |
86 | 113 | } |
|
90 | 117 |
|
91 | 118 | //change event: if select change check also validation only on the field |
92 | 119 | // *** is the id of the input component |
93 | | - function onSelectHandler(e, id) { |
| 120 | + function onSelectHandler(e: CustomEvent<any>, id: string) { |
94 | 121 | // // set prefix = null if type = prefix |
95 | 122 | // // type id for prefix == 1 |
96 | 123 | console.log(id, e); |
97 | 124 | if (id == 'type' && e.detail.text == 'prefix') { |
98 | | - // if type and prefix |
99 | | - console.log('reset prefix'); |
100 | | - link.prefix = undefined; |
| 125 | + localLink.prefix = undefined; |
101 | 126 | } |
102 | | -
|
103 | | - res = suite(link, id); |
| 127 | + (async () => { |
| 128 | + await tick(); |
| 129 | + res = suite({ name: localLink.name, uri: localLink.uri, type: localLink.type, id: localLink.id }, id || undefined); |
| 130 | + localLink = { ...localLink }; |
| 131 | + })(); |
104 | 132 | } |
105 | 133 | </script> |
106 | 134 |
|
|
111 | 139 | <div class="w-1/2"> |
112 | 140 | <TextInput |
113 | 141 | id="name" |
114 | | - bind:value={link.name} |
115 | | - on:change |
| 142 | + label="Name" |
| 143 | + required={true} |
| 144 | + bind:value={localLink.name} |
| 145 | + on:change={() => isChanged = true} |
116 | 146 | on:input={onChangeFn} |
117 | 147 | placeholder="Name" |
118 | 148 | valid={res.isValid('name')} |
|
136 | 166 | complexTarget={true} |
137 | 167 | isMulti={false} |
138 | 168 | clearable={false} |
139 | | - bind:target={link.type} |
| 169 | + required={true} |
| 170 | + bind:target={localLink.type} |
140 | 171 | placeholder="-- Please select --" |
141 | 172 | invalid={res.hasErrors('type')} |
142 | 173 | feedback={res.getErrors('type')} |
143 | 174 | on:change={(e) => onSelectHandler(e, 'type')} |
| 175 | + on:change={() => isChanged = true} |
144 | 176 | {help} |
145 | 177 | /> |
146 | 178 | </div> |
147 | 179 |
|
148 | 180 | <div class="w-1/4"> |
149 | | - {#if link.type?.id === externalLinkTypeEnum.prefix} |
| 181 | + {#if localLink.type?.id === externalLinkTypeEnum.prefix} |
150 | 182 | <MultiSelect |
151 | 183 | id="prefixCategory" |
152 | 184 | title="Prefix Category" |
|
158 | 190 | complexTarget={true} |
159 | 191 | isMulti={false} |
160 | 192 | clearable={true} |
161 | | - bind:target={link.prefixCategory} |
| 193 | + bind:target={localLink.prefixCategory} |
162 | 194 | placeholder="-- Please select --" |
163 | | - invalid={res.hasErrors('type')} |
164 | | - feedback={res.getErrors('type')} |
165 | | - on:change={(e) => onSelectHandler(e, 'type')} |
| 195 | + invalid={res.hasErrors('prefixCategory')} |
| 196 | + feedback={res.getErrors('prefixCategory')} |
| 197 | + on:change={(e) => onSelectHandler(e, 'prefixCategory')} |
| 198 | + on:change={() => isChanged = true} |
166 | 199 | {help} |
167 | 200 | /> |
168 | 201 | {/if} |
169 | 202 | </div> |
170 | 203 | </div> |
171 | 204 | <div class="flex gap-5"> |
172 | | - {#if link.type?.id !== externalLinkTypeEnum.prefix} |
| 205 | + {#if localLink.type?.id !== externalLinkTypeEnum.prefix} |
173 | 206 | <div class="w-1/4"> |
174 | 207 | <MultiSelect |
175 | 208 | id="prefix" |
|
182 | 215 | complexTarget={true} |
183 | 216 | isMulti={false} |
184 | 217 | clearable={true} |
185 | | - bind:target={link.prefix} |
| 218 | + bind:target={localLink.prefix} |
186 | 219 | placeholder="-- Please select --" |
187 | | - invalid={res.hasErrors('type')} |
188 | | - feedback={res.getErrors('type')} |
189 | | - on:change={(e) => onSelectHandler(e, 'type')} |
| 220 | + invalid={res.hasErrors('prefix')} |
| 221 | + feedback={res.getErrors('prefix')} |
| 222 | + on:change={(e) => onSelectHandler(e, 'prefix')} |
| 223 | + on:change={() => isChanged = true} |
190 | 224 | {help} |
191 | 225 | /> |
192 | 226 | </div> |
|
195 | 229 | <TextInput |
196 | 230 | id="uri" |
197 | 231 | label="Uri" |
198 | | - bind:value={link.uri} |
199 | | - on:change |
| 232 | + bind:value={localLink.uri} |
| 233 | + on:change={() => isChanged = true} |
200 | 234 | on:input={onChangeFn} |
201 | 235 | placeholder="Uri" |
202 | 236 | valid={res.isValid('uri')} |
203 | 237 | invalid={res.hasErrors('uri')} |
204 | 238 | feedback={res.getErrors('uri')} |
| 239 | + required={true} |
205 | 240 | {help} |
206 | 241 | /> |
207 | 242 | </div> |
208 | 243 | </div> |
209 | 244 |
|
210 | | - <UrlPreview bind:link /> |
| 245 | + <UrlPreview bind:link={localLink} /> |
211 | 246 |
|
212 | 247 | <div class="py-5 text-right col-span-2"> |
213 | 248 | <!-- svelte-ignore a11y-mouse-events-have-key-events --> |
|
224 | 259 | class="btn variant-filled-primary h-9 w-16 shadow-md" |
225 | 260 | title="Save external link, {link.name}" |
226 | 261 | id="save" |
227 | | - disabled={!isValid} |
| 262 | + disabled={!(isChanged && isValid)} |
228 | 263 | > |
229 | 264 | <Fa icon={faSave} /></button |
230 | 265 | > |
|
0 commit comments