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

Dynamic components #640

Closed
Rich-Harris opened this issue Jun 14, 2017 · 22 comments
Closed

Dynamic components #640

Rich-Harris opened this issue Jun 14, 2017 · 22 comments

Comments

@Rich-Harris
Copy link
Member

This idea isn't at all fleshed out, but it's something that's come up a few times. Occasionally it's useful to create nested components dynamically:

<!-- this... -->
<[CurrentRoute]/>

<!-- ...instead of this: -->
{{#if route === 'home'}}
  <Home/>
{{elseif route === 'about'}}
  <About/>
{{elseif ...}}
  <-- ... -->
{{/if}}

The same idea could be used for e.g. lazy-loading components:

<LazyLoad src='./SomeComponent.html'>
  <p>loading, please wait...</p>
</LazyLoad>
<!-- LazyLoad.html -->
{{#if Component}}
  <[Component]/>
{{else}}
  {{yield}}
{{/if}}

<script>
  export default {
    oncreate () {
      import(this.get('src')).then(Component => {
        this.set({ Component });
      });
    }
  };
</script>

(To be clear, you could create the lazy loaded component programmatically, but this way is clearer and doesn't rely on you making a container element.)

What I haven't figure out is how you'd pass props down in that second case if you don't know know what props there are. Does it require a special syntax like <[Component] ...props/>? How do we know when props have changed, and ensure that changes are batched?

Also, <[Component]/> breaks syntax colouring, at least in GitHub and VSCode. Bit annoying. Any suggestions for an alternative that doesn't? This seems to work, though it looks a bit wild frankly:

<dynamic(foo ? Bar : Baz) ...props/>

(As an aside, <:Self/> and <:Window/> syntax colouring is broken in recent versions of VSCode... grrr.)

@taylorzane
Copy link
Contributor

I'll throw in my 2¢, since this is a feature (currently using the "map" workaround) that I'd greatly appreciate.

An alternative to the computed component name is something akin to Vue (static: <component is="my-component-name"></component> or dynamic: <component is="{{ myVar ? 'my-component' : 'your-component'></component>). You could have a reserved component called :DyanmicComponent and then you could do any magic you'd want on that.

@jacwright
Copy link
Contributor

What is the "map" workaround @taylorzane?

@taylorzane
Copy link
Contributor

Rich had an example in his initial post. But basically it's just a huge if/elseif block of all the components you'd like to possibly use. For example I have a bunch of icons, and I need to be able to pull an icon based on some data in the containing component, that's determined at runtime.

For example:

<!-- Icon -->
<div class="icon">
  {{#if icon === 'a'}}
  <IconA class="{{ iconClass }}"/>
  {{elseif icon === 'b'}}
  <IconB class="{{ iconClass }}"/>
  {{elseif icon === 'c'}}
  <IconC class="{{ iconClass }}"/>
  {{elseif icon === 'd'}}
  <IconD class="{{ logoClass }}"/>
  {{/if}}
</div>

<script>
import IconA from 'Components/Icons/A'
import IconB from 'Components/Icons/B'
import IconC from 'Components/Icons/C'
import IconD from 'Components/Icons/D'

export default {
  components: {
    IconA,
    IconB,
    IconC,
    IconD
  },

  data() {
    return {
      iconClass: ''
    }
  }
}
</script>

<!-- Parent -->
<div class="place-where-icon-goes">
  <Icon icon="{{ someVar }}" iconClass="my-special-icon"/>
</div>

<script>
import Icon from 'Components/Icon'

export default {
  components: {
    Icon
  },

  data() {
    return {
      icon: null
    }
  },

  oncreate() {
    const icon = getMyIconFromAPI().then(icon => {
      this.set({ icon })
    })
  }
}
</script>

@TehShrike
Copy link
Member

(continuing from #687 (comment))

Given a template like this:

<!-- BoldLink.html -->
<strong>
  <[LinkComponent] href="{{href}}">{{yield}}</[LinkComponent]>
</strong>

it would be great to support both of these cases:

<!-- App.html -->
<BoldLink 
 LinkComponent={MagicalClickInterceptingComponent} 
 href="/internal/link" 
>
   click
</BoldLink>

<BoldLink 
   LinkComponent={'a'} 
   href="//google.com" 
/>
   CLICK
</BoldLink>

with the second example there resulting in this output:

<strong>
  <a href="{{href}}">CLICK</a>
</strong>

@Rich-Harris
Copy link
Member Author

That would be rather tricky from the compiler perspective — BoldLink would basically need to accommodate both possibilities:

update (state, ...) {
  if (typeof state.LinkComponent === 'string') {
    linkComponent.href = state.href;
  } else {
    linkComponent.set({ href: state.href });
  }
}

...and so on. Unless we added a special internal component type, maybe:

function ElementComponent(name) {
  this.node = createElement(name);
}

ElementComponent.prototype.set = function(values) {
  for (var key in values) {
    // actually more complicated than this, because
    // of things like className and stuff that requires
    // setAttribute instead of direct property setting
    this.node[key] = values[key];
  }
};

// also probably need to figure out some stuff around bindings etc? eesh

/* LATER */
function create_main_fragment(component, state) {
  var linkComponent;

  return {
    create() {
      linkComponent = typeof state.LinkComponent === 'string' ?
        new ElementComponent(state.LinkComponent) :
        new state.LinkComponent(...); // also need to handle initial data
    },

    ...
  };
}

Not saying it couldn't be done, but I think it would probably be more practical just to declare an anchor component in your app:

<!-- Anchor.html -->
<a :href>{{yield}}</a>
<!-- App.html -->
<BoldLink 
 LinkComponent={MagicalClickInterceptingComponent} 
 href="/internal/link" 
>
   click
</BoldLink>

<BoldLink 
   LinkComponent={Anchor} 
   href="//google.com" 
/>
   CLICK
</BoldLink>

@fskreuz
Copy link

fskreuz commented Jul 12, 2017

Just throwing this one in. Ractive recently has this thing called Anchors (dynamic mounting points) and ractive.link() (manual data linking). With those, the user can manually dynamically attach/detach/link/unlink components in components instead of the lib. Good for things like router components. Everything happens JS-side instead of on the template.

2c :D

@cristinecula
Copy link
Contributor

I achieved something similar using a custom component that creates an instance of a component passed as a property:

<div ref:root></div>

<script>
export default {
  data() {
    return {
      data: {},
    };
  },

  oncreate() {
    this.observe('component', (Component, prev) => {
      if(prev) this.component.destroy();
      this.component = new Component({
        target: this.refs.root,
        data: this.get('data')
      });
    })
    this.observe('data', data => this.component.set(data));
    this.component.on('load', this.fire.bind(this, 'load'));
  },

  ondestroy() {
    this.component.destroy();
  },
};
</script>

It's used like this:

<Dynamic component="{{template}}" data="{{templateData}}" on:load="fadeIn()"/>

This works wonders, but it has a couple of downsides:

  1. I have to manually relay the child components events
  2. the component property must be a svelte component, imported in the parent, so no lazy-loading

I used this approach when implementing a Slideshow component that does not impose any layout for it's slides.
You can achieve a simple RouterOutlet using this approach:
https://svelte.technology/repl?version=1.41.2&gist=494d5e9034849d2c81660f327d5232f3

@paulocoghi
Copy link
Contributor

I believe the best solution is to implement what @PaulBGD mentioned on the issue #742 : "a way to pass components as properties" #742 (comment)

This is interesting because you can get an already compiled component through ajax, without compiling at run time.

And a use-case would be a project with a hundred or even thousands of components, where you want to load only the used ones in the requested route.

@Rich-Harris
Copy link
Member Author

Dynamic components are in 1.45:

<:Component {someExpressionThatEvaluatesToAComponentConstructor} someProp='whatever'>
  <!-- children go here -->
</:Component>

Will update the docs page soon.

@brtn
Copy link

brtn commented Jan 27, 2018

I could not find the docs on this feature. Can anyone provide an example?

@ChrisTalman
Copy link

@brtn I was able to make use of them with the example provided by @Rich-Harris. Is there something in particular you're wondering about?

Here is an example which switches between two components on a regular interval.

@brtn
Copy link

brtn commented Jan 28, 2018

Ok, thanks! I didn't see this example before. This helps 👍

@Conduitry
Copy link
Member

Dynamic components are also described here in the docs

@brtn
Copy link

brtn commented Jan 28, 2018

I am more sleep deprived than I realize, apparently :(

@louiidev
Copy link

@ChrisTalman seems your example is broken, has something changed?

@taylorzane
Copy link
Contributor

@louisgjohnson Unless you're still on v1.x I wouldn't reference his example anymore. Here's a v2.x compatible example: repl. Hope that helps. If you're still looking for a v1.x example, let me know.

@louiidev
Copy link

@taylorzane cheers mate, that's what I'm looking for (v2)

@s0kil
Copy link

s0kil commented Sep 19, 2018

looking forward to this also.

@Rich-Harris
Copy link
Member Author

@DanielSokil https://svelte.technology/guide#svelte-component

@BennyHinrichs
Copy link

Does this cover handling being able to add components that you get, for example, as instructions from an API? Like if you got back

{
  component: 'List',
  content: [
    {
      component: 'ListItem',
      content: ['Item 1']
    }, {
      component: 'ListItem',
      content: ['Item 2']
    },
  ],
  props: {
    'on:click': 'handleListOnClick()'
  }
}

Or however it might structured. Is there any way to turn that into Svelte components and inject it into the page during runtime? Sorry if this is the wrong place to ask.

@slominskir
Copy link

@BennyHinrichs Yeah, that is how I'm using the svelte:component tag. Not sure if there is a better way, but I declare all possible component constructors and then use that map to convert string component names found in JSON to Components. See: #2324 (comment)

@tinymachine
Copy link

Thanks for the example in #2324 (comment), @slominskir. For what it's worth, using ES6 shorthand you can simplify further from:

const components = {};
components['Label'] = Label;
components['Tree'] = Tree;
components['Menu'] = Menu;

to:

const components = { Label, Tree, Menu };

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

No branches or pull requests