Vue.extend()
works well but i think there is no way to extend the corresponding template without changing scope. Here is an example of what i'd like to achieve. <block>
is like <slot>
but i renamed it not to be confused with the current slot implementation
const BaseForm = Vue.extend({
template: `
<form>
<block name="header">
<h1>{{ title }}</h1>
</block>
<block name="content">Form inputs go here</block>
<block name="actions">
<button @click="saveRecord">Save</button>
<button @click="deleteRecord">Delete</button>
</block>
</form>
`,
data () {
return {
title: 'Base Title',
apiUrl: null,
record: {}
}
},
methods: {
saveRecord () {
// axios.post(this.apiUrl + this.record.id)...
},
deleteRecord () {
// axios.delete(this.apiUrl + this.record.id)...
}
}
})
const PersonForm = BaseForm.extend({
template: `
<extend>
<div block="header">
{{ title }} From Person Form
<button @click="saveRecord">Save from header</button>
</div>
<div block="content">
<input type="text" v-model="record.name" />
<input type="text" v-model="record.age" />
<button @click="incAge">Increase age</button>
</div>
</extend>
`,
data () {
return {
title: 'My Title',
apiUrl: '/api/my/',
record: {
id: 1,
name: '',
age: 0
}
}
},
methods: {
incAge () {
this.record.age++
}
}
})
So js is merged as usual but then the base template blocks are replaced with the new ones when compiling the template. So everything works within the same scope.
<block>
and <extend>
can be changed with different words/options in the api
Does this make sense? Or is there a better way to do it? Or is it not intact with Vue principles?
The end results should be:
{
template: `
<form>
<div>
{{ title }} From Person Form
<button @click="saveRecord">Save from header</button>
</div>
<div>
<input type="text" v-model="record.name" />
<input type="text" v-model="record.age" />
<button @click="incAge">Increase age</button>
</div>
<button @click="saveRecord">Save</button>
<button @click="deleteRecord">Delete</button>
</form>
`,
data () {
return {
title: 'My Title',
apiUrl: '/api/my/',
record: {
id: 1,
name: '',
age: 0
}
}
},
methods: {
incAge () {
this.record.age++
},
saveRecord () {
// axios.post(this.apiUrl + this.record.id)...
},
deleteRecord () {
// axios.delete(this.apiUrl + this.record.id)...
}
}
}
Hi, thanks for filling this issue. It seems that you're trying to make PersonForm
's template access two scopes (parent and itself) at the same time, which violates the principal of vue's component system and thus coupling the implementation detail of the parent and the child.
If you'd really want to do it this way, a simpler approach would be accessing this.$parent
, e.g.
this.$parent.saveRecord
inside PersonForm
. But the recommended approach would be passing everything via props, and using custom events for parent-child communication. e.g.
inside PersonForm
:
<button @click="$emit('save')/>
inside BaseForm
:
<PersonForm @save="saveRecord"/>
Thanks @fnlctrl . I'm familiar with the approaches you provide but the problem here is the v-model bindings on the inputs/customInputsComponents. I don't think that currently there is a way around this that's why this was more of a proposal.
I'm sure this is probably against Vue's parent-child relation and component specification but extending the Template in context of using Vue.extend() makes sense to me. Have in mind that i'll be using the BaseFrom a lot.. not just once in PersonForm.
Will probably end up just using the js extend and writing the full templates
Vue.extend()
only serves for creating a subclass of Vue (to create component instances later), it has nothing to do with template/scope etc though the name extend
may have suggested that to you.
What you're talking about is merging two scopes - in your example PersonForm
has it's data
(to which v-model
s in the template are bound), and then you want it to be merged with BaseForm
(so that the data and methods are available to both scopes), which doesn't seem to be a good idea to me.
Maybe you can try something like:
inside PersonForm
:
<button @click="$emit('save', $data)"/> //pass the whole data object to parent
inside BaseForm
:
<PersonForm @save="saveRecord"/>
where saveRecord
is
saveRecord (data) {
axios.post(data.apiUrl + data.record.id)...
}
@fnlctrl @purepear I think this is still an issue worth discussing. It sounds like you guys are talking about two slightly different things maybe a miscommunication.
The problem is when you extend a component, its extremely useful for all of the JS options, methods, data etc. But... for the template you either have to take the parents template as is or if you want to slightly tweak it you have to copy paste the entire thing, which is very difficult to maintain. You now have to maintain two templates anytime you change the base component.
You can add more template or options to the base component so that inherited component's can make adjustments to the template but then the base component has to have some pre-knowledge of the components that extend it and then they are tightly coupled which is worse.
Slots and scoped slots are tools for composition and not inheritance. Slots are great for a car has tire relationship but I think purepear was suggesting a tool for a car is a vehicle relationship.
As an example I have a giant custom form with tons of fields and I have an inherited from a component where 99% of the JS and 90% template is the same, but I've had to copy paste the entire template and duplicate a significant amount of code just for the 10% that is different. I'd be happy to contribute to improving the Vue.js project and look into this more.
Here is a simple example of what this feature might look like (of course in this example its so simple it would be much easier to solve with a prop but its just to illustrate).
var Hello = Vue.component('hello', {
template: '<p>Hello</p> <block>World</block>!'
})
The inheritance strategy instead of just totally replacing the template might first try something like....
Vue.component('hello-universe', {
extends: Hello,
template: "<block>Universe</block>
}
the extend merge algorithm:
if component template is a block tag:
component.template = super.template.replace(<block></block>, component.template)
else:
component.template = super.template
@purepear @fnlctrl I just implemented a quick proof on concept of this for some of my worst copy/pastes. I had to put my template into a multiline string for the time being string unfortunately. I'm not deep enough in vue to know how the .vue file template tags are parsed in the build pipeline. Basically I just took my template moved it into a JS multiline string and stored in on the ancestor component as "html" option. Then for the ancestor replaced the
@aldencolerain One way to deal with the code duplication is to use Pug. You'll probably have to put the template in separate file and pass pug config to vue-loader (basePath or sth).. It's not clean but it might work. I don't think Vue will implement this template extend concept cause it'll be confusing for developers how they should structure the component tree. Also i haven't seen this in other js libs react/angular... though i wish :)
I imagine it would be like how Pug/Jade extends the layout.
P.S. I use pug so much i didn't consider that it might not seem intuitive to others.. but other template engines might work too
@purepear Thanks. I'll check it out. I really like Vue's template engine and I know a lot of other template engines support this simplified inheritance feature as well as composition. Maybe what would be a nice compromise would be to store the source template string on the object automatically when compiling a Vue file along with the render function. That would be an very very easy adjustment (if it isn't already there) and would give people a lot of flexibility with the templates.
@fnlctrl Do you know if the template source string before its compiled to a render function is stored on a component? That would be a very simple change that would be a huge help and give a lot of flexibility without Vue.js having to support any addition template features.
I guess if it ends up being the decision to totally not support inheritance... this might be useful to others hitting this issue:
https://facebook.github.io/react/docs/composition-vs-inheritance.html
someone wrote an article about this http://vuejsdevelopers.com/2017/06/11/vue-js-extending-components/
What doesnt feel right about composition is that the logic must be chained to the extended component residing inside the inherited... but maybe a combination of composition and extend/mixin could be a good solution, just exploring the idea
parent base-form:
<template>
<h1>Standard Header</h1>
<slot></slot>
<button @onclick="submit()">
</template>
specific user-form:
<template>
<base-form ref="baseForm" v-model="data"><!--w
<!--...lots of inputs specific to users...-->
</base-form>
</template>
usage
<user-form ref="form"></user-form>
I would expect to do $refs.form.submit()
, but its not possible...
I would either have to do $refs.form.baseForm.submit()
or have a submit()
method in each specific forms that I create using the base-form.
The other way would I see is to use also base-form in the extend
option (or mixins
depending on desired result). This way, we can use
The specific form could look like:
<template>
<base-form ref="baseForm" v-model="data">
<!--...lots of inputs specific to users...-->
</base-form>
</template>
<script>
import BaseFormfrom './BaseForm.vue'
export default {
extends: BaseForm,
components: {BaseForm},
// ...additional methods, data, etc.. they will be overriden by BaseForm as expected
}
</script>
Now with this I, my submit()
method as expected.
Does that make sense? ..I'm not sure since the
But if we have a different extend/mixin (or just a util?) that gets all props/data(using getter/setter)/methods from specified $ref (ie: base-form) and creates a complete mapping?
For example, if submit() was not declared in user-form, submit() {return $ref.baseForm.submit()} would be automatically created for user-form
That may add up to why event broadcasting and global state is common practice in these framework... for me it's not correct
I like the idea of using the baseform slots directly in the userform.. perhaps we can just use them directly in the overriden template in a natural way
base-form
<template>
<slot name="header"><h1>Standard Header</h1></slot>
<slot></slot>
<slot name="toolbar">
<button @onclick="submit()">
</slot>
</template>
user-form 1
<template extendstemplate> <!-- similar to `inlinetemplate` option? -->
<extend name="header"><h1>Users Form</h1></extend>
<!--...lots of inputs specific to users, would appear in <slot></slot> of base-form...-->
<slot name="sidebar">Default sidebar content</slot><!-- a new slot! -->
</template>
super-user-form!
<template extendstemplate>
<extend name="sidebar">Custom super content</slot><!-- slight changes -->
</template>
So it's essentially, reusing the defined slots from the extended class?
But this does not allows the template to "expand", let's say I want to enclose the form like a decorator. In that case, yes composition is necessary, but could be like this...
user-form 2
<template>
<panel>
<template name="title">Another Title</template>
<template name="content">
<extends>
<template name="header"><h1>Users Form</h1></template >
<!--...lots of inputs specific to users, would appear in <slot></slot> of base-form...-->
</extends>
</template>
</panel>
</template>
I would agree that user-form 2 concept is a bit breaking the concept of components, but user-form 1 concept seems logical to me, as it really only extends the existing component structure?
Here is something that worked for me as it allows me to easily override the existing slots.
Note that it will not work with .vue
files as they do not allow to define the template this way.
helpers/extendTemplateSlots.js
export default function extendTemplateSlots(template, slots) {
Object.keys(slots).map((slotName, index) => {
let regexPattern = slotName=='default' ? '<slot[\s\S]*>' : '<slot(.+name="('+slotName+')")?[\s\S]*>'
let regex = new RegExp(regexPattern, 'ig');
let results = [];
let result = null;
while ((result = regex.exec(template)) !== null) {
results.push(result);
}
if (!results.length) {
return false;
}
results.map((result, index) => {
if (slotName=='default' || (result[2]!=null && result[2]==slotName)) {
let start = result.index + result[0].length;
let end = result.input.indexOf('</slot>', start);
if (end == -1) {
return;
}
template = template.substring(0, start) + slots[slotName] + template.substring(end);
}
});
});
return template;
}
components/BaseForm.js
export default {
template: `
<div>
<div>
<slot name="header"><h1>{{ title }}</h1></slot>
</div>
<slot></slot>
<div>
<slot name="actions"></slot>
<button type="button" v-if="showSave" @click="submit()">Save</button>
<button type="button" v-if="showCancel" @click="$router.back()">Cancel</button>
</div>
</div>
`,
props: ['title', 'showSave', 'showCancel'],
data() {
return {
data: {},
errors: {}
}
},
methods: {
submit() {
// submit etc...
}
}
}
components/UserForm.js
import BaseForm from './BaseForm'
import extendTemplateSlots from '../helpers/extendTemplateSlots'
export default {
extends: BaseForm,
template: extendTemplateSlots(Form.template, {
'default': `
<div><input type="text" v-model="name" /></div>
<div><input type="text" v-model="email" /></div>
`
'actions': `
<button type="button">Do Something</button>
`
,
props: {
title: {
default: 'User Form'
}
}
}
Above looked like a functional component, so here is one that should work... well almost ..I would need to convert a string of html into vnodes, but i dont know how to do that.
components/UserForm.js
import BaseForm from './BaseForm'
const UserForm = {
extends: BaseForm,
// a custom option to override the existing slots
slots: {
default: `
<div><input type="text" v-model="name" /></div>
<div><input type="text" v-model="email" /></div>
`,
actions: `
<button type="button">Do Something</button>
`
},
props: {
title: {
default: 'User Form'
}
}
}
// this could be generic component...
export default {
render(createElement) {
return createElement(
UserForm,
{
scopedSlots: {
// how to create v-nodes from UserForm.slots.* strings ???
header: props => this.$slots.header ? this.$slots.header : null,
actions: props => this.$slots.actions ? this.$slots.actions : UserForm.slots.actions
}
},
this.$slots.default ? this.$slots.default : UserForm.slots.default
)
},
props: component.props
}
EDIT: Oops, the component for user-form is still needed, but we finally export a functional component to configure its slots. The reason is to be able to add more methods and data over the BaseForm. If it is not needed (like example up here), then a functional component would be enough.
I think Vue.compile()
is what would be needed above, but I have trouble using it.
https://vuejs.org/v2/api/#Vue-compile
Before you continue, I hope you realized that this issue is closed and therefore probably nobody will notice what your write here...
If you want to discuss something, forum.vuejs.org is probably a better place.
oh, you are right :)
Somebody has a reference to the new issue if there is one??
I need this to create a rehusable declarative tree of components for alternative view configuration / composition on global confs. ...
A pity this discussion ended here. Extending components which combines extension/inheritance would be great.
Hey guys, I made a loader that allows for this kind of functionality:
https://github.com/mrodal/vue-inheritance-loader
It currently only works with Single File Components with the template part defined in the tag though. You might want to try it out. I appreciate any feedback!
Before you continue, I hope you realized that this issue is closed and therefore probably nobody will notice what your write here...
If you want to discuss something, forum.vuejs.org is probably a better place.
I just read a lot of this thread, so just because issues are closed does not mean somebody else won't come through and try to understand what conversation took place so they understand the history of software development concerns.
Most helpful comment
@fnlctrl @purepear I think this is still an issue worth discussing. It sounds like you guys are talking about two slightly different things maybe a miscommunication.
The problem is when you extend a component, its extremely useful for all of the JS options, methods, data etc. But... for the template you either have to take the parents template as is or if you want to slightly tweak it you have to copy paste the entire thing, which is very difficult to maintain. You now have to maintain two templates anytime you change the base component.
You can add more template or options to the base component so that inherited component's can make adjustments to the template but then the base component has to have some pre-knowledge of the components that extend it and then they are tightly coupled which is worse.
Slots and scoped slots are tools for composition and not inheritance. Slots are great for a car has tire relationship but I think purepear was suggesting a tool for a car is a vehicle relationship.
As an example I have a giant custom form with tons of fields and I have an inherited from a component where 99% of the JS and 90% template is the same, but I've had to copy paste the entire template and duplicate a significant amount of code just for the 10% that is different. I'd be happy to contribute to improving the Vue.js project and look into this more.