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

feat: functional template generation #15538

Open
wants to merge 17 commits into
base: main
Choose a base branch
from

Conversation

paoloricciuti
Copy link
Member

@paoloricciuti paoloricciuti commented Mar 18, 2025

This swap our template.push calls from bare strings to a series of instructions that looks like this

You can use this playground link if you want to play around with the output.

[
    { "kind": "create_element", "args": ["main"] },
    { "kind": "set_prop", "args": ["style", "background: red"] },
    { "kind": "push_element" },
    { "kind": "create_element", "args": ["div"] },
    { "kind": "push_element" },
    { "kind": "create_text", "args": ["cool "] },
    { "kind": "create_element", "args": ["b"] },
    { "kind": "push_element" },
    { "kind": "create_text", "args": ["stuff"] },
    { "kind": "pop_element" },
    { "kind": "pop_element" },
    { "kind": "create_text", "args": [" "] },
    { "kind": "create_element", "args": ["input"] },
    { "kind": "set_prop", "args": ["checked"] },
    { "kind": "set_prop", "args": ["type", "checkbox"] },
    { "kind": "push_element" },
    { "kind": "pop_element" },
    { "kind": "create_text", "args": [" "] },
    { "kind": "create_anchor" },
    { "kind": "pop_element" }
]

for the moment i've basically written some code that generate the string for the template from this series of instructions but i'll work on a separate transformation to generate a series of document.createElement (and in the future renderer.createElement) from this series of instructions.

There's one thing that i don't properly like and it's the fact that there are those push_element and pop_element instructions that don't translate into anything and just serve the purpose of delimit the children...but this allow us to not pass around variables and just refer to "the last element"

Update

I've also included the transformer and the option to convert with string template or with functional template and updated the test suite to run with both modes.

The new option will look like this

// svelte.config.js
export default {
    templatingMode: "string" // or "functional",
}

and the code produced looks like this.

import 'svelte/internal/disclose-version';
import 'svelte/internal/flags/legacy';
import * as $ from 'svelte/internal/client';

var root = $.template_fn(() => {
    var main = document.createElement('main');

    main.setAttribute('style', 'background: red')

    var div = document.createElement('div');

    main.insertBefore(div, undefined)

    var text = document.createTextNode('cool');

    div.insertBefore(text, undefined)

    var b = document.createElement('b');

    div.insertBefore(b, undefined)

    var text_1 = document.createTextNode('stuff');

    b.insertBefore(text_1, undefined)

    var text_2 = document.createTextNode(' ');

    div.insertBefore(text_2, undefined)

    var input = document.createElement('input');

    div.insertBefore(input, undefined)
    input.setAttribute('checked', '')
    input.setAttribute('type', 'checkbox')

    var text_3 = document.createTextNode(' ');

    div.insertBefore(text_3, undefined)

    var comment = document.createComment('');

    div.insertBefore(comment, undefined)

    var fragment = document.createDocumentFragment();

    fragment.append(main)
    return fragment;
});

export default function App($$anchor) {
    var main = root();
    var div = $.child(main);
    var input = $.sibling($.child(div), 3);

    $.remove_input_defaults(input);

    var node = $.sibling(input, 2);

    {
        var consequent = ($$anchor) => {};

        $.if(node, ($$render) => {
            if (true) $$render(consequent);
        });
    }

    $.reset(div);
    $.reset(main);
    $.append($$anchor, main);
}

If you want to play around with it here's a stackblitz with everything setup.

Also since this is quite a big PR if you want i can split it in two (even tho it's already separated by commits well enough)

Before submitting the PR, please make sure you do the following

  • It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs
  • Prefix your PR title with feat:, fix:, chore:, or docs:.
  • This message body should clearly illustrate what problems it solves.
  • Ideally, include a test that fails without this PR but passes with it.
  • If this PR changes code within packages/svelte/src, add a changeset (npx changeset).

Tests and linting

  • Run the tests with pnpm test and lint the project with pnpm lint

Copy link

changeset-bot bot commented Mar 18, 2025

🦋 Changeset detected

Latest commit: a48df4a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
svelte Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@svelte-docs-bot
Copy link

Copy link
Contributor

Playground

pnpm add https://pkg.pr.new/svelte@15538

@paoloricciuti paoloricciuti changed the title feat: templateless template generation feat: functional template generation Mar 20, 2025
@paoloricciuti paoloricciuti marked this pull request as ready for review March 20, 2025 13:56
@Rich-Harris
Copy link
Member

Should we create a helper function that lets us emit more compact code? The hello world currently looks like this:

var root = $.template_fn(() => {
  var h1 = document.createElement('h1');
  var text = document.createTextNode('hello world');

  h1.insertBefore(text, undefined)

  var fragment = document.createDocumentFragment();

  fragment.append(h1)
  return fragment;
});

If template_fn looked something like this...

function template_fn(flags, ...items) {
  var fragment = document.createDocumentFragment();

  // TODO get from flags
  var ns;
  
  for (var item of items) {
    fragment.append(create_item(item, ns));
  }

  return fragment;
}

function create_item(item, ns) {
  if (item === undefined) {
    return document.createComment('');
  }

  if (typeof item === 'string') {
    return document.createTextNode(item);
  }

  var name = item[0];

  if (!ns) {
    if (name === 'svg') return create_item(item, 'http://www.w3.org/2000/svg');
    if (name === 'math') return create_item(item, 'http://www.w3.org/1998/Math/MathML');
  }

  var element = ns ? document.createElementNS(ns, name) : document.createElement(name);

  for (var i = 1; i < item.length - 1; i += 2) {
    element.setAttribute(item[i], item[i + 1]);
  }

  const children = item[i];

  if (children) {
    for (var child of children) {
      element.append(create_item(child));
    }
  }

  return element;
}

...then it could look like this instead:

var root = $.template_fn(0, ['h1', ['hello world']]);

On larger apps this would become significant. I might be glossing over some details of e.g. attributes vs props but you get the idea

@paoloricciuti
Copy link
Member Author

The point is that we should then substitute document.createElement with renderer.createElement where renderer come from the user specified path.

We could probably accept that as an input of the template_fn tho 🤔

@paoloricciuti
Copy link
Member Author

This would also move more computation at the runtime as opposed as the build time (probably not really noticeable but something to keep in mind nonetheless)

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