DomView§
The DomView
is a specialization of View
which comes equipped to deal with
HTML fragment views. It provides tools for drawing fragments, reattaching to
already-drawn fragments, wiring up client events, and fetching the final HTML
markup, among other things.
In general, you will want to use the build
class method to define DomView
classes, though you may also derive from it if you prefer.
A detailed account of DomView
and its constituent tools can be found in its
own chapter.
Building a DomView§
To define your own DomView
, you will at least wish to call DomView.build
,
or directly extend the result of @build
in order to implement _wireEvents
.
@build§
DomView.build(fragment: $Node, template: Template): @DomView
DomView.build(viewModelClass: @Model, fragment: $Node, template: Template): @DomView
The @build
class method is the primary interface for creating DomView
class
definitions. It takes a fragment
target HTML fragment, wrapped in a jQuery-like
interface, and a template
, which defines how data should be bound to that fragment.
The optional leading parameter viewModelClass
will set the @viewModelClass
property on the resulting DomView class.
In general, the results of calling find(…).operation(…)
(where operation
is any mutator) or template(…)
may be passed as the template
.
The full signature for Template
s may be found in the chapter section about
them.
More information about find
and template
follows after this sample.
const SampleView = DomView.build(
$('<div><div class="name"/><p/></div>'),
template(
find('.name').text(from('name')),
find('p').text(from('description'))
)
);
const model = new Model({
name: 'DomView',
description: `
The DomView is a specialization of View which comes equipped to deal with
HTML fragment views. It provides tools for drawing fragments, reattaching to
already-drawn fragments, wiring up client events, and fetching the final HTML
markup, among other things.`
});
return new SampleView(model, { app });
The DomView is a specialization of View which comes equipped to deal with HTML fragment views. It provides tools for drawing fragments, reattaching to already-drawn fragments, wiring up client events, and fetching the final HTML markup, among other things.
λfind§
find(selector: String).{mutator}(…): Template
Where
{mutator}
is one of the mutators packaged with thefind
. By default, this is the set documented below, but you may generate your own set usingfind.build
.
A chaining function. First, it takes a selector
which references which node(s)
within the view fragment are the targets for this databinding. Only nodes present
when the DomView
class is defined will be selected; manually modifying the DOM
structure will not add to the selection, and will likely disrupt existing selections.
Once called with the selector
, a plain object is returned which contains the
various mutator functions as described in the inset note just above. Calling one
of these will result in a Template
which may be passed to @build
[#@build] or
template(…)
.
The mutator section of find
may chain, as you see in this sample:
const Link = DomView.build(
$('<a/>'),
find('a')
.attr('href', from('url'))
.text(from('name'))
);
const resource = new Model({ name: 'API Documentation', url: '/api' });
return new Link(resource, { app });
λfindλbuild§
find.build({ String : Mutator }): find
Given a mapping of names to Mutator
s, this method returns a new find
function
which works exactly like the normal one, except with the mutators you have provided.
Mutator
s are documented later on this page, and in their own theory
chapter section.
The default mutators are not included by default, so if you wish to interleave your custom mutators with the default, you'll need to do something like this:
const customfind = find.build(Object.assign({
bolded: (data) => (dom, point, immediate = true) =>
data.all.point(point).react(flag => {
dom.css('font-weight', (flag === true) ? 'bold' : 'normal')
})
}, mutators));
const SampleView = DomView.build(
$('<div/>'),
customfind('div')
.text(from('name'))
.bolded(from('important'))
);
const model = new Model({ name: 'Some Name', important: true });
return new SampleView(model, { app });
λtemplate§
template(…Template): Template
The template
function lets you group multiple Template
s together into a single
Template
, since DomView@build
only takes a singular Template
.
Essentially, find(…).{mutator}(…)
expressions and other template()
results
may all be given as parameters to template()
.
More information about how template
works and its various uses may be found in
its own theory chapter section.
const WithTitle = template(
find('h1 a')
.text(from('title'))
.attr('href', from('url'))
);
const ArticleView = DomView.build(
$(`<div class="article">
<h1><a/></h1>
<p/>
</div>`),
template(
WithTitle,
find('p').text(from('body'))
)
);
const article = new Model({
title: 'Why Janus is Cool',
url: '/',
body: 'Because functional composition is pretty neat.'
});
return new ArticleView(article, { app });
Why Janus is Cool
Because functional composition is pretty neat.
Mutators§
A Mutator
is any function that follows the given signature:
(…\*) -> dom: $Node, point: (types.from -> Varying), immediate: Boolean? -> Observation
By nature of returning an Observation
, Mutators
are impure functions. Usually,
they are used in conjunction with template
, find
,
and DomView@build
to define DomView
classes. In these cases, the
impurity and related resource management are handled by the DomView
.
You may implement your own Mutator
s; a sample may be found under the documentation
for find.build
above. If you do so, keep in mind that dom
may refer to more than one node.
Otherwise, documentation for the standard Janus mutators follows. Below, all the
standard mutators are shown in usage with find
, but they may also be independently
found under the mutators
top-level Janus export.
.attr§
find(…).attr(prop: String, data: From[*]): Template
The attr
mutator sets the given prop
attribute on the DOM node(s) to the given
data
value, through whatever jQuery-like framework you are using. If you are
using jQuery, its documentation may be found here.
Whenever the data
value changes, it will be reapplied.
As with jQuery
#attr
, you may instead wish to use.prop
.
const SampleView = DomView.build(
$('<a>look, a link!</a>'),
find('a')
.attr('href', from('url'))
.attr('title', from('name'))
);
const model = new Model({ name: 'cool link', url: '/' });
return new SampleView(model, { app });
.prop§
find(…).prop(prop: String, data: From[*]): Template
The prop
mutator sets the given prop
attribute on the DOM node(s) to the given
data
value, through whatever jQuery-like framework you are using. If you are
using jQuery, its documentation may be found here.
Whenever the data
value changes, it will be reapplied.
const SampleView = DomView.build(
$('<input type="text"/>'),
find('input').prop('value', from('name'))
);
const model = new Model({ name: 'cool text' });
return new SampleView(model, { app });
.classed§
find(…).classed(className: String, data: From[Boolean]): Template
The classed
mutator ensures that if data
is === true
, then the DOM node(s)
will have the HTML class className
applied. If data
is anything other than
true
, it will not have className
.
const SampleView = DomView.build(
$('<div>Some div</div>'),
find('div').classed('isError', from('isError'))
);
const model = new Model({ isError: true });
return new SampleView(model, { app });
.classGroup§
find(…).classGroup(prefix: String, data: From[String]): Template
The classGroup
mutator manages all HTML classes on the target DOM node(s) that
begin with the prefix
. At all times, it ensures the only class that exists on
the target(s) that begins with the prefix
ends with the current value of the
provided data
.
const SampleView = DomView.build(
$('<div>Some div</div>'),
find('div').classGroup('divColor-', from('color'))
);
const model = new Model({ color: 'red' }); // try blue or green
return new SampleView(model, { app });
.css§
find(…).css(property: String, data: From[String]): Template
The css
mutator sets an inline style on the target DOM node(s). It will set the
CSS property property
to the present value of the given data
at all times.
const SampleView = DomView.build(
$('<div>Some div</div>'),
find('div').css('font-size', from('size'))
);
const model = new Model({ size: '25px' });
return new SampleView(model, { app });
.text§
find(…).text(From[String]): Template
The text
mutator sets the text of the target DOM node(s) to the String
content
provided at all times.
Typically, and certainly when Janus is used with jQuery, this method will sanitize
all HTML input as it is being set. This includes HTML entities. If you wish to
set raw HTML, see the html
mutator just below.
const SampleView = DomView.build(
$('<div/>'),
find('div').text(from('name'))
);
const model = new Model({ name: '“test name”' });
return new SampleView(model, { app });
.html§
find(…).html(From[String]): Template
The html
mutator sets the innerHTML
of the target DOM node(s) to the String
content provided at all times.
Be warned that this mutator does not sanitize user input for XSS attacks!
const SampleView = DomView.build(
$('<div/>'),
find('div').html(from('name'))
);
const model = new Model({
name: 'this is <strong>some <em>“html”!</em></strong>'
});
return new SampleView(model, { app });
.render§
find(…).render(From[*]): Template
The render
mutator is explained thoroughly in its own theory chapter section,
but in general it takes any value and attempts, through the application App.views
library, to render a child view under the given target DOM node. If the value changes,
a new View is generated to replace the old one.
Unlike the other mutators, render
sub-chains for additional configuration:
.render(…).context(cxt: String|From[String])
will use the givencxt
as the Library context when requesting a View class from theapp.views
Library
..render(…).criteria(obj: Object|From[Object])
will use the givencriteria
as the Library criteria (one of which may becontext
) when requesting a View class from theapp.views
Library (see the above links for more detail)..render(…).options(opts: Object|From[Object])
will provide the givenopts
to the instantiated childView
constructor as the secondoptions
argument. Ifopts
changes, the View will be recycled for a new one so that the new constructor argument may be applied.
These subchains may chain together, and you can seamlessly chain back up to the
find(…)
selection:
const SampleParentView = DomView.build(
$('<div/>'),
find('div')
.render(from('subobject'))
.context('summary')
.criteria(from('subobject').get('type').map(type => ({ type })))
.classed('hasChild', from('subobject').map(x => (x != null)))
);
class ChildModel extends Model {}
const BoldChildView = DomView.build(
$('<div class="boldChild"/>'),
find('div').text(from('name'))
);
const ItalicChildView = DomView.build(
$('<div class="italicChild"/>'),
find('div').text(from('name'))
);
const app = new App();
app.views.register(ChildModel, BoldChildView, { context: 'summary', type: 'bold' });
app.views.register(ChildModel, ItalicChildView, { context: 'summary', type: 'italic' });
const model = new Model({
subobject: new ChildModel({ name: 'a child', type: 'bold' })
});
return new SampleParentView(model, { app });
.on§
find(…).on(event: String, handler: Handler): Template
find(…).on(event: String, selector: String, handler: Handler): Template
The on
mutator attaches a client-side event to the target DOM node(s). When
the DomView
it is attached to performs a #wireEvents
action,
all the event handlers created with on
are activated and wired onto their respective
targets.
As a result of this special "time of activation," as it were, the
on
mutator is a little bit special and only works when used in conjunction withDomView
. Invoking it directly, for example, will not yield the expected result.
The event
name is passed directly to the jQuery-like framework you are using,
and will have whatever properties and features that framework provides. If provided,
the selector
filter is similarly passed along, and will filter the event according
to its target.
The Handler
given in the signatures above are an augmented version of the usual
event -> void
function:
Handler: (event: Event, subject: \*, view: DomView, artifact: $Node) -> void
Where event
is the Event
object you would normally receive, subject
is the
View subject, view
is a reference to the containing DomView
instance, and
artifact
is that DomView
's artifact
.
const SampleView = DomView.build(
$('<a/>').attr('href', '#'),
find('a')
.text(from('count').map(x => `clicked ${x} times`))
.on('click', (event, subject) => {
event.preventDefault();
subject.set('count', subject.get_('count') + 1);
})
);
const model = new Model({ count: 0 });
const view = new SampleView(model, { app });
view.wireEvents(); // try commenting this line out.
return view;
Creation§
@constructor§
new DomView(subject: *, options: Object): DomViewinherited
Inherited from View@constructor
.
Rendering and Events§
#artifact§
.artifact(): $Node
Returns a copy of the template fragment of this DomView
, databound against its
subject
. Only one copy will ever be returned; subsequent calls just return the
same fragment again.
Calling this method alone will not wire client-side events. To do that, call
#wireEvents
as well.
As with the parent View#artifact
, this method calls out to
#_render
to actually perform the rendering. Unlike the View#artifact
,
DomView
has a (very short) implementation that you would not likely wish to
override.
See also:
#attach
, just below.
const SampleView = DomView.build(
$('<p>hello, this is a <em>test fragment</em></p>'),
template()
);
const model = new Model();
const view = new SampleView(model, { app });
return view.artifact();
hello, this is a test fragment
#pointer§
.pointer(): types.from → Varying[*]inherited
Inherited from View#pointer
.
#attach§
.attach(node: $Node): $Nodeimpure
Like #artifact
, #attach
walks through a fragment and sets up
databinding against it, forever remembering that fragment as its artifact.
Unlike #artifact
, #attach
takes an existing node
which was previously rendered
by the same DomView
class, and binds against it, making the assumption that it
is already fully up-to-date with its given subject
data, and therefore not mutating
any data into the fragment until the next time the data changes. (In other words,
Varying#react
is called with immediate
set to false
.)
For more information about this capability, please see this chapter section. And if you implement your own custom
View
orDomView
that performs its own rendering, take care to implement#_attach
in addition to#_render
if you intend to use#attach
.
#attach
allows you to render a fragment in one execution context (say, on your
webserver) and pick the fragment (and all its subfragments) back up at near-zero
cost and without rerendering or recycling the entire DOM tree.
const SampleView = DomView.build(
$(`<div><h1/><p/></div>`),
template(
find('h1').text(from('name')),
find('p').text(from('body'))));
const model = new Model({ name: 'DomView', body: 'is pretty cool' });
const firstView = new SampleView(model, { app });
const markup = firstView.markup(); // markup is a string!
const secondView = new SampleView(model, { app });
secondView.attach($(markup));
model.set('body', 'is really cool');
return secondView;
DomView
is really cool
#wireEvents§
.wireEvents(): voidimpure
When called, #wireEvents
sets up client-side events for the DomView
. If #wireEvents
has already been called, nothing happens. Otherwise, the following five things
occur:
#artifact
is called to ensure a target fragment to actually attach events to.- For your future convenience, if your jQuery-like wrapper supports
#data
, then theDomView
instance will be attached as data on its root DOM node(s) under the keyview
. - The user-overridable method
_wireEvents
is called. - Any
on
mutators present on theDomView
are activated. #wireEvents
is called on all child views, and any future child viewsrender
ed into thisDomView
subtree will have#wireEvents
automatically called.
Because client-side events are not applicable during server-side rendering, the
goal behind the #wireEvents
system is to avoid this event-wiring overhead when
processing pages on the server. If your application is set up as a tree with a
single application root node, you can just call .wireEvents()
on that root view
upon startup, and never worry about event wiring again.
Please see the sample listing for the on
mutator to see that technique
in use. Here, we demonstrate the use of #_wireEvents
instead. As implied by the
ordered list above, the two may be used in conjuction.
class SampleView extends DomView.build(
$(`<div class="collapsableSection"><h1/><p/></div>`),
template(
find('div').classed('collapsed', from('collapsed')),
find('h1').text(from('name')),
find('p').text(from('body')))
) {
_wireEvents() {
const artifact = this.artifact();
artifact.on('click', () => {
this.subject.set('collapsed', !this.subject.get_('collapsed'));
});
}
}
const model = new Model({ name: 'DomView', body: 'is pretty cool' });
const view = new SampleView(model, { app });
view.wireEvents();
return view;
DomView
is pretty cool
Navigation§
All View Navigation methods are inherited from View
.
#parent§
.parent(selector: Selector?): Varying[View?]inherited
Inherited from View#parent
.
#parent_§
.parent_(selector: Selector?): View?inherited
Inherited from View#parent_
.
#closest§
.closest(selector: Selector?): Varying[View?]inherited
Inherited from View#closest
.
#closest_§
.closest_(selector: Selector?): View?inherited
Inherited from View#closest_
.
#into§
.into(selector: Selector?): Varying[View?]inherited
Inherited from View#into
.
#into_§
.into_(selector: Selector?): View?inherited
Inherited from View#into_
.
#intoAll§
.intoAll(selector: Selector?): List[View]inherited
Inherited from View#intoAll
.
#intoAll_§
.intoAll_(selector: Selector?): Array[View]inherited
Inherited from View#intoAll_
.
Extending DomView (Overrides)§
Generally, DomView@build
should cover all but the most extreme needs.
Most overrides of DomView
will only involve providing a #_wireEvents
method.
However, in rendering complex visualizations and other performance-heavy situations,
it may be helpful to augment the default _render
method (in which case it is
advisable to also pay attention to _attach
. Even in these cases, the best practice
is to still use DomView@build
for everything you can.
#_wireEvents§
._wireEvents(): void
Please see #wireEvents
.
#_render§
._render(): $Node
This method is called by DomView
when it is asked to furnish a view artifact.
Some additional details may be found at #artifact
, and in the theory
chapter section about implementing
custom rendering.
It is relevant to note that since
DomView
derives fromBase
, any databinding operations may be done via#reactTo
,#destroyWith
, or implementing#_destroy
.
Sample code can be found in the relevant theory chapter linked above.
#_attach§
.attach(): void
This method is called by DomView
when it is asked to attach to an existing DOM
fragment via #attach
. You only need to override it if you have also
overridden #_render
.
More information and sample code may be found in the further reading chapter
about attach()
ing responsibly.
@viewModelClass§
DomView.viewModelClass: @Model?inherited
Inherited from View.viewModelClass
and works the same
way, but usually you'll want to define this using @build
.