On this page ...

A Wrapper with a Button

In this part of the example, we’ll create a custom wrapper component in Freon. The component wraps a native Freon property using the

Flowbite Svelte library. Our example combines a person’s **phone number** with a **button** that can trigger actions using that value. Here, clicking the button shows the phone number in a snackbar notification—but you could just as well send it to an external system to initiate a call or validate it against another database.

Step 1: Create the Svelte Component

We’ll create PhoneButton.svelte, a wrapper for the phone number that provides a trigger button. There are three key parts: Script, HTML, and CSS.

The Script Part

Declare the two mandatory props: editor and box.
The box is a NumberWrapperBox, which can wrap a number node (see Wrapping Property Projections of Primitive type).

// CourseSchedule/phase3/src/external/PhoneButton.svelte#L8-L9

// Props
let { editor, box }: FreComponentProps<NumberWrapperBox> = $props();

Define functions to keep the component in sync when the AST or box model changes.
setFocus forwards focus to the wrapped box; refresh updates any model-driven view state.

// CourseSchedule/phase3/src/external/PhoneButton.svelte#L16-L21

async function setFocus(): Promise<void> {
    box.childBox.setFocus();
}
const refresh = (why?: string): void => {
    // do whatever needs to be done to refresh the elements that show information from the model
};

Register these with the box using reactive lifecycle hooks:

// CourseSchedule/phase3/src/external/PhoneButton.svelte#L22-L29

$effect(() => {
    box.setFocus = setFocus;
    box.refreshComponent = refresh;
});

const colorCls: string = 'text-light-base-50 dark:text-dark-base-900 ';
const buttonCls: string =
  'bg-light-base-600 					dark:bg-dark-base-200 ' +

The HTML Part

The HTML contains a wrapper <div> with the rendered phone number and an Flowbite button. You cannot mount a box directly; instead, use Freon’s RenderComponent which renders any known box. It requires both box and editor—the same props your component receives.

// CourseSchedule/phase3/src/external/PhoneButton.svelte#L33-L36

</script>

<div class="wrapper">
    Phone number: <RenderComponent box={box.childBox} editor={editor}/>

Add a toast/snackbar that appears when the button is clicked. The message includes the current phone value:

// CourseSchedule/phase3/src/external/PhoneButton.svelte#L38-L43

    <PhoneOutline class="{iconCls}" />
    </Button>
</div>

{#if showToast}
    <Toast color="green" onclick={() => showToast = false}>

The Style Part

Basic styling for the wrapper; Flowbite components themselves are themed via

Tailwind (already configured for the host app).
// CourseSchedule/phase3/src/external/PhoneButton.svelte#L45-L52

        {#snippet icon()}
            <PhoneOutline class="{iconCls}" />
        {/snippet}
    </Toast>
{/if}

<style>
    .wrapper {

The Complete Component

// CourseSchedule/phase3/src/external/PhoneButton.svelte

<script lang="ts">
    import { Toast } from "flowbite-svelte";
    import { PhoneOutline } from 'flowbite-svelte-icons';
    import { type FreComponentProps, RenderComponent } from "@freon4dsl/core-svelte";
    import { NumberWrapperBox } from "@freon4dsl/core";
    import { Button } from 'flowbite-svelte';

    // Props
    let { editor, box }: FreComponentProps<NumberWrapperBox> = $props();

    let clicked: number = 0;
    let showToast: boolean = $state(false);

    // The following three functions need to be included for the editor to function properly.
    // Please, set the focus to the first editable/selectable element in this component.
    async function setFocus(): Promise<void> {
        box.childBox.setFocus();
    }
    const refresh = (why?: string): void => {
        // do whatever needs to be done to refresh the elements that show information from the model
    };
    $effect(() => {
        box.setFocus = setFocus;
        box.refreshComponent = refresh;
    });

    const colorCls: string = 'text-light-base-50 dark:text-dark-base-900 ';
    const buttonCls: string =
      'bg-light-base-600 					dark:bg-dark-base-200 ' +
      'hover:bg-light-base-900 		dark:hover:bg-dark-base-50 ' +
      'border-light-base-100 			dark:border-dark-base-800 ';
    const iconCls: string = 'ms-0 inline h-6 w-6';
</script>

<div class="wrapper">
    Phone number: <RenderComponent box={box.childBox} editor={editor}/>
    <Button tabindex={-1} id="about-button" class="{buttonCls} {colorCls} " name="ToastOpen" onclick={() => {clicked++; showToast = true}}>
    <PhoneOutline class="{iconCls}" />
    </Button>
</div>

{#if showToast}
    <Toast color="green" onclick={() => showToast = false}>
        This person has been called on number {box.getPropertyValue()}.
        {#snippet icon()}
            <PhoneOutline class="{iconCls}" />
        {/snippet}
    </Toast>
{/if}

<style>
    .wrapper {
        display:flex;
        flex-direction: row;
        justify-content: center;
        align-items: center;
    }
</style>

Step 2: Add to the Global Section

Register the component in the editor’s global section:

// CourseSchedule/phase3/defs/main.edit#L3-L8

global {
    external {
        PersonIcon,
        PhoneButton
    }
}

Step 3: Include in the Projection

Use the wrapper in the Person projection by adding wrap=PhoneButton to the phone property. We’ll keep this in the externals.edit projection set we introduced earlier:

// CourseSchedule/phase3/defs/externals.edit

editor externals

Person {[
    [fragment nameAndIcon]
        Availability: ${self.availability checkbox} Competence: ${self.competence}
]
fragment nameAndIcon [
[external=PersonIcon] Nickname: ${self.name}
Full Name: ${self.fullName}
${self.phone wrap=PhoneButton}
]
}

Step 4: Register in the Starter Code

Tell Freon how to instantiate the component:

// CourseSchedule/phase3/src/external/externals.ts

import {setCustomComponents} from "@freon4dsl/core-svelte";
import PersonIcon from "./PersonIcon.svelte";
import PhoneButton from "./PhoneButton.svelte";

/**
 * Configure the external components used, so Freon can find them.
 */
export function configureExternals() {
    setCustomComponents([
        { component: PersonIcon, knownAs: "PersonIcon" },
        { component: PhoneButton, knownAs: "PhoneButton" },
    ]);
}

Final Result

Your editor now shows a phone button next to each number. Clicking it opens a snackbar with the number:

Image 'examples/CourseSchedule/Screenshot-step3.png' seems to be missing
Figure 1. Editor with added Phone Button

Conclusion

You’ve added a custom wrapper component to the Freon editor.
It wraps a phone number and includes a button that triggers a snackbar.
From here, you can build richer integrations—dialers, validations, or external lookups—fully integrated with your Freon projections.

Next, you’ll learn how to replace the component that renders a list.

© 2018 - 2025 Freon contributors - Freon is open source under the MIT License.