This repository has been archived by the owner on Nov 22, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 34
Modals #21
Merged
Merged
Modals #21
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
<script> | ||
import { | ||
Button, | ||
Card, | ||
TextField, | ||
} from '../src/index.js'; | ||
|
||
export let doYou; | ||
export let closeCallback; | ||
</script> | ||
|
||
<Card> | ||
<h1>Do you {doYou}?</h1> | ||
<TextField /> | ||
<Button on:click={closeCallback}>close this</Button> | ||
</Card> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export default {}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
@import '../var-defaults.scss'; | ||
@import '_attractions-theme.scss'; | ||
|
||
.modal-overlay { | ||
position: fixed; | ||
left: 0; | ||
top: 0; | ||
width: 100%; | ||
height: 100%; | ||
overflow: hidden; | ||
background-color: $modal-overlay-bg; | ||
z-index: 1000; | ||
display: none; | ||
|
||
&.open { | ||
display: flex; | ||
align-items: center; | ||
justify-content: center; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
<script> | ||
import { setContext, onDestroy } from 'svelte'; | ||
import { writable } from 'svelte/store'; | ||
import modalContextKey from './modal-context-key.js'; | ||
|
||
export const modalOverlayKey = {}; | ||
|
||
/* Stores modal registration objects. | ||
A key for such a registration is a store object | ||
that stores the open state of the modal. */ | ||
let registeredModals = new Map(); | ||
|
||
/* Meant to be bound to registration objects and used as a subscription function. */ | ||
function updateOpenState(newState) { | ||
this.open = newState; | ||
registeredModals = registeredModals; | ||
} | ||
|
||
/* Register and render a new modal from the `component` and `props`. | ||
Will return the open state store | ||
– the object that is used for identifying the registration. */ | ||
function register(component, props) { | ||
const openState = writable(false); | ||
const registration = { component, props, open: false }; | ||
registration.unsubscribe = openState.subscribe(updateOpenState.bind(registration)); | ||
|
||
registeredModals.set(openState, registration); | ||
return openState; | ||
} | ||
|
||
/* Unregister and destroy the modal that corresponds to this `openState`. */ | ||
function unregister(openState) { | ||
const registration = registeredModals.get(openState); | ||
if (registration == null) { | ||
return; | ||
} | ||
|
||
registration.unsubscribe(); | ||
registeredModals.delete(openState); | ||
} | ||
|
||
/* Pass new props to the modal that corresponds to this `openState`. | ||
Needed to support reactivity. */ | ||
function updateProps(openState, newProps) { | ||
const registration = registeredModals.get(openState); | ||
if (registration == null) { | ||
return; | ||
} | ||
|
||
registration.props = newProps; | ||
} | ||
|
||
setContext(modalContextKey, { register, update: updateProps, unregister }); | ||
|
||
onDestroy(() => { | ||
for (let registration of registeredModals.values()) { | ||
registration.unsubscribe(); | ||
} | ||
}); | ||
</script> | ||
|
||
<slot /> | ||
{#each [...registeredModals.entries()] as [openState, registration] (openState)} | ||
<div | ||
class="modal-overlay" | ||
class:open={registration.open} | ||
on:click|self={() => openState.set(false)} | ||
> | ||
<svelte:component | ||
this={registration.component} | ||
{...registration.props} | ||
closeCallback={() => openState.set(false)} | ||
/> | ||
</div> | ||
{/each} | ||
|
||
<style src="./modal-overlay.scss"></style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
<script> | ||
import { getContext, onMount, onDestroy } from 'svelte'; | ||
import modalContextKey from './modal-context-key.js'; | ||
|
||
const { register, update, unregister } = getContext(modalContextKey); | ||
|
||
export let open = false; | ||
|
||
/* The component to use in creating a modal. */ | ||
export let component; | ||
|
||
/* The props to pass to the modal component. | ||
It will also receive the `closeCallback` prop which can be used to close the modal. */ | ||
export let props; | ||
|
||
let openState = null; | ||
let unsubscribe = null; | ||
|
||
$: syncOpenState(open); | ||
$: update(openState, props); | ||
|
||
/* Written out as a separate function to ensure | ||
that the reactive statement only depends on `open`. */ | ||
function syncOpenState(openStateValue) { | ||
openState && openState.set(openStateValue); | ||
} | ||
|
||
onMount(() => { | ||
openState = register(component, props); | ||
unsubscribe = openState.subscribe((openStateValue) => { | ||
/* To prevent unneeded invalidation (if Svelte isn't smart enough not to) */ | ||
if (open !== openStateValue) { | ||
open = openStateValue; | ||
} | ||
}); | ||
}); | ||
|
||
onDestroy(() => { | ||
unregister(openState); | ||
if (unsubscribe != null) { | ||
unsubscribe(); | ||
} | ||
}); | ||
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really don't like the idea of taking a component as a prop instead of a slot. I see only 2 ways around it:
using
$$props.$$slots
, which works but isn't part of the official API (although it is used in some libraries. Check out sveltejs/svelte#2106 (comment) for an example and the rest of the discussion) but would make more sense for the user; it willconsole.warn
that the component doesn't expect a slot, though. The other way would be to wrap a slot in a meaninglessdiv
just to have something tobind:this
to, and use that as the component. This seems like the more resilient way, although it introduces a useless elementThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Me neither, but that is the only way we can have the
<Modal>
instances anywhere in the descendants of the<ModalOverlay>
, which was a first requirement. Otherwise it would create this slotted component right where it is placed.Some might argue something like a portal could remedy that, but no such thing exists in Svelte as of now, and doing it manually with
appendChild
is a sure way for disaster.Not only is
$$props.$$slots
internal API, the use of$$props
themselves is discouraged by Svelte.As a final note, given the experience of writing a shit ton of modals in the View Project page of Innopoints, I can assure you that you always want the modal window to be a separate component. Simply because it contains some sort of logic that is best isolated away.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But don't you think that this way is much more limiting than using
$$props.$$slots
(which was even sorta suggested by a Svelte member: sveltejs/rfcs#15 (comment)) or using a dummy wrapper withbind:this
? You are only passing props (in a way that looks weird, tbh), which doesn't allow listening for events or adding directivesThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it is more limiting. But again, that's okay because you want the modal created in a different place, in fact. And because most modals require a component of their own for convenience anyway. This is a limitation that we cannot get around with, for if we go with
$$props.$$slots
, we will end up creating the component further down the element tree, and our only option would be to move it.Not exactly. Remember the events action? Here's your way to listen to events. As for directives, you cannot add them to a custom component anyway (I think), and even if you'd want to, you could do that in the component itself, to one of its elements, and then pass that component here.