Description
Purpose
A feature sorely missed since the inception of CanJS's implementation of components is the ability to extend them. I'm not really sure why this never happened, but I think it's important that it does.
Allowing for the extensibility of components promotes the notion that people should be working in a component-heavy paradigm. Small, easily digestible, components should be encouraged that work as building blocks for bigger ones. A good example of this would be a simple form element:
Example: form-element
component
A simple form element component might look like the following:
// src/components/form-element.js
import Component from 'can-component'
export default Component.extend({
tag: 'form-element',
viewModel: {
key: 'string',
value: 'string',
},
template: `
<label>
{{key}}
<input {($value)}="value" />
</label>
`
});
What if we want to create another form-element, but one that would show some sort of validation error? Currently we would have one of two options:
- Add additional functionality to the
form-element
component, thereby also adding to the complexity of the component - Duplicate this component, but with validation capabilities, and error rendering, but if our basic
form-element
template changes, we have to change this too.
Neither of these are ideal; however, if we could extend form-element
, this could be a snap. The trick is figuring out how to handle extending each of the complicated properties: viewModel
template
, events
, and helpers
.
Implementation Ideas
events
Given that events
is the source of a can-component
's underlying control, this should work in the same way extending any can-control
and subsequently any can-construct
works, by initializing a new instance, and allowing it to inherit the parent control's prototype properties. If can-construct-super
is utilized, the parent's functions should be accessible via this._super
helpers
Given the non-complex nature of these, the parent's helper functions should be made available via a shallow copy.
viewModel
Depending on whether a constructor function or an object instance is provided for the viewModel
different things should happen.
Constructor function
If a constructor function is provided, this should override the parent component's view model.
Object instance
If an object instance is provided, it should attempt to extend the parent view model's constructor function with it's new properties.
template
While it might be implied that the rest of a component's properties could and should be extended, templates maybe moreso than the rest need to be extensible. The only way we can make truly extensible components is by allowing a child component access and use of it's parent's template. The way we would make that happen would be through a special tag: <super />
<super />
tag
This new tag, similar to <content/>
, has one very explicit purpose: to take the parent component's template and render it at whatever level of scope it is rendered at in the template. It should not create any additional components whatsoever, and should be treated, more or less, as a suped-up subtemplate.
Basic Example
Using our first example of a form element where we start out with the following template:
<label>
{{key}}
<input {($value)}="value" />
</label>
Except now, we want to make a component that shows some error validation, perhaps with an unordered list. Because we are inheriting the parent component, we can do the following:
<ul>
{{#each errors}}
<li>{{.}}</li>
{{/each}}
</ul>
<super />
This would ultimately render out the following:
<ul>
<li>Email can't be blank</li>
</ul>
<label>
Email
<input />
</label>
Nested Example
Sometimes we want to inherit a component that might utilize a <content/>
tag, we might want to use that DOM and shove our child component's DOM inside of it. Rather than treating <super/>
as a self-closed tag, we should be able to achieve this by nested DOM elements inside <super></super>
.
Let's take an example template for a person-greeting
component. It might look like the following:
<div class="person">
<h1>Hello, my name is {{name}}</h1>
<content />
</div>
What if we wanted to take our person-greeting
component and make it for businesses? We might want to include some information about a business or a company or a title. If we used a nested <super>
tag, we could achieve this:
<super>
<h2>I am a {{position}} at {{business}}
<content />
</super>
Which would ultimately render out something like the following to our page:
<div class="person">
<h1>Hello, my name is Josh</h1>
<h2>I am a JavaScript developer at Bitovi</h2>
</div>
Adding the <content />
tag in our child template allows the new component to be utilized in the same manner as the parent one and provide light DOM to be rendered inside.
Summary
By adding these improvements, we promote the idea that we should be building small, easy-to-digest components. We also make it much more easy to simplify our codebases as well as making them less error prone by making DRYer. Using this feature should be very simple; we can easily create the originally desired error-form-element
described at the beginning in the following manner:
// src/components/error-form-element.js
import FormElement from './form-element.js';
export default FormElement.extend({
tag: 'error-form-element',
viewMode: {
errors: []
},
template: `
<ul>
{{#each errors}}
<li>{{.}}</li>
{{/each}}
</ul>
<super />
`
});