Reputation: 13082
In Svelte.js, How to detach a component prop bindings?
But NOT via reloading the whole component?
This demo is as simple as possible, the first cube does propagate his sizes, to all other Cube components.
The zoom
object is shared across all components, via the parent component zoom
object. Since those objects have all the same name, zoom
.
In some situation, i am looking to detach the binding on one or more Cube
component, stopping the zoom
propagation later after mount.
The simplest would be of course to fully reload the component without the binding, but unlike this demo, reloading a huge component does create extra work for the browser and break the fluidity.
Parent component:
<script>
import Cube1 from "./Cube1.svelte"
import Cube2 from "./Cube2.svelte"
let zoom = { width: 100, height: 100 }
</script>
<div>
<Cube1 bind:zoom />
<Cube2 bind:zoom />
</div>
Cube1.svelte
<script>
export let zoom = { width: 100, height: 100 };
setInterval(() => {
zoom.width = 50 + Math.random() * 100;
zoom.height = 50 + Math.random() * 100;
}
,1000)
</script>
<div class="cube"
style="width:{zoom.width}px;height:{zoom.height}px">
</div>
<style>.cube {background: grey}</style>
Cube2.svelte
<script>
export let zoom = { width: 120, height: 120 };
</script>
<div class="cube" style="width:{zoom.width}px;height:{zoom.height}px"></div>
<style>.cube {background: chartreuse}</style>
REPL: https://svelte.dev/repl/13599f372e704b059aba5fbcc3758e84?version=3.46.5
Upvotes: 1
Views: 803
Reputation: 25001
You can't change a binding after component creation, so you'll have to imagine a solution with what is possible. As a side note, this seems pretty dangerous to share a two-way binding across multiple components...
For example, if you only have one source of change, like in your example, you can bind to the source of change, and use normal props in the consumer components:
<script>
...
let depZoom = null
const toggle = () => {
if (depZoom) {
depZoom = null
} else {
depZoom = {...zoom}
}
}
</script>
</script>
<Cube1 bind:zoom />
<Cube2 zoom={depZoom || zoom} />
<button on:click={toggle}>Toggle</button>
Or, if the change can really come from any child component, you can pass down stores instead of using a binding. Contrary to bindings, you'll be able to change the store you pass as much as you want.
<script>
...
const zoom = writable({ width: 100, height: 100 })
let fixZoom = null
const toggle = () => {
if (fixZoom) {
fixZoom = null
} else {
fixZoom = readable({...zoom})
// allow assignment to the store, but ignore them
fixZoom.set = () => {}
}
}
</script>
<Cube1 zoom={fixZoom || zoom} />
<Cube2 zoom={fixZoom || zoom} />
Of course, you'll have to rewrite your Cube components internal to expect a store too...
Cube1.svelte
<script>
export let zoom
setInterval(() => {
$zoom.width = 50 + Math.random() * 100;
$zoom.height = 50 + Math.random() * 100;
}
,1000)
</script>
...
See this REPL.
Here's an example that preserves "disconnected" state with stores (REPL).
I'm using clicks instead of setInterval
as example source of change because crossed setInterval
would just be chaotic.
<script>
import { writable, readable } from 'svelte/store'
import MadCube from "./MadCube.svelte"
const sharedZoom = writable({ width: 100, height: 100 })
const cubes = [
{ color: "red", connected: true },
{ color: "green", connected: true },
{ color: "blue", connected: true },
]
</script>
{#each cubes as cube (cube)}
<div>
<MadCube color={cube.color} zoom={cube.connected ? sharedZoom : null} />
<label>
<input type="checkbox" bind:checked={cube.connected} />
Connected
</label>
</div>
{/each}
<style>
div {
display: flex;
flex-direction: column;
align-items: center;
margin: 1em;
}
div > * {
margin: .5em;
}
</style>
MadCube.svelte
<script>
import { writable } from 'svelte/store'
export let color = 'grey'
export let zoom = null
const localZoom = writable({ width: 100, height: 100 })
// decide what source of data to use for read/write:
// external if present, else local
$: currentZoom = zoom || localZoom
// when connected to external source, sync it to local
// store on any change
$: if ($zoom) {
$localZoom = $zoom
}
const change = () => {
$currentZoom.width = 50 + Math.random() * 100;
$currentZoom.height = 50 + Math.random() * 100;
}
</script>
<div
class="cube"
style:width="{$currentZoom.width}px"
style:height="{$currentZoom.height}px"
style:background={color}
on:click={change}
>
Click me to change
</div>
<style>
div {
padding: 1em;
color: white;
display: flex;
align-items: center;
text-align: center;
}
</style>
Upvotes: 2
Reputation: 7699
Since the first cube recalculates the size and passes it out to the parent, the bind:
is needed. The second cube only 'consumes' the value without changing it, so it can be just {zoom}
without the binding. Here's an even simplified example (setting the invertal in onMount and clearing it on destroy might be advisable) I think the question is
How 'detach' a component so that a prop is no longer passed, while the value inside the component is preserved (or reset to a default value?)
(If you might also want to disconnet the first cube or the second needs the binding because it modifies the value as well, I would adjust the example)
While hoping that there's a "real Svelte solution" to this, this is a workaround how I would solve your exact given example REPL
Compared to rixo's answer this doesn't implement toggling the connection, but the connection could be detached individually for every component
<script>
...
let cube2
</script>
<button on:click={() => cube2.detach()}>button</button>
<div>
<Cube1 bind:zoom />
<Cube2 {zoom} bind:this={cube2}/>
</div>
<script>
export let zoom = { width: 120, height: 120 };
let width, height, detached
$: if(!detached) {
width = zoom.width + 'px'
height = zoom.height + 'px'
}
export const detach = () => detached = true
</script>
<div class="cube" style:width style:height></div>
<style>
.cube {
background: chartreuse
}
</style>
The usage of stores in rixo's example gave me the idea of using a store and manually handle the subscription inside the component so it can be unsubscribed to detach. Here's a solution with zoom being a writable store REPL
<script>
import {writable} from 'svelte/store'
import Cube1 from "./Cube1.svelte"
import Cube2 from "./Cube2.svelte"
const zoom = writable({ width: 100, height: 100 })
let cube2
</script>
<button on:click={() => cube2.detach()}>
detach Cube2
</button>
<div>
<Cube1 {zoom} />
<Cube2 {zoom} bind:this={cube2}/>
</div>
<script>
export let zoom;
setInterval(() => {
$zoom.width = 50 + Math.random() * 100;
$zoom.height = 50 + Math.random() * 100;
}
,1000)
</script>
<div class="cube"
style="width:{$zoom.width}px;height:{$zoom.height}px">
</div>
<style>
.cube {
background: grey
}
</style>
<script>
export let zoom
let width, height
const cancelSubscription = zoom.subscribe(zoom => {
width = zoom.width + 'px'
height = zoom.height + 'px'
})
export const detach = () => cancelSubscription()
</script>
<div class="cube" style:width style:height></div>
<style>
.cube {
background: chartreuse
}
</style>
And yet another way REPL making use of <svelte:options accessors={true} />
<script>
import Cube1 from "./Cube1.svelte"
import Cube2 from "./Cube2.svelte"
let zoom = { width: 100, height: 100 }
const attached = {}
$: Object.values(attached).forEach(component => {
if(component) component.zoom = zoom
})
</script>
<button on:click={() => {delete attached.cube2}}>detach Cube2</button>
<div>
<Cube1 bind:zoom />
<Cube2 bind:this={attached.cube2}/>
</div>
<svelte:options accessors={true} />
<script>
export let zoom = { width: 120, height: 120 };
$: width = zoom.width + 'px'
$: height = zoom.height + 'px'
</script>
<div class="cube" style:width style:height></div>
<style>
.cube {
background: chartreuse
}
</style>
Upvotes: 1