BarefootJS

 view release on metacpan or  search on metacpan

lib/BarefootJS.pm  view on Meta::CPAN

}

sub render_child ($self, $name, @args) {
    my $renderer = $self->_child_renderers->{$name};
    die "No renderer registered for child component '$name'" unless $renderer;
    # Accept both the Mojo list form — `bf->render_child($name, k => v, ...)`
    # — and the single-hashref form — `$bf.render_child($name, { k => v })`.
    # Template languages whose method calls can't splat a hash into positional
    # args (Text::Xslate Kolon, Template Toolkit) pass one hashref instead.
    my %props = (@args == 1 && ref $args[0] eq 'HASH') ? %{ $args[0] } : @args;
    # JSX children come in via the engine's children-capture mechanism
    # (Mojo's `begin %>...<% end`, which produces a CODE ref returning a
    # Mojo::ByteStream). Materialize it through the backend before handing
    # the props to the child renderer so the child template sees
    # `$children` as already-rendered HTML. Guard on `exists` so a
    # childless invocation (`bf->render_child('counter')`) doesn't gain a
    # spurious `children => undef` key — preserving the historical "only
    # touch children when present" behaviour.
    $props{children} = $self->backend->materialize($props{children})
        if exists $props{children};
    # Renderer contract (#1897): the renderer is invoked with TWO
    # arguments — the props hashref and the INVOKING instance. A renderer
    # registered on the root may be called from a nested child render
    # (AccordionTrigger -> ChevronDownIcon), and the grandchild's scope /
    # slot identity must chain off the CALLER's scope id, not the
    # registrant's. Renderers unpack `@_` (`my ($props, $caller) = @_;`)
    # rather than enforcing arity with a one-arg subroutine signature —
    # see `register_child_renderer`.
    return $renderer->(\%props, $self);
}

# ---------------------------------------------------------------------------
# Bulk registration from build manifest
# ---------------------------------------------------------------------------
#
# `bf build` emits dist/templates/manifest.json describing every
# component the page might invoke (Counter, ui/button/index, ...).
# This helper walks that manifest and registers one child renderer per
# UI registry entry — the path shape `ui/<name>/index` maps to the
# `<name>` slot key Counter.html.ep and friends use via
# `<%= bf->render_child('<name>', ...) %>`.
#
# Each manifest entry carries an `ssrDefaults` hash derived statically
# from the component's JSX (prop destructure defaults + signal /
# memo initial values, see packages/jsx/src/ssr-defaults.ts). The
# child renderer seeds every template variable from that hash,
# preferring the caller's matching prop where one exists. This
# replaces the per-component `signal_init` callback that every
# scaffold's `app.pl` used to hand-roll for items 1/3 of issue #1416.
#
# `signal_init` remains as an opt-in override for cases the static
# extractor can't see through (e.g. signal initial values that
# reference imported helpers). When supplied for a given slot key
# it takes precedence over the manifest's `ssrDefaults` for that
# child, allowing callers to mix manual overrides with auto-derived
# defaults for siblings.
sub register_components_from_manifest ($self, $manifest, %opts) {
    my $signal_inits = $opts{signal_init} // {};
    my $parent_scope = $self->_scope_id;
    # Weaken the parent capture so the child-renderer closures stored on
    # `$self->_child_renderers` don't keep `$self` alive (the direct
    # closure <-> parent cycle). The controller is reached through `$parent`
    # at call time rather than captured strongly here, so the closures hold
    # no strong reference to `$c` either — see the controller-cycle note in
    # `new`. `$parent` is always live whenever a closure runs (the closure is
    # stored on `$parent`, so `$parent` outlives every invocation).
    weaken(my $parent = $self);

    for my $entry_name (keys %$manifest) {
        # `__barefoot__` is the runtime entry, not a component.
        next if $entry_name eq '__barefoot__';
        # Only UI registry components (path shape `ui/<name>/index`)
        # become child renderers; top-level page components are the
        # render target rather than a child.
        next unless $entry_name =~ m{^ui/([^/]+)/index$};
        my $slot_key = $1;
        my $marked = $manifest->{$entry_name}{markedTemplate} // '';
        next unless $marked;
        # `templates/ui/button/index.html.ep` → `ui/button/index`
        my $template_name = $marked;
        $template_name =~ s{^templates/}{};
        $template_name =~ s{\.html\.ep$}{};

        my $signal_init = $signal_inits->{$slot_key};
        my $manifest_defaults = $manifest->{$entry_name}{ssrDefaults};
        $self->register_child_renderer($slot_key, sub {
            # `$caller` is the instance whose template invoked
            # `render_child` (#1897) — for a nested render that is a child
            # instance, and the grandchild's scope/slot identity must chain
            # off ITS scope id (`root_s0_s0`), not the registrant's.
            my ($props, $caller) = @_;
            my $host = $caller // $parent;
            my $host_scope = $host->_scope_id // $parent_scope;
            # Child shares the parent's backend so nested renders go
            # through the same engine + controller (and inherit any
            # injected json_encoder). The controller is fetched via the weak
            # `$parent` at call time — never captured strongly — so the
            # closure adds no edge to the per-request reference cycle.
            my $child_bf = BarefootJS->new($parent->c, { backend => $parent->backend });
            my $slot_id = delete $props->{_bf_slot};
            # JSX `key` (a reserved prop) → data-key on the child's scope root
            # for keyed-loop reconciliation (see `data_key_attr`).
            my $data_key = delete $props->{key};
            $child_bf->_data_key($data_key) if defined $data_key;
            $child_bf->_scope_id(
                $slot_id ? $host_scope . '_' . $slot_id
                         : $template_name . '_' . substr(rand() =~ s/^0\.//r, 0, 6)
            );
            $child_bf->_is_child(1);
            # (#1249) Slot identity: host scope + slot id. Emitted as
            # bf-h / bf-m attributes by hydration_attrs.
            if ($slot_id) {
                $child_bf->_bf_parent($host_scope);
                $child_bf->_bf_mount($slot_id);
            }
            # Share the root registry so the child's own template can
            # render further imported components (#1897).
            $child_bf->_child_renderers($parent->_child_renderers);
            $child_bf->_scripts($parent->_scripts);
            $child_bf->_script_seen($parent->_script_seen);



( run in 1.574 second using v1.01-cache-2.11-cpan-df04353d9ac )