Skip to content

Add support for mixins #1041

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

Closed
PaulMaly opened this issue Dec 22, 2017 · 11 comments
Closed

Add support for mixins #1041

PaulMaly opened this issue Dec 22, 2017 · 11 comments

Comments

@PaulMaly
Copy link
Contributor

PaulMaly commented Dec 22, 2017

Hi everyone!

To continue the conversation begun in a chat, I want to suppose my vision of Mixin implementation to Svelte.

Theory about mixins

Wikipedia

Medium article

Vue mixins

First of all, I don't want to violate Svelte's simplicity. That's why I'll try to design my proposal so simple as I can.

So in my opinion, Svelte's mixin need to be just a POJO-object that just reflects already used "options" object. If we accept it everything else becomes so simple. For example, User.js mixin could look like this:

export default {
    oncreate () {
        let token = localStorage.getItem('token');
        token && fetch('/me?token=${token}').then(r => r.json())
					.then((user) => this.set({user}));
    },
    data: () => ({
        user: {
            firstName: '',
	    lastName: ''
        }
    }),
    computed: {
        fullName: (user) => user.firstName + ' ' + user.lastName
    },
    methods: {
        login () {
            // login logic
        }
    },
    helpers: {
        generatePassword(len) {
            return Math.random().toString(36).substr(2, len);
        }
    }
};

And used like this:

<script>
    import userMixin from './mixins/user'

    export default {
        mixins: [userMixin],
        data: () => ({
            otherData: {}
       }),
       oncreate () {...},
       methods: {...}
    };
</script>

So, just like Vue does. Under the hood, Svelte just need to merge all these props together, for example like this:

function merge(options, mixin, force) {

	const _join = (prop, obj) => {
		if (Array.isArray(options[prop])) {
			options[prop] = options[prop].concat(obj);
		} else if (isObject(this[prop])) {
			Object.assign(this[prop], obj);
		} else if (force) {
			this[prop] = obj;
		}
	};

	for (let p in mixin) {
		if (typeof mixin[p] === 'undefined') {
			continue;
		}
		if (typeof options[p] !== 'undefined' && options.hasOwnProperty(p)) {
			if (typeof options[p] !== typeof mixin[p]) {
				throw new Error("Property already exist and mixin value hasn't corresponding type.");
			}
			if (typeof options[p] === 'object' && mixin[p] !== null) {
				_join(p, mixin[p]);
                        } else if (typeof options[p] === 'function') {
                                 // actually I don't know how exactly Svelte handle it
			} else if (force) {
				options[p] = props[p];
			} else {
				throw new Error("Property already exist and can be re-assigned only in force mode.");
			}
		} else {
			options[p] = mixin[p];
		}
	}
	return options;
}

And we just need to apply this function to all mixins sequentially:

// somewhere inside Svelte constructor

const mixins = options.mixins || [];
delete options.mixins;
mixins.forEach(merge.bind(null, options));

I think it's not so hard to implement but gives us many advantages.

Also, I think we really need to have a declarative way to set up observers and event listeners. It could be implemented just like Ractive does:

...
observe: {
    show ( value, old ) {

    },
    users: {
      handler ( value, old ) {

      },
      init: false,
      strict: true
    }
  },
  on: {
    create () {

    },
    somethingHappened: {
      handler ( ctx ) {
      },
      once: true
    }
  }
}
...

Also, we could provide something like Vue's plugins which is just a function invoked in Component's instance context. Basic implementation looks like this:

Component.prototype.use = function (plugin, ...args) {
	const plugins = (this._plugins || (this._plugins = []));
	if (typeof plugin === 'function' && ! plugins.includes(plugin)) {
		plugin.apply(this, args);
		plugins.push(plugin);
	}
};

In difference from Vue, in my vision, plugin works with the concrete instance, not with global things.

Mixins and plugins both need to good separation of the code to modules. Mixin more declarative thing, it needs to construct an object. Plugin for imperative usage, to add some stuff to the instance.

@Rich-Harris if you too busy, I can create a PR for these features if you'll show me the right way to do these things.

@ALL, if you, guys, have your own proposals feel free to join the tread!

@tomcon
Copy link

tomcon commented Dec 22, 2017

Svelte's mixin need to be just a POJO-object that just reflects already used "options" object
++1 for mixins (especially as there is no .extend())

+1 for declarative way to set up observers

maybe same for event listeners

@PaulBGD
Copy link
Member

PaulBGD commented Dec 22, 2017

I've recently been working on a project that's MVC styled using Svelte's Store. Separating everything correctly hasn't left any space for needing mixins and so I'm very interested in the situations that you foresee mixins being used for.

@Rich-Harris
Copy link
Member

Unfortunately, the proposal outlined above isn't possible without majorly rethinking how Svelte is architected — I'm going to focus on computed to illustrate why.

When Svelte compiles a component, it checks to see if there is a computed property. Then, it reads all the properties of computed so that it understands the graph of dependencies, and uses that information to generate a function.

For example, this...

<script>
  export default {
    computed: {
      c: b => b * 2,
      b: a => a * 2
    }
  };
</script>

...results in this:

App.prototype._recompute = function _recompute(changed, state) {
  if (changed.a) {
    if (differs(state.b, (state.b = b(state.a)))) changed.b = true;
  }

  if (changed.b) {
    if (differs(state.c, (state.c = c(state.b)))) changed.c = true;
  }
}

If we had a computed property in a mixin, Svelte wouldn't know about it — it wouldn't be able to generate that code.

Now, we could fix that, if we were highly motivated: the compiler could look at the userMixin identifier and figure out that it's the default export from ./mixins/user. And if it knew about the filesystem (currently it doesn't — it just knows about your source code), and the location of the component it was compiling, then it could resolve that relative path and load that file.

At that point, it would have the extra knowledge it needed to generate the function correctly. But it could break in a dizzying number of ways. What if the mixin was written in TypeScript, or needed to be transformed by Babel? Does Svelte now need to know about those? What if it's imported from npm — does Svelte need to know how to resolve files in node_modules? What about something more esoteric — maybe the project uses jspm instead?

The file itself would have to be written with some fairly tight constraints. What if a new developer on the team thought 'you know, this would be easier to read if we did it this way instead':

import fullName from './computed/fullName.js';

const mixin = {};
mixin.computed = {
  fullName
};

Oops, you just broke everything, and now you have to explain to your team member why this particular JavaScript module, unlike all the others, has to be written in a very particular way and can't rely on the Babel transforms or injected variables the rest of your app is using.

As you can see, the complexity soon becomes completely overwhelming. No new features get added to Svelte, because we have to spend all our time dealing with the edge cases that fall out of these new requirements.


Now on top of that, it's far from clear that mixins are actually the right way to solve the problem at hand. As I've expressed elsewhere, it makes it much harder to understand what a component's capabilities are. And there are some real issues that arise when you have multiple mixins in a component, as the React community found.

You can share code between components — I often have a central helpers.js file, for example. I don't think it's so terrible having to do this sort of thing...

import { formatDate, addCommas } from '../helpers.js';

export default {
  helpers: { formatDate, addCommas }
};

...and it's much easier to see when you actually no longer need, say, addCommas. Which in turn means you're less likely to end up carting technical debt around.

I'm not unsympathetic to the idea that certain forms of code reuse could be made easier, but hopefully this spells out why I think there's a high barrier.

@PaulMaly
Copy link
Contributor Author

PaulMaly commented Dec 22, 2017

@PaulBGD

I've recently been working on a project that's MVC styled using Svelte's Store. Separating everything correctly hasn't left any space for needing mixins and so I'm very interested in the situations that you foresee mixins being used for.

I don't use MVC architecture because it's too bulky. Just look what MVC is really mean - http://vuemc.io/. I don't think you could do the same with Svelte.

I wonder, how you suppose to resolve the case which I describe above - then we need to have a complex User-model in multiple components. As you can see, there are many things depending on each other (for example, fullName depends on firstName and lastName). So we can't declare all these things separately because it can cause discrepancies and hard to control.

@Rich-Harris

Unfortunately, the proposal outlined above isn't possible without majorly rethinking how Svelte is architected

Perhaps, it should be at the time current architecture doesn't allow to implement new features?

Now, we could fix that, if we were highly motivated: the compiler could look at the userMixin identifier and figure out that it's the default export from ./mixins/user. And if it knew about the filesystem (currently it doesn't — it just knows about your source code), and the location of the component it was compiling, then it could resolve that relative path and load that file.

If you say that Svelte compiler nothing know about filesystem and actually don't import dependencies, so how it works with these things and what differences:

<script>
  import MyComponent from 'MyComponent.html'
  import {foo} from 'somemodule'
  import mixin from 'mymixin'

  export default {
    components: {
        MyComponent
    },
    methods: {
        foo
    }
  };
</script>

At that point, it would have the extra knowledge it needed to generate the function correctly. But it could break in a dizzying number of ways. What if the mixin was written in TypeScript, or needed to be transformed by Babel? Does Svelte now need to know about those? What if it's imported from npm — does Svelte need to know how to resolve files in node_modules? What about something more esoteric — maybe the project uses jspm instead?

So, is it mean that now we are limited to use the module from node_modules and even from our own project folders, right? It turns out that I can't do these things:

<script>
  import {debounce} from 'lodash'
  import myHelpers from './helpers'

  export default {
    helpers: {
        myHelpers
    },
    methods: {
        debounce
    }
  };
</script>

How does it suppose to be? How can I write the serious application without 3rd party modules? It's a nonsense!

The file itself would have to be written with some fairly tight constraints. What if a new developer on the team thought 'you know, this would be easier to read if we did it this way instead':

It's not a problem because we already have many constraints when writing Svelte-components. We can suffer it, using svelte-mixins too because it's not the "some module" but a special type of the module.

Now on top of that, it's far from clear that mixins are actually the right way to solve the problem at hand. As I've expressed elsewhere, it makes it much harder to understand what a component's capabilities are. And there are some real issues that arise when you have multiple mixins in a component, as the React community found.

I used mixins with Ractive many years and my experience gives me a good results. Concerning React-guys, I think that it the only opinion of Dan Abramov. Just check the comments below that article. How can we trust the guy who has created so silly thing as Redux?

You can share code between components — I often have a central helpers.js file, for example. I don't think it's so terrible having to do this sort of thing...

Oh, so how you import that files if Svelte compiler nothing know about file-system? And why we can't import an object from the same place?

Or maybe you have an idea how to solve the task I described above in this way? You don't want to tell what I should do so:

import { login, auth } from '../methods.js'
import { generatePassword } from '../helpers.js'
import { user } from '../models.js'

export default {
  oncreate () {
        auth();
  },
  data: () => ({
      user
  }),
  computed: {
       fullName: (user) => user.firstName + ' ' + user.lastName
  },
  methods: { login },
  helpers: { generatePassword }
};

It's weird and can bring us to inconsistent cases.

@Rich-Harris
Copy link
Member

Perhaps, it should be at the time current architecture doesn't allow to implement new features?

It allows us to implement lots of new features, and our roadmap is very full! Just not this new feature.

If you say that Svelte compiler nothing know about filesystem and actually don't import dependencies, so how it works with these things and what differences:

Of course you can import stuff — Svelte just doesn't know (or care) about what's inside the files you're importing. It leaves the import declarations intact for your bundler to handle.

So, is it mean that now we are limited to use the module from node_modules and even from our own project folders, right? It turns out that I can't do these things:

Yes, you can. Of course you can! That would be insane if you couldn't.

It's not a problem because we already have many constraints when writing Svelte-components.

There's a huge difference between those constraints existing in an .html file, which is unambiguously a Svelte component, and a .js file. It would be wildly confusing to start introducing hard constraints on some subset of your JS files.

@PaulMaly
Copy link
Contributor Author

PaulMaly commented Dec 22, 2017

@Rich-Harris

Just not this new feature.

Ok, can we at least implement component.use() feature? And also declarative observe and on ? Perhaps it will reduce the pain...

Yes, you can. Of course you can! That would be insane if you couldn't.

So, why I can't import a simple object? Do you analyze Svelte components on the fly? I thought that you use something like posthtml-parser to parse the file to separated parts eg "style", "script", "template" and after execute js to get exported object or something like that. And only after that analyze the code.

There's a huge difference between those constraints existing in an .html file, which is unambiguously a Svelte component, and a .js file. It would be wildly confusing to start introducing hard constraints on some subset of your JS files.

Ok, it could be .mixin extension. It doesn't matter.

@PaulBGD
Copy link
Member

PaulBGD commented Dec 22, 2017

@PaulMaly

I don't use MVC architecture because it's too bulky. Just look what MVC is really mean - http://vuemc.io/. I don't think you could do the same with Svelte.

I actually have been doing the same thing with Svelte! https://twitter.com/PaulBGD/status/944058331290898432
It's very fast and easy to add new features to the codebase without having to rewrite methods. It's also super easy to test.

I wonder, how you suppose to resolve the case which I describe above - then we need to have a complex User-model in multiple components. As you can see, there are many things depending on each other (for example, fullName depends on firstName and lastName). So we can't declare all these things separately because it can cause discrepancies and hard to control.

You can put firstName and lastName in a Store (or the highest svelte component, but I try to avoid that with MVC), then compute the fullName.

Features like mixins seem really handy initially, but it creates bad patterns and causes a lot of code change just to add simple features.

@PaulMaly
Copy link
Contributor Author

PaulMaly commented Dec 22, 2017

@PaulBGD

It's very fast and easy to add new features to the codebase without having to rewrite methods. It's also super easy to test.

Hm, controller, models, all is svelte-components? What markup you provide in these things? Actually, I don't udnerstand. Could you please provide some examples of controller and model?

You can put firstName and lastName in a Store (or the highest svelte component, but I try to avoid that with MVC), then compute the fullName.

I don't get it. Store is OK, because it propagated to all components tree, but how adding this things to top component will helps me to get these props in child components? All components are isolated. Could you please provide some example too?

Features like mixins seem really handy initially, but it creates bad patterns and causes a lot of code change just to add simple features.

As I already said, I'm using mixins with Ractive many years and my experience gives me a good results.

@PaulBGD
Copy link
Member

PaulBGD commented Dec 22, 2017

Here's an example: https://svelte.technology/repl?version=1.49.1&gist=47dfce0e82992650aad8233cf5d12ac7
App.html should really be called "Something"Model, but basically in this ideal MVC pattern you'd use a model to handle the logic (and thus computations) and pass the result to the controller.

A similar pattern when you use fullName a lot with different variables would be to create a helper for it like Rich suggested and that'd look like this: https://svelte.technology/repl?version=1.49.1&gist=71e731d94534749ee5addfdbad326021

I'm not going to suggest that MVC is the official, only pattern of using Svelte, however I personally think it leads to better code quality than mixins.

@PaulMaly
Copy link
Contributor Author

PaulMaly commented Dec 22, 2017

@PaulBGD

Wiki:

The model is the central component of the pattern. It expresses the application's behavior in terms of the problem domain, independent of the user interface.[6] It directly manages the data, logic and rules of the application.
A view can be any output representation of information, such as a chart or a diagram. Multiple views of the same information are possible, such as a bar chart for management and a tabular view for accountants.
The third part or section, the controller, accepts input and converts it to commands for the model or view.

I don't see any parts of this pattern in your code. Only titles. If this code is good for Svelte, so..it's regrettable.

So, if you need have a props firstName and lastName and computed prop fullName, and also additional method login() in multiple components, you would define it in each? How many time you could do that before you will be tired?

@PaulMaly
Copy link
Contributor Author

PaulMaly commented Dec 22, 2017

@Rich-Harris It's a pity, but I think we can skip the questions about mixins and plugins. I checked the source code a little bit, seems you're right.

How about observe and on ?

UPDATE: I'll raise a new issue for this part.

@PaulMaly PaulMaly mentioned this issue Jun 13, 2018
sacrosanctic pushed a commit to sacrosanctic/svelte that referenced this issue Dec 24, 2024
sync svelte docs

Co-authored-by: Rich-Harris <[email protected]>
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

4 participants