A light-weight component implementation inspired by React and Ember. In contrast to the large frameworks it does much less things automagically in favour of synchronous rendering and a minimalistic life-cycle. It also provides up-tree communication and dependency injection.
Why synchronous rendering?
Synchronous rendering, while it may seem less performant, is necessary because substance must render the model, after it has changed before the next change is triggered by the user.
Asynchronous rendering as it exists in React means that the UI will eventually catch up to changes in the model. This is not acceptable in substance because substance plays with contenteditable and thus, cursor positions, etc are maintained in the browser's DOM. If we went the async way, the cursor in the DOM would be briefly inconsistent with the cursor in the model. In this brief window, changes triggered by the user would be impossible to apply.
Concepts:
-
props
are provided by a parent component. An initial set of properties is provided via constructor. After that, the parent component can callsetProps
orextendProps
to update these properties which triggers rerendering if the properties change. -
state
is a set of flags and values which are used to control how the component gets rendered given the current props. UsingsetState
the component can change its internal state, which leads to a rerendering if the state changes. Prefer usingextendState
rather thansetState
.Normally, a component maintains its own state. It isn't recommended that a parent pass in or update state. If you find the need for this, you should be looking at
props
.State would be useful in situations where the component itself controls some aspect of rendering. Eg. whether a dropdown is open or not could be a state within the dropdown component itself since no other component needs to know it.
-
A child component with a
ref
id will be reused on rerender. All others will be wiped and rerender from scratch. If you want to preserve a grand-child (or lower), then make sure that all anchestors have a ref id. After rendering the child will be accessible viathis.refs[ref]
. -
A component can send actions via
send
which are bubbled up through all parent components until one handles it. A component declares that it can handle an action by calling thehandleActions
method on itself in the constructor or thedidUpdate
lifecycle hook.
Lifecycle hooks
The RenderingEngine triggers a set of hooks for you to define behavior in various stages of the rendering cycle. The names are pretty self explanatory. If in doubt, please check out the method documentation below.
Define a component:
class HelloMessage extends Component {
render() {
return $$('div').append(
'Hello ',
this.props.name
)
}
}
And mount it to a DOM Element:
HelloMessage.mount({name: 'John'}, document.body)
Construcutor is only used internally.
Provides the context which is delivered to every child component. Override
if you want to provide your own child context. Child context is available to
all components rendered from within this component's render method as
this.context
.
Object | the child context |
class A extends Component {
...
getChildContext() {
// Optional, but useful to merge super's context
return Object.assign({}, super.getChildContext(), {foo: 'bar'})
}
render($$) {
return $$(B)
}
}
class B extends Component {
render($$) {
// this.context.foo is available here
}
}
Component
Override this within your component to provide the initial state for the component. This method is internally called by the RenderingEngine and the state defined here is made available to the Component#render method as this.state.
Object | the initial state |
Provides the parent of this component.
Component | the parent component or null if this component does not have a parent. |
Get the top-most Component. This the component mounted using ui/Component.mount
Component | The root component |
Short hand for using labelProvider API
render($$) {
let el = $$('div').addClass('sc-my-component')
el.append(this.getLabel('first-name'))
return el
}
Get a component class for the component name provided. Use this within the render method to render children nodes.
componentName | String | The component's registration name |
maybe | Boolean | if |
Class | The ComponentClass |
render($$) {
let el = $$('div').addClass('sc-my-component')
let caption = this.props.node.getCaption() // some method that returns a node
let CaptionClass = this.getComponent(caption.type)
el.append($$(CaptionClass, {node: caption}))
return el
}
Render the component.
ATTENTION: this does not create a DOM presentation but a virtual representation which is compiled into a DOM element later.
Every Component should override this method.
$$ | Function | method to create components |
VirtualNode | VirtualNode created using {@param $$} |
Mount a component to the DOM.
var app = Texture.mount({
configurator: configurator,
documentId: 'elife-15278'
}, document.body)
Determines if Component should be rendered again using ui/Component#rerender
after changing props. For comparisons, you can use this.props
and
newProps
.
The default implementation simply returns true.
newProps | Object | The props are being applied to this component. |
a boolean indicating whether rerender() should be run. |
Rerenders the component.
Call this to manually trigger a rerender.
Triggers didMount handlers recursively.
Gets called when using component.mount(el)
on an element being
in the DOM already. Typically this is done for a root component.
If this is not possible because you want to do things differently, make sure you call 'component.triggerDidMount()' on root components.
isMounted | an optional param for optimization, it's used mainly internally |
var frag = document.createDocumentFragment()
var comp = MyComponent.mount(frag)
...
$('body').append(frag)
comp.triggerDidMount()
Called when the element is inserted into the DOM. Typically, you can use this to set up subscriptions to changes in the document or in a node of your interest.
Remember to unsubscribe from all changes in the ui/Component#dispose method otherwise listeners you have attached may be called without a context.
class Foo extends Component {
didMount() {
this.props.node.on('label:changed', this.rerender, this)
}
dispose() {
// unless this is done, rerender could be called for a dead, disposed
// component instance.
this.props.node.off(this)
}
}
Make sure that you call component.mount(el)
using an element
which is already in the DOM.
var component = new MyComponent()
component.mount($('body')[0])
Hook which is called after state or props have been updated and the implied rerender is completed.
boolean | indicating if this component has been mounted |
Triggers dispose handlers recursively.
A hook which is called when the component is unmounted, i.e. removed from DOM, hence disposed. See ui/Component#didMount for example usage.
Remember to unsubscribe all change listeners here.
Send an action request to the parent component, bubbling up the component hierarchy until an action handler is found.
action | the name of the action | |
... | arbitrary number of arguments |
Boolean | true if the action was handled, false otherwise |
Define action handlers. Call this during construction/initialization of a component.
actionHandler | Object | An object where the keys define the handled actions and the values define the handler to be invoked. These handlers are automatically removed once the Component is disposed, so there is no need to unsubscribe these handlers in the {@link ui/Component#dispose} hook. |
class MyComponent extends Component {
constructor(...args) {
super(...args)
this.handleActions({
'openPrompt': this.openPrompt,
'closePrompt': this.closePrompt
})
}
}
Define an action handler. Call this during construction/initialization of a component.
action | String | name |
a | Functon | function of this component. |
Get the current component state
Object | the current state |
Sets the state of this component, potentially leading to a rerender. It is better practice to use ui/Component#extendState. That way, the code which updates state only updates part relevant to it.
Eg. If you have a Component that has a dropdown open state flag and another enabled/disabled state flag for a node in the dropdown, you want to isolate the pieces of your code making the two changes. The part of your code opening and closing the dropdown should not also automatically change or remove the enabled flag.
Note: Usually this is used by the component itself.
newState | object | an object with a partial update. |
This is similar to setState()
but extends the existing state instead of
replacing it.
newState | object | an object with a partial update. |
Called before state is changed.
Get the current properties
Object | the current state |
Sets the properties of this component, potentially leading to a rerender.
an | object | object with properties |
Extends the properties of the component, without necessarily leading to a rerender.
an | object | object with properties |
Hook which is called before properties are updated. Use this to dispose objects which will be replaced when properties change.
For example you can use this to derive state from props.
newProps | object |