Skip to content

Proposal: Flesh out .extend ability for components #76

Open
@imjoshdean

Description

@imjoshdean

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:

  1. Add additional functionality to the form-element component, thereby also adding to the complexity of the component
  2. 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 />
  `
});

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions