Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add-on components #1336

Closed
wants to merge 5 commits into from
Closed

Add-on components #1336

wants to merge 5 commits into from

Conversation

hgiesel
Copy link
Contributor

@hgiesel hgiesel commented Aug 20, 2021

This addresses number 3 in this post.

Adds a new directory ts/addons, which contains wrappers for add-on purposes. Sadly we cannot just have some general purpose wrappers, but we need to wrap each component individually.

{@const}

This syntax in e.g. ts/addons/WithShortcut.svelte looks utterly confusing:

{#each [getComponent(state, updateState)] as data}
    <svelte:component this={data.component} {...data.props} />
{/each}

but could be simplified once there is {@const}, which is currently an RFC:

{@const data = getComponent(state, updateState)}
<svelte:component this={data.component} {...data.props} />

Usage

When used in an add-on, instead of:

<script>
    const {
        IconButton,
        WithContext,
        WithState,
        WithShortcut,
        contextKeys,
    } = components;

    const key = "strikeThrough";
</script>

<WithContext key={contextKeys.fieldFocusedKey} let:context={fieldFocused}>
    <WithContext key={contextKeys.inCodableKey} let:context={inCodable}>
        <WithShortcut shortcut="Control+Shift+S" let:createShortcut let:shortcutLabel>
            <WithState
                {key}
                update={() => document.queryCommandState(key)}
                let:state={active}
                let:updateState
            >
                <IconButton
                    disabled={!fieldFocused || inCodable}
                    {active}
                    tooltip="Strike-through text {shortcutLabel}"
                    on:click={(event) => {
                        document.execCommand(key);
                        updateState(event);
                    }}
                    on:mount={(event) => createShortcut(event.detail.button)}
                >
                    <!-- Bootstrap strike through icon -->
                    <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-type-strikethrough" viewBox="0 0 16 16">
                        <path d="M6.333 5.686c0 .31.083.581.27.814H5.166a2.776 2.776 0 0 1-.099-.76c0-1.627 1.436-2.768 3.48-2.768 1.969 0 3.39 1.175 3.445 2.85h-1.23c-.11-1.08-.964-1.743-2.25-1.743-1.23 0-2.18.602-2.18 1.607zm2.194 7.478c-2.153 0-3.589-1.107-3.705-2.81h1.23c.144 1.06 1.129 1.703 2.544 1.703 1.34 0 2.31-.705 2.31-1.675 0-.827-.547-1.374-1.914-1.675L8.046 8.5H1v-1h14v1h-3.504c.468.437.675.994.675 1.697 0 1.826-1.436 2.967-3.644 2.967z"/>
                    </svg>
                </IconButton>
            </WithState>
        </WithShortcut>
    </WithContext>
</WithContext>

it now looks like this:

const {
    iconButton,
    slottedHtml,
    withContext,
    withShortcut,
    withState,
} = components;

const {
    fieldFocusedKey,
    inCodableKey,
} = components.contextKeys;

const key = "strikeThrough";

const icon = `<!-- Bootstrap strike through icon -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-type-strikethrough" viewBox="0 0 16 16">
    <path d="M6.333 5.686c0 .31.083.581.27.814H5.166a2.776 2.776 0 0 1-.099-.76c0-1.627 1.436-2.768 3.48-2.768 1.969 0 3.39 1.175 3.445 2.85h-1.23c-.11-1.08-.964-1.743-2.25-1.743-1.23 0-2.18.602-2.18 1.607zm2.194 7.478c-2.153 0-3.589-1.107-3.705-2.81h1.23c.144 1.06 1.129 1.703 2.544 1.703 1.34 0 2.31-.705 2.31-1.675 0-.827-.547-1.374-1.914-1.675L8.046 8.5H1v-1h14v1h-3.504c.468.437.675.994.675 1.697 0 1.826-1.436 2.967-3.644 2.967z"/>
</svg>`

export const strikeThrough = withContext({
    key: fieldFocusedKey,
    getComponent: ({ context: fieldFocused }) => withContext({
        key: inCodableKey,
        getComponent: ({ context: inCodable }) => withShortcut({
            shortcut: "Control+Shift+S",
            getComponent: ({ createShortcut, shortcutLabel }) => withState({
                key,
                update: () => document.queryCommandState(key),
                getComponent: ({ state, updateState }) => slottedHtml({
                    slotted: icon,
                    component: iconButton({
                        disabled: !fieldFocused || inCodable,
                        active: state,
                        tooltip: `Strike-through text ${shortcutLabel}`,
                        onClick: (event) => {
                            document.execCommand(key);
                            updateState(event);
                        },
                        onMount: (event) => {
                            createShortcut(event.detail.button);
                        },
                    }),
                }),
            }),
        }),
    }),
});

I've updated the whole New Format Pack add-on to use this syntax, which you can look at here. It works quite well I have to say. The reactivity of Svelte still works, and even bindings and events work (thus WithShortcut also works).

Drawbacks:

  • You should be quite comfortable with JS callbacks to work with this syntax
  • The addon-dev has to work through two abstraction layers: how Svelte abstracts the DOM, and how we abstract Svelte.
  • We need to build wrappers for each component we want to expose, which are also very formulaic

Advantages

  • We can propagate most Svelte functionality to users
  • By having explicit wrappers, we can reroute some addon components if required, or mark them explicitly as deprecated
  • These basic addon components can also be combined on the side of the add-on developer to be more DRY, for more info, see how I recreated onlyEditable here.

Open questions

  • I need to see if more complex components, like e.g. dropdowns can be expressed this way.

@dae
Copy link
Member

dae commented Aug 23, 2021

Thanks for looking into that Henrik; it looks workable. The work required to build wrappers shouldn't be too bad, and it seems like the main downside of this is that it's more complicated for add-on authors (but also means they can avoid Svelte if they want). It seems to me this might be a better approach than distributing .svelte files.

Having said that, I think I've discovered a better approach for #1327 - will follow up there.

@dae
Copy link
Member

dae commented Aug 24, 2021

Happy to drop this given #1340 has been merged in?

@hgiesel
Copy link
Contributor Author

hgiesel commented Aug 24, 2021

Yes. The main issue of this approach would have been, that it's hard to customize the HTML, if you want anything else than what we provide. I was thinking about ways around that, but luckily I don't need to anymore :)

@hgiesel hgiesel closed this Aug 24, 2021
@hgiesel hgiesel deleted the addoncomps branch October 20, 2021 13:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants