Skip to content
This repository has been archived by the owner on Nov 22, 2024. It is now read-only.

Modals #21

Merged
merged 3 commits into from
May 18, 2020
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 55 additions & 41 deletions demo/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
Switch,
RadioChipGroup,
CheckboxChipGroup,
ModalOverlay,
Modal,
} from '../src/index.js';
import ModalCard from './modal-card.svelte';
import { ChevronDownIcon } from 'svelte-feather-icons';

let items = [
Expand All @@ -29,50 +32,61 @@
];
let item = items[1].value;
let colorItem = colorItems[1].value;

let open1 = false;
let open2 = false;
</script>

<Card>
<TextField label="Write something" outline />
<Button filled>
Button!!
</Button>
<RadioGroup items={colorItems} color bind:value={colorItem} name="colors" />
<RadioGroup items={items} bind:value={item} name="numbers" />
<CheckboxGroup items={items} max={1} name="numbers-check" />
<div class="dropdown-holder">
<DropdownShell let:toggle on:change={() => console.log('yay')}>
<Button on:click={toggle}>
test me!
<ChevronDownIcon size="24" class="ml dropdown-chevron" />
</Button>
<Dropdown top>
<div class="padded">
I'm a little dropdown short and stout
</div>
</Dropdown>
</DropdownShell>
</div>
<ModalOverlay>
<Card>
<TextField label="Write something" outline />
<Button filled>
Button!!
</Button>
<RadioGroup items={colorItems} color bind:value={colorItem} name="colors" />
<RadioGroup items={items} bind:value={item} name="numbers" />
<CheckboxGroup items={items} max={1} name="numbers-check" />
<div class="dropdown-holder">
<DropdownShell let:toggle on:change={() => console.log('yay')}>
<Button on:click={toggle}>
test me!
<ChevronDownIcon size="24" class="ml dropdown-chevron" />
</Button>
<Dropdown top>
<div class="padded">
I'm a little dropdown short and stout
</div>
</Dropdown>
</DropdownShell>
</div>

<Switch>
<span class="padded">
default
</span>
</Switch>
<Switch slotLeft value={true}>
<span class="padded">
on
</span>
</Switch>
<Switch slotLeft value={true} disabled>
<span class="padded">
disabled
</span>
</Switch>
<div class="flex">
<RadioChipGroup {items} name="radio-chip-group" outline />
<CheckboxChipGroup {items} name="checkbox-chip-group" max={2} small />
</div>
</Card>
<Switch>
<span class="padded">
default
</span>
</Switch>
<Switch slotLeft value={true}>
<span class="padded">
on
</span>
</Switch>
<Switch slotLeft value={true} disabled>
<span class="padded">
disabled
</span>
</Switch>
<div class="flex">
<RadioChipGroup {items} name="radio-chip-group" outline />
<CheckboxChipGroup {items} name="checkbox-chip-group" max={2} small />
</div>
<div class="flex">
<Button on:click={() => open1 = true}>open modal 1</Button>
<Button on:click={() => open2 = true}>open modal 2</Button>
</div>
<Modal component={ModalCard} props={{ doYou: 'hear what I hear' }} bind:open={open1} />
<Modal component={ModalCard} props={{ doYou: 'care if I care' }} bind:open={open2} />
</Card>
</ModalOverlay>

<style>
.padded {
Expand Down
16 changes: 16 additions & 0 deletions demo/modal-card.svelte
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>
3 changes: 3 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,6 @@ export { default as RadioChip } from './chip/radio-chip.svelte';
export { default as RadioChipGroup } from './chip/radio-chip-group.svelte';
export { default as CheckboxChip } from './chip/checkbox-chip.svelte';
export { default as CheckboxChipGroup } from './chip/checkbox-chip-group.svelte';

export { default as ModalOverlay } from './modal/modal-overlay.svelte';
export { default as Modal } from './modal/modal.svelte';
1 change: 1 addition & 0 deletions src/modal/modal-context-key.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default {};
20 changes: 20 additions & 0 deletions src/modal/modal-overlay.scss
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;
}
}
77 changes: 77 additions & 0 deletions src/modal/modal-overlay.svelte
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>
44 changes: 44 additions & 0 deletions src/modal/modal.svelte
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;
Copy link
Collaborator

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 will console.warn that the component doesn't expect a slot, though. The other way would be to wrap a slot in a meaningless div just to have something to bind:this to, and use that as the component. This seems like the more resilient way, although it introduces a useless element

Copy link
Owner Author

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.

Copy link
Collaborator

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 with bind:this? You are only passing props (in a way that looks weird, tbh), which doesn't allow listening for events or adding directives

Copy link
Owner Author

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...

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.

You are only passing props (in a way that looks weird, tbh), which doesn't allow listening for events or adding directives

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.

/* 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>
1 change: 1 addition & 0 deletions src/var-defaults.scss
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ $textfield-item: #7a7a7a;
$off-state: #aaa;
$chip-bg: transparentize(black, .92);
$chip-fg: #656565;
$modal-overlay-bg: transparentize(black, .75);

$chip-radius: 1.5625em;
$button-radius: 1.5625em;
Expand Down