View§
The View class is an abstract base class which defines the general lifecycle
of a Janus view, and the interface through which this lifecycle is managed. For
most web applications, the DomView class will be more immediately
useful.
However, for applications that fall outside the HTML-specific purview of DomView
(voice-driven APIs, for example), it may make more sense to use or extend View
rather than DomView.
Creation§
@constructor§
new View(subject: *, options: Object): View
All Views have some particular, singular subject which they represent. This
subject is always taken as the first argument to the View constructor. The subject
is always saved off as an instance property, at this.subject.
If the View class has a viewModelClass class property declared, the behavior
is slightly different.
The
optionshash is primarily for your own use. Janus itself does not make use of it, although many components of the Janus Standard Library do.
If an _initialize method exists on the View class, it will be
called as the final step of instantiation.
return new View(new Model());Rendering and Context§
#artifact§
.artifact(): *impure
The #artifact method is the primary interface of a View; it retrieves the
actual view artifact itself, whether that be an HTML fragment, or an XML payload,
a native system view component, or anything else.
Behind the scenes, #artifact calls #_render to actually generate
this artifact.
Its only semantic is that because each View is meant to represent a single view
instance of a single subject, #artifact will always yield the same reference
no matter how many times it is called—the result of #_render is cached
and returned directly after the first call.
class SampleView extends View {
_render() {
const node = $('<div/>');
this.reactTo(this.subject.get('name'), name => {
node.text(name);
});
return node;
}
}
const model = new Model({ name: 'Alice' });
const view = new SampleView(model);
return [
view.artifact(),
view.artifact() === view.artifact()
];#pointer§
.pointer(): types.from → Varying[*]
The #pointer method is primarily for use by the View itself. It provides from-binding
expressions with context based on the View and its subject. Most of the cases use
the subject as the context, while self will point at the View instance itself.
const binding = from('name').map(name => `hello, ${name}!`);
const model = new Model({ name: 'Alice' });
const view = new View(model);
return binding.all.point(view.pointer());Navigation§
View Navigation provides a way to leverage the known tree and hierarchy of drawn
Views. Just as you can get the .children or .parent of a DOM node, and with
some knowledge of the structure of your own application make use of those relationships,
you can get the children or parents of a .rendered Janus View in a principled
way to do contextual work on them.
And just as tools like jQuery add selector semantics and shortcut tools like .closest
on top of this tree navigation, Janus allows selection of a sort.
Most of these methods take a selector parameter. If a value is given, Janus will
interpret that value as a selector, using semantics as follows, in order:
- If
selectoris aStringorNumber, and the navigation operation moves toward the leaves of the tree, then that value will be passed to.get_on the.subjectof the View. If a non-nullish value is returned, it will be used as the selector for the following checks. Otherwise,selectoris used as-is. - If
selectoris `undefined', there is no selection and everything is a valid match. - If
selectoris===to the navigation candidate View, it will match. - If
selectoris===to the.subjectof the navigation candidate View, it will match. - If
selectoris aninstanceofthe navigation candidate View, it will match. - If
selectoris aninstanceofthe.subjectof the navigation candidate View, it will match.
More explanation about View Navigation can be found here.
#parent§
.parent(selector: Selector?): Varying[View?]
Returns the parent of the View. If a selector is given, null
will be returned unless the parent matches the selector.
Because the parent of a .rendered View never changes, the contained value of
the returned Varying will never change. Thus, it usually makes more sense to
call #parent_.
const parents = new List();
const StackView = DomView.build(
$('<div><button/><span/></div>'),
template(
find('button')
.text(from('name'))
.on('click', (event, subject, view) => { parents.add(view.parent()); }),
find('span').render(from('child'))
));
const stack = new Model({ name: 'one', child: new Model({ name: 'two' }) });
const app = new App();
app.views.register(Model, StackView);
return [ app.view(stack), inspect.panel(parents) ];-1 Ø
#parent_§
.parent_(selector: Selector?): View?
Like #parent, but returns the current value immediately. Because the
parent of a View never changes, it is probably more sensible to use this method
than #parent.
const parents = new List();
const StackView = DomView.build(
$('<div><button/><span/></div>'),
template(
find('button')
.text(from('name'))
.on('click', (event, subject, view) => { parents.add(view.parent_()); }),
find('span').render(from('child'))
));
const stack = new Model({ name: 'one', child: new Model({ name: 'two' }) });
const app = new App();
app.views.register(Model, StackView);
return [ app.view(stack), inspect.panel(parents) ];-1 Ø
#closest§
.closest(selector: Selector?): Varying[View?]
Iterates up the stack of parents of the View, and returns the first that matches
the given selector. If no selector is given, this method is
equivalent to calling #parent with no selector.
Because the parents of a .rendered View never changes, the contained value of
the returned Varying will never change. Thus, it usually makes more sense to
call #closest_.
const closests = new List();
const StackView = DomView.build(
$('<div><button/><span/></div>'),
template(
find('button')
.text(from('name'))
.on('click', (event, subject, view) => { closests.add(view.closest(Root)); }),
find('span').render(from('child'))
));
class Root extends Model {}
const stack = new Root({ name: 'one',
child: new Model({ name: 'two',
child: new Model({ name: 'three' }) }) });
const app = new App();
app.views.register(Model, StackView);
return [ app.view(stack), inspect.panel(closests) ];-1 Ø
#closest_§
.closest_(selector: Selector?): View?
Like #closest, but returns the current value immediately. Because
the parents of a View never change, it is probably more sensible to use this method
than #closest.
const closests = new List();
const StackView = DomView.build(
$('<div><button/><span/></div>'),
template(
find('button')
.text(from('name'))
.on('click', (event, subject, view) => { closests.add(view.closest_(Root)); }),
find('span').render(from('child'))
));
class Root extends Model {}
const stack = new Root({ name: 'one',
child: new Model({ name: 'two',
child: new Model({ name: 'three' }) }) });
const app = new App();
app.views.register(Model, StackView);
return [ app.view(stack), inspect.panel(closests) ];-1 Ø
#into§
.into(selector: Selector?): Varying[View?]
Returns the first immediate child of the View that matches the given selector.
The order that the .render children are checked is the order in which they are
declared in the template, but it is probably inadvisable to rely on this ordering,
as it can be difficult to reason about and will result in brittleness and sensitivity
in your code.
For this reason, it is recommended to only use #into if you are sure only one
child View will result. Usually, this means there is only one .rendered child,
or the given selector will only match one child.
Because #into navigates toward the leaves of the tree, the selector may be
a String or Number data key reference as described above.
const children = new List();
const StackView = DomView.build(
$('<div><button/><span/></div>'),
template(
find('button')
.text(from('name'))
.on('click', (event, subject, view) => { children.add(view.into()); }),
find('span').render(from('child'))
));
const stack = new Model({ name: 'one', child: new Model({ name: 'two' }) });
const app = new App();
app.views.register(Model, StackView);
return [ app.view(stack), inspect.panel(children) ];-1 Ø
#into_§
.into_(selector: Selector?): View?
Like #into, but returns the current result immediately.
const children = new List();
const StackView = DomView.build(
$('<div><button/><span/></div>'),
template(
find('button')
.text(from('name'))
.on('click', (event, subject, view) => { children.add(view.into_()); }),
find('span').render(from('child'))
));
const stack = new Model({ name: 'one', child: new Model({ name: 'two' }) });
const app = new App();
app.views.register(Model, StackView);
return [ app.view(stack), inspect.panel(children) ];-1 Ø
#intoAll§
.intoAll(selector: Selector): List[View]
Returns all immediate children of the View that match the given selector.
The order that the matching .render children appear in the returned List is the
order in which they are declared in the template, but it is probably inadvisable
to rely on this ordering, as it can be difficult to reason about and will result
in brittleness and sensitivity in your code.
A notable exception is when using the Janus Standard Library ListView, which
will always respect the order of the source List.
Because #intoAll navigates toward the leaves of the tree, the selector may be
a String or Number data key reference as described above.
class Special extends Model {}
const children = new List();
const TreeView = DomView.build(
$('<div><button class="main"/><button class="x">x</button><span/></div>'),
template(
find('button.main')
.text(from('name'))
.on('click', (event, subject, view) => {
const listView = view.into_(List);
if (listView == null) children.add('no children');
else children.add(listView.intoAll(Special));
}),
find('button.x').on('click', (event, subject) => { subject.destroy(); }),
find('span').render(from('children'))
));
const tree = new Model({ name: 'root', children: new List([
new Special({ name: '1', children: new List([
new Model({ name: '1a' }),
new Special({ name: '1b' }),
new Special({ name: '1c' })
]) }),
new Model({ name: '2', children: new List([
new Special({ name: '2a' })
]) })
]) });
const app = new App();
app.views.register(Model, TreeView);
stdlib.view($).registerWith(app.views);
return [ app.view(tree), inspect.panel(children) ];-1 Ø
#intoAll_§
.intoAll_(selector: Selector): Array[View]
Like #intoAll, but returns the current result immediately, in the
form of an Array.
class Special extends Model {}
const children = new List();
const TreeView = DomView.build(
$('<div><button class="main"/><button class="x">x</button><span/></div>'),
template(
find('button.main')
.text(from('name'))
.on('click', (event, subject, view) => {
const listView = view.into_(List);
if (listView == null) children.add('no children');
else children.add([ listView.intoAll_(Special) ]);
}),
find('button.x').on('click', (event, subject) => { subject.destroy(); }),
find('span').render(from('children'))
));
const tree = new Model({ name: 'root', children: new List([
new Special({ name: '1', children: new List([
new Model({ name: '1a' }),
new Special({ name: '1b' }),
new Special({ name: '1c' })
]) }),
new Model({ name: '2', children: new List([
new Special({ name: '2a' })
]) })
]) });
const app = new App();
app.views.register(Model, TreeView);
stdlib.view($).registerWith(app.views);
return [ app.view(tree), inspect.panel(children) ];-1 Ø
Extending View (Overrides)§
As noted at the top of this page, most HTML-based web applications will want to
use DomView rather than View, and indeed will want to use DomView.build rather
than directly extend DomView.
However, in all other cases, extending View is the likely path. At minimum,
one will wish to override the _render method, as the default implementation is
empty.
@viewModelClass§
View.viewModelClass: @Model?
The optional viewModelClass class property changes the construction process for
the View. Instead of directly assigning the subject to itself, it will construct
a new instance of viewModelClass, passing it the following arguments:
new viewModelClass({ view: View, subject: \*, options: Object }, { app: App })Where view is the view itself, and subject and options are the parameters
originally passed to the view constructor. app is the App instance that usually
exists at options.app.
The resulting viewModelClass instance is scheduled to be destroyed whenever the
parent View is destroyed.
class SampleViewModel extends Model.build(
bind('greeting', from('subject').get('name').map(name => `Hello, ${name}!`))
) {};
class SampleView extends View {
static get viewModelClass() { return SampleViewModel; }
}
const model = new Model({ name: 'Jane' });
const view = new SampleView(model);
return view.subject;#_render§
._render(): *impure
This is the method that is called by #artifact to actually produce
the view artifact for this view instance. In general, this method should both produce
the artifact object itself, as well as kick off whatever databinding and reaction
is required to keep that artifact up-to-date as the subject data changes.
Keep in mind that
Viewderives fromBase, which gives it access to resource management convenience methods like#reactTo,#destroyWith, and others.
As long as something is returned by _render, it will only ever be called once,
as #artifact caches/memoizes the response given by _render and returns it directly
thereafter.
class SampleView extends View {
_render() {
const node = $('<div/>');
this.reactTo(this.subject.get('name'), name => {
node.text(name);
});
return node;
}
}
const model = new Model({ name: 'Alice' });
const view = new SampleView(model);
return view.artifact(); // calls _render