Skip to content

feat: functional template generation #15538

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

Merged
merged 86 commits into from
May 22, 2025
Merged

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)

closes #15799, closes #15801

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: 5a748f9

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)

* chore: alterative functional templating syntax

* fix: remove `.at(-1)`

* chore: move create elements, text, comment etc to `operations`

* chore: only use `append` to append to the fragment
@Rich-Harris
Copy link
Member

Rich-Harris commented May 22, 2025

Implementation-wise I think this is ready to go, am just wondering about the naming. Am allergic to option names with too many syllables, so I'd be inclined to go with templating: '...' or even templates: '...' over templatingMode: '...' (though I'm also open to other ideas — is it right to speak of 'templates' in the functional case? $.template is so named because of <template>, which we're not using here).

As for the value, 'string' feels a bit vague — perhaps 'innerHTML' instead? I also think 'functional' is a somewhat overloaded term, though I don't have a great suggestion for an alternative right now.

Once we've picked names we'll also need to flesh out the docs a bit to explain why you'd choose one over the other (e.g. innerHTML is faster, but has CSP caveats)

@paoloricciuti
Copy link
Member Author

Implementation-wise I think this is ready to go, am just wondering about the naming. Am allergic to option names with too many syllables, so I'd be inclined to go with templating: '...' or even templates: '...' over templatingMode: '...' (though I'm also open to other ideas — is it right to speak of 'templates' in the functional case? $.template is so named because of , which we're not using here).

I think templating is fine since you are specifying how you want the template part of your svelte component to be created.

As for the value, 'string' feels a bit vague — perhaps 'innerHTML instead? I also think 'functional' is a somewhat overloaded term, though I don't have a great suggestion for an alternative right now.

I'm just worried innerHTML could feel "wrong"...maybe the alternative to functional could be dom? Although it feels less clear about what's happening.

@Rich-Harris
Copy link
Member

For the XHTML issue, I wonder if we should just replace <!> with <!----> before passing it to the <template>, and add /> for void elements. That way people who for whatever reason need to use XHTML a) don't have to use slow mode, and b) don't have to know that they have to use slow mode

@Rich-Harris
Copy link
Member

Made the XHTML change

@paoloricciuti
Copy link
Member Author

paoloricciuti commented May 22, 2025

Made the XHTML change

Great...can we add a second change set so it keep tracks of this in the change log?

@Rich-Harris
Copy link
Member

Great...can we add a second change set so it keep tracks of this in the change log?

way ahead of you 😛

@Rich-Harris
Copy link
Member

@paoloricciuti technically I can approve this PR since you're the creator, but since a bunch of commits are mine I feel like they ought to get a stamp of approval from you before I do — good to merge once the tests pass?

@paoloricciuti
Copy link
Member Author

@paoloricciuti technically I can approve this PR since you're the creator, but since a bunch of commits are mine I feel like they ought to get a stamp of approval from you before I do — good to merge once the tests pass?

Good to go! Thanks as usual for fixing my s**t lol 🧡

@Rich-Harris
Copy link
Member

team work makes the dream work! excited to have this in

@paoloricciuti
Copy link
Member Author

Now let me fix the playground so that we can also quickly test this in development

@paoloricciuti paoloricciuti merged commit d1141a1 into main May 22, 2025
13 checks passed
@paoloricciuti paoloricciuti deleted the templateless-template-generation branch May 22, 2025 15:21
@github-actions github-actions bot mentioned this pull request May 22, 2025
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.

<input> is not valid XHTML regression in Svelte 5 <!> is not valid XHTML regression in Svelte 5
3 participants