phlex-reactive 0.2.6 → 0.2.8

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6db6575334b8d59a054812e6815dc9b1968274a7917c66ec48c353dd95ba477d
4
- data.tar.gz: 294d2d892c5e8a4b55466e0102b89fe945f7b4f23e1a882b06566c6898520f03
3
+ metadata.gz: 9b321b7411a892ec63333f9d85d988f7cf63609601386a03593efd736e88fb42
4
+ data.tar.gz: 00d40c394774eb54af634d328251f646a3fae1ac9bc0413ccbc89dd863608001
5
5
  SHA512:
6
- metadata.gz: 362f900e12b5d4d0e3240d230b64b7ea1366e9109c1957c266d7695a7f3985b762ce29843c644c12dba6e91ffead0bf6ff7f7e73dc32eac046de79c5c5e3c8cc
7
- data.tar.gz: 3fc968f66ea7892eba20e050e5be95ad75c878a5518052eaef4006a86b963ac3a3fd1e155d1d4b45f0ea05602d3db1decae1fbb3c75e3bd76cc93bc66a29a673
6
+ metadata.gz: bd7a2129ec09c6b33a9f623f201037f5bc6639f7b097c95f521a6bf7ba4999b8dc8e11f53f2b922f5e617d0dfd8e2b0185cc5676e20f040a9caf97637d2d9438
7
+ data.tar.gz: b6e611eab9a2ab3f3d3a6afff9c4780061ba07321ddb5db89970e076facc9d7a374f7809fe8bc38958040c34303a42484eefe25109495bdfb2b6f894a597ae3e
data/CHANGELOG.md CHANGED
@@ -6,6 +6,52 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ### Added
10
+
11
+ - **Nested & array param types (issue #16).** Action param schemas can now
12
+ declare arrays and nested hashes, not just scalars: wrap a type in an array
13
+ (`bank_account_ids: [:integer]`) for an array param, or wrap a hash schema in
14
+ an array (`invoice_items_attributes: [{ id: :integer, quantity: :float,
15
+ _destroy: :boolean }]`) for Rails-style nested attributes. Coercion recurses
16
+ per field, drops undeclared nested keys (no mass assignment), and accepts an
17
+ array as either a JSON array or a Rails index hash (`{ "0" => …, "1" => … }`).
18
+ A malformed (present-but-non-array) value for an array param is dropped — not
19
+ coerced to `[]` — so a bad payload can't read as an explicit "clear all" on an
20
+ `update!(declared_array:)`; a real empty array still passes through as `[]`.
21
+ A reactive form can now mirror a normal nested-attributes update in one action
22
+ instead of being forced into a per-row component architecture.
23
+ - **Debounce option on `on(...)` (issue #17).** A trigger can declare
24
+ `on(:update, event: "input", debounce: 300)` (milliseconds) to coalesce rapid
25
+ events — typically keystrokes — into a SINGLE action round trip fired after the
26
+ quiet period, instead of one POST per keystroke. A `blur` flushes a pending
27
+ dispatch so the last edit is never dropped, `preventDefault` still fires
28
+ synchronously (a debounced `submit` won't navigate), and the debounced round
29
+ trip still goes through the per-component queue so token threading holds.
30
+ Omitting `debounce:` keeps the immediate-dispatch default.
31
+
32
+ ### Fixed
33
+
34
+ - **Model-scoped form fields feed a nested param (issue #21).** A Rails
35
+ `Form(model: @invoice)` posts flat bracketed keys (`invoice[date]`,
36
+ `invoice[status]`) because the client keeps each input's `name` verbatim. The
37
+ server did exact-key matching, so a nested schema (`params: { invoice: { date:
38
+ :string } }`) looked for the literal key `"invoice"`, never found it, and
39
+ dropped the whole param. Param normalization now expands bracket notation
40
+ before coercion — `invoice[date]` nests under `invoice`, and
41
+ `items_attributes[0][qty]` becomes the Rails index-hash form the array coercer
42
+ already understands. A nested schema matches a normal Rails form with zero
43
+ field renaming, which is what makes the issue #16 nested-param types useful for
44
+ real forms. Pre-nested objects, plain scalars, and non-string values (a
45
+ checkbox boolean) pass through unchanged.
46
+ - **Nested reactive roots no longer leak fields (issue #15).** When a reactive
47
+ component is rendered inside another (both are `data-controller="reactive"`
48
+ roots), an action on the outer root previously swept *every* descendant named
49
+ input — including the nested roots' inputs — into its own params. Field
50
+ collection now stops at nested reactive roots: an action collects only the
51
+ inputs whose nearest `[data-controller~="reactive"]` ancestor is its own root.
52
+ Outer flat fields and per-row reactive editing compose cleanly, with no
53
+ name-disjointness workarounds.
54
+
9
55
  ## [0.2.6]
10
56
 
11
57
  ### Added
data/README.md CHANGED
@@ -268,10 +268,62 @@ Use in controllers: `render turbo_stream: Counter.replace(counter)`.
268
268
  | `action :name, params: { x: :integer }` | Declare a client-invokable action + its param schema. **Default-deny.** |
269
269
  | `reactive_attrs` | Spread onto the root element: marks it reactive + carries the signed token. |
270
270
  | `on(:action, event: "click", **params)` | Spread onto a trigger element. Adds `type=button` for clicks. |
271
+ | `on(:action, event: "input", debounce: 300)` | Coalesce rapid events into one round trip after a quiet period (live-as-you-type). |
271
272
 
272
273
  Param types: `:string` (default), `:integer`, `:float`, `:boolean`. Anything not
273
274
  in the schema is dropped before reaching your method.
274
275
 
276
+ **Array & nested params.** Wrap a type in an array for an array param, or a hash
277
+ schema in an array for Rails-style nested attributes — so one reactive action can
278
+ mirror a normal nested-attributes update instead of forcing a per-row component:
279
+
280
+ ```ruby
281
+ action :save, params: {
282
+ date: :string,
283
+ bank_account_ids: [:integer], # array of scalar
284
+ invoice_items_attributes: [ # array of hash
285
+ { id: :integer, quantity: :float, price: :float, _destroy: :boolean }
286
+ ]
287
+ }
288
+
289
+ def save(date:, bank_account_ids:, invoice_items_attributes:)
290
+ @invoice.update!(date:, bank_account_ids:, invoice_items_attributes:)
291
+ end
292
+ ```
293
+
294
+ Nested coercion recurses per field, drops undeclared nested keys, and accepts an
295
+ array as either a JSON array or a Rails index hash (`{ "0" => …, "1" => … }`).
296
+
297
+ **Model-scoped form fields just work.** A standard Rails `Form(model: @invoice)`
298
+ names its inputs `invoice[date]`, `invoice[status]`, … and the client posts those
299
+ names verbatim. A nested schema matches them with zero field renaming — the
300
+ endpoint expands bracket notation before coercion, so `invoice[date]` nests under
301
+ `invoice` and `invoice_items_attributes[0][qty]` becomes the index-hash form
302
+ above:
303
+
304
+ ```ruby
305
+ action :save, params: {invoice: {date: :string, status: :string}}
306
+ # client posts { "invoice[date]": "…", "invoice[status]": "…" } → save(invoice: { date:, status: })
307
+ ```
308
+
309
+ **Nested reactive components compose.** A reactive component rendered inside
310
+ another is its own root — field collection stops at nested
311
+ `data-controller="reactive"` roots, so an outer action collects only *its own*
312
+ named inputs, never a nested component's. An invoice editor's `save` sees its
313
+ flat fields; each line-item row's `quantity`/`price` belong to that row's own
314
+ action. No name-disjointness workarounds required.
315
+
316
+ **Debounced triggers (live-as-you-type).** Pass `debounce:` (milliseconds) to
317
+ coalesce rapid events — typically keystrokes on an `"input"` trigger — into a
318
+ single action round trip fired after the quiet period, instead of one POST per
319
+ keystroke. A blur flushes a pending dispatch so the last edit is never dropped.
320
+ Omit `debounce:` for the immediate-dispatch default.
321
+
322
+ ```ruby
323
+ # Recompute a total live as the user types, without hammering the endpoint.
324
+ input(**mix(on(:update, event: "input", debounce: 300), name: "quantity", value: @item.quantity))
325
+ ```
326
+
275
327
  **Combining `on(...)` / `reactive_attrs` with your own attributes.** Both return
276
328
  a hash that includes a `data:` key. Spreading them *and* passing another `data:`
277
329
  (or `class:`, `id:`) would clobber it — use Phlex's `mix` to deep-merge:
@@ -123,18 +123,63 @@ module Phlex
123
123
 
124
124
  # Coerce client params against the action's declared schema. Anything not
125
125
  # in the schema is dropped — no raw mass assignment reaches the component.
126
+ # The top-level params arrive as an ActionController::Parameters; coerce
127
+ # them against the action's hash schema (same recursion as nested hashes).
126
128
  def coerce_params(schema)
127
129
  return {} if schema.blank?
128
130
 
129
- raw = params.fetch(:params, {})
131
+ coerce_hash(params.fetch(:params, {}), schema)
132
+ end
133
+
134
+ # Sentinel: a declared key whose value can't be coerced to its type is
135
+ # DROPPED (not assigned), so the method's keyword default applies — exactly
136
+ # as if the client had omitted the key. Distinct from a coerced nil/[].
137
+ DROP = Object.new
138
+ private_constant :DROP
139
+
140
+ # Coerce a value against a declared type. A type is one of:
141
+ # * a scalar symbol (:string/:integer/:float/:boolean)
142
+ # * a Hash schema ({ id: :integer, ... }) — nested object
143
+ # * a one-element Array ([:integer] / [{ ... }]) — array of that
144
+ # Arrays accept both a real JSON array and a Rails-style index hash
145
+ # ({ "0" => ..., "1" => ... }), so a fields_for collection works either way.
146
+ def coerce(value, type)
147
+ if type.is_a?(Array)
148
+ coerce_array(value, type.first)
149
+ elsif type.is_a?(Hash)
150
+ coerce_hash(value, type)
151
+ else
152
+ coerce_scalar(value, type)
153
+ end
154
+ end
155
+
156
+ # A real array (or Rails index hash) coerces element-wise. A malformed
157
+ # present-but-non-array value returns DROP rather than [] — coercing a stray
158
+ # scalar to an empty array would let a bad payload read as an explicit
159
+ # "clear everything" on update!(declared_array:).
160
+ def coerce_array(value, element_type)
161
+ values = array_values(value)
162
+ return DROP if values.nil?
163
+
164
+ values.map { |element| coerce(element, element_type) }
165
+ end
166
+
167
+ # Keep declared keys only (drop undeclared — no mass assignment), recursing
168
+ # for nested hash/array element types. Symbolizes keys to splat as kwargs.
169
+ # A key whose value coerces to DROP is skipped (keyword default applies).
170
+ def coerce_hash(value, schema)
171
+ hash = to_param_hash(value)
130
172
  schema.each_with_object({}) do |(key, type), out|
131
- next unless raw.key?(key.to_s)
173
+ next unless hash.key?(key.to_s)
132
174
 
133
- out[key.to_sym] = coerce(raw[key.to_s], type)
175
+ coerced = coerce(hash[key.to_s], type)
176
+ next if coerced.equal?(DROP)
177
+
178
+ out[key.to_sym] = coerced
134
179
  end
135
180
  end
136
181
 
137
- def coerce(value, type)
182
+ def coerce_scalar(value, type)
138
183
  case type
139
184
  when :integer then value.to_i
140
185
  when :float then value.to_f
@@ -143,6 +188,84 @@ module Phlex
143
188
  end
144
189
  end
145
190
 
191
+ # Normalize an array param: a real array passes through; a Rails index hash
192
+ # ({ "0" => ..., "1" => ... }) becomes its values in index order. Anything
193
+ # else (a stray scalar, nil) is malformed → nil, so the caller drops the
194
+ # param rather than fabricating an empty array.
195
+ def array_values(value)
196
+ return value.to_a if value.is_a?(Array)
197
+
198
+ if value.respond_to?(:to_unsafe_h) || value.is_a?(Hash)
199
+ to_param_hash(value).sort_by { |k, _| k.to_i }.map(&:last)
200
+ end
201
+ end
202
+
203
+ # Unwrap ActionController::Parameters (or a plain Hash) to a string-keyed
204
+ # Hash so coercion can index it uniformly, then expand bracket notation so
205
+ # a model-scoped Rails form's flat keys nest (issue #21).
206
+ def to_param_hash(value)
207
+ flat =
208
+ if value.respond_to?(:to_unsafe_h)
209
+ value.to_unsafe_h.stringify_keys
210
+ elsif value.is_a?(Hash)
211
+ value.stringify_keys
212
+ else
213
+ return {}
214
+ end
215
+
216
+ expand_bracket_keys(flat)
217
+ end
218
+
219
+ # The client's #collectFields keeps a form input's name verbatim, so a
220
+ # Rails Form(model: @invoice) posts FLAT bracketed keys like
221
+ # "invoice[date]". Coercion does exact-key matching, so without this a
222
+ # nested schema (params: { invoice: { date: … } }) never finds "invoice"
223
+ # and drops everything. Expand "invoice[date]" => "2026-01-02" into
224
+ # { "invoice" => { "date" => "2026-01-02" } } — and "items[0][qty]" into
225
+ # the Rails index-hash form coerce_array already understands — deep-merging
226
+ # so sibling keys (invoice[date], invoice[status]) coalesce. Keys WITHOUT
227
+ # brackets and already-nested values pass through untouched, so a
228
+ # pre-nested object (issue #16) and plain scalars still work. Value types
229
+ # (a checkbox boolean, an explicit array) are preserved verbatim — unlike a
230
+ # round-trip through parse_nested_query, which only handles strings.
231
+ def expand_bracket_keys(flat)
232
+ flat.each_with_object({}) do |(key, value), out|
233
+ deep_assign(out, bracket_path(key), value)
234
+ end
235
+ end
236
+
237
+ # "invoice[items_attributes][0][qty]" => ["invoice", "items_attributes",
238
+ # "0", "qty"]. A key with no brackets is a single-element path.
239
+ def bracket_path(key)
240
+ return [key] unless key.include?("[")
241
+
242
+ head, rest = key.split("[", 2)
243
+ [head, *rest.scan(/[^\[\]]+/)]
244
+ end
245
+
246
+ # Walk/create nested hashes along `path`, then merge `value` at the leaf so
247
+ # a bracket key and a sibling pre-nested object coalesce regardless of which
248
+ # arrives first ({ "invoice[date]" => …, invoice: { status: … } } keeps
249
+ # both). #merge_value deep-merges hash/hash collisions and otherwise lets
250
+ # the later value win (a bracket key colliding with a flat scalar).
251
+ def deep_assign(hash, path, value)
252
+ *parents, leaf = path
253
+ node = parents.reduce(hash) do |acc, segment|
254
+ acc[segment] = {} unless acc[segment].is_a?(Hash)
255
+ acc[segment]
256
+ end
257
+ node[leaf] = merge_value(node[leaf], value)
258
+ end
259
+
260
+ # Combine an existing leaf value with a new one. Two hashes deep-merge (so
261
+ # bracket-expanded fields and a pre-nested object for the same key both
262
+ # survive); any other collision takes the new value.
263
+ def merge_value(existing, value)
264
+ return value unless existing.is_a?(Hash) && value.is_a?(Hash)
265
+
266
+ existing.merge(value.stringify_keys) { |_k, old, new| merge_value(old, new) }
267
+ end
268
+
146
269
  # Only components that opt into Reactive may be resolved. The signature
147
270
  # already gates this; defense in depth against constant injection.
148
271
  def resolve_component(name)
@@ -45,6 +45,15 @@ export default class extends Controller {
45
45
  }
46
46
 
47
47
  #tokenCache // freshest token, threaded synchronously across queued requests
48
+ #debounceTimers = new Map() // trigger element -> { timer, flush } pending dispatch
49
+
50
+ // Tear down any pending debounce timers when the controller leaves the DOM
51
+ // (Turbo morph/navigation removes the element). Otherwise a timer that hasn't
52
+ // fired yet would later call #enqueue on a disconnected controller — a round
53
+ // trip against a detached element / stale token (issue #17 follow-up).
54
+ disconnect() {
55
+ this.#clearAllDebounces()
56
+ }
48
57
 
49
58
  // Serialize requests per component. Each round trip rewrites the signed
50
59
  // token in the DOM (state lives in the token, not the client). If events
@@ -53,7 +62,7 @@ export default class extends Controller {
53
62
  // a per-controller promise makes each dispatch wait for the previous one, so
54
63
  // it always uses the freshest token.
55
64
  dispatch(event) {
56
- const { action, params } = event.params
65
+ const { action, params, debounce } = event.params
57
66
  if (!action) return
58
67
 
59
68
  // Stop native behavior (button submit / FORM NAVIGATION) HERE, synchronously
@@ -61,15 +70,57 @@ export default class extends Controller {
61
70
  // still being handled — deferring it into the request-queue microtask (below)
62
71
  // is too late: a `submit` trigger would natively POST the form and navigate
63
72
  // before the reactive round trip runs (issue #11). For a `click` trigger
64
- // there's no default to miss, so this was previously invisible.
73
+ // there's no default to miss, so this was previously invisible. This holds
74
+ // for debounced triggers too — the round trip is deferred, but the native
75
+ // default must still be prevented now.
65
76
  event.preventDefault()
66
77
 
78
+ // Debounced trigger (e.g. on(:update, event: "input", debounce: 300)):
79
+ // coalesce rapid events into ONE round trip after a quiet period, instead of
80
+ // one POST per keystroke (issue #17). A blur flushes a pending dispatch.
81
+ const ms = Number(debounce) || 0
82
+ if (ms > 0) return this.#debounceDispatch(event.target, ms, action, params)
83
+
67
84
  // Capture action/params now; the queued work runs in a later microtask, by
68
85
  // which point the event object may have been reset by the browser.
86
+ return this.#enqueue(action, params)
87
+ }
88
+
89
+ #enqueue(action, params) {
69
90
  this.queue = (this.queue ?? Promise.resolve()).then(() => this.#perform(action, params))
70
91
  return this.queue
71
92
  }
72
93
 
94
+ // Reset a per-element timer; only enqueue the round trip after `ms` of quiet.
95
+ // Also flush immediately on blur so leaving the field never drops the last
96
+ // edit (a long debounce shouldn't swallow a value the user tabbed away from).
97
+ #debounceDispatch(target, ms, action, params) {
98
+ this.#clearDebounce(target)
99
+
100
+ const flush = () => {
101
+ this.#clearDebounce(target)
102
+ this.#enqueue(action, params)
103
+ }
104
+ const timer = setTimeout(flush, ms)
105
+ target?.addEventListener?.("blur", flush, { once: true })
106
+ this.#debounceTimers.set(target, { timer, flush })
107
+ }
108
+
109
+ #clearDebounce(target) {
110
+ const pending = this.#debounceTimers.get(target)
111
+ if (!pending) return
112
+ clearTimeout(pending.timer)
113
+ target?.removeEventListener?.("blur", pending.flush)
114
+ this.#debounceTimers.delete(target)
115
+ }
116
+
117
+ // Clear every pending debounce timer (used on disconnect). Reuses
118
+ // #clearDebounce so all timer/listener teardown stays in one place. Snapshot
119
+ // the keys first — #clearDebounce mutates the map as it goes.
120
+ #clearAllDebounces() {
121
+ for (const target of [...this.#debounceTimers.keys()]) this.#clearDebounce(target)
122
+ }
123
+
73
124
  async #perform(action, params) {
74
125
  // Auto-collect named field values inside this component so a button-
75
126
  // triggered action still receives sibling inputs (Livewire-style).
@@ -145,10 +196,21 @@ export default class extends Controller {
145
196
  return match?.[1]
146
197
  }
147
198
 
199
+ // True when `el` is collected by THIS reactive root and not by a nested one.
200
+ // A reactive component can be rendered inside another (both are
201
+ // data-controller="reactive" roots). querySelectorAll() descends into nested
202
+ // roots, so without this guard an outer action would sweep the inner roots'
203
+ // inputs into its own params (issue #15). An element belongs to this root iff
204
+ // its nearest [data-controller~="reactive"] ancestor is this.element.
205
+ #ownsField(el) {
206
+ return el.closest('[data-controller~="reactive"]') === this.element
207
+ }
208
+
148
209
  #collectFields() {
149
210
  const fields = {}
150
- // Standard form controls.
211
+ // Standard form controls owned by THIS root (not a nested reactive root).
151
212
  this.element.querySelectorAll("input[name], select[name], textarea[name]").forEach((field) => {
213
+ if (!this.#ownsField(field)) return
152
214
  if (field.type === "checkbox") {
153
215
  fields[field.name] = field.checked
154
216
  } else if (field.type === "radio") {
@@ -167,6 +229,7 @@ export default class extends Controller {
167
229
  this.element
168
230
  .querySelectorAll("[name]:is(lexxy-editor, trix-editor, [contenteditable=''], [contenteditable=true], [contenteditable=plaintext-only])")
169
231
  .forEach((el) => {
232
+ if (!this.#ownsField(el)) return // skip editors owned by a nested reactive root (issue #15)
170
233
  // A plain element (e.g. a <div contenteditable>) has no `name` IDL
171
234
  // property — only the attribute — so read getAttribute, not el.name.
172
235
  const name = el.getAttribute("name")
@@ -87,7 +87,21 @@ module Phlex
87
87
  # Declare a client-invokable action with an optional param schema.
88
88
  # action :increment
89
89
  # action :rename, params: { title: :string }
90
- # Param types: :string (default), :integer, :float, :boolean.
90
+ #
91
+ # Param types are coerced server-side; anything not in the schema is
92
+ # dropped before reaching your method (no mass assignment):
93
+ # * Scalars — :string (default), :integer, :float, :boolean
94
+ # * Array of scalar — wrap the type in an array: [:integer]
95
+ # * Array of hash (Rails nested attributes) — wrap a hash schema:
96
+ # action :save, params: {
97
+ # date: :string,
98
+ # bank_account_ids: [:integer],
99
+ # invoice_items_attributes: [
100
+ # { id: :integer, quantity: :float, price: :float, _destroy: :boolean }
101
+ # ]
102
+ # }
103
+ # Array params accept BOTH a JSON array and a Rails-style index hash
104
+ # ({ "0" => ..., "1" => ... }), so a fields_for collection works either way.
91
105
  def action(name, params: {})
92
106
  reactive_actions[name.to_sym] = Action.new(name: name.to_sym, params: params)
93
107
  end
@@ -168,11 +182,18 @@ module Phlex
168
182
  # Attributes for an element that triggers an action.
169
183
  # button(**on(:toggle)) { "○" }
170
184
  # form(**on(:save, event: "submit")) { ... }
185
+ # input(**on(:update, event: "input", debounce: 300)) # live-as-you-type
171
186
  #
172
187
  # Extra keyword args become explicit params merged over collected form
173
188
  # fields. For click triggers we force type="button" so a bare button
174
189
  # inside a <form> can't submit it and cause a full-page navigation.
175
- def on(action_name, event: "click", **params)
190
+ #
191
+ # `debounce:` (milliseconds) coalesces rapid events (e.g. keystrokes on an
192
+ # "input" trigger) into ONE round trip fired after the quiet period — so
193
+ # live-update-as-you-type doesn't POST per keystroke. A blur flushes a
194
+ # pending dispatch so the last edit is never dropped. Omit it for the
195
+ # immediate-dispatch default.
196
+ def on(action_name, event: "click", debounce: nil, **params)
176
197
  attrs = {
177
198
  data: {
178
199
  action: "#{event}->reactive#dispatch",
@@ -180,6 +201,7 @@ module Phlex
180
201
  reactive_params_param: params.to_json
181
202
  }
182
203
  }
204
+ attrs[:data][:reactive_debounce_param] = debounce if debounce
183
205
  attrs[:type] = "button" if event == "click"
184
206
  attrs
185
207
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Phlex
4
4
  module Reactive
5
- VERSION = "0.2.6"
5
+ VERSION = "0.2.8"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: phlex-reactive
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.6
4
+ version: 0.2.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikael Henriksson