Skip to content

Commit 382f66a

Browse files
authored
fixes #282, needs docs, but improves docs on other parts of can-component (#283)
1 parent cb8023a commit 382f66a

File tree

4 files changed

+200
-73
lines changed

4 files changed

+200
-73
lines changed

can-component.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,7 @@ var Component = Construct.extend(
455455
this.viewModel = viewModel;
456456

457457
el[viewModelSymbol] = viewModel;
458+
el.viewModel = viewModel;
458459
domData.set.call(el, "preventDataBindings", true);
459460

460461
// ## Helpers
@@ -558,6 +559,8 @@ var Component = Construct.extend(
558559
}
559560
if(disconnectedCallback) {
560561
disconnectedCallback(el);
562+
} else if(typeof viewModel.stopListening === "function"){
563+
viewModel.stopListening();
561564
}
562565
}, componentTagData.parentNodeList || true, false);
563566
nodeList.expression = "<" + this.tag + ">";

docs/ViewModel.md

Lines changed: 65 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -5,49 +5,65 @@
55

66
Provides or describes a constructor function that provides values and methods
77
to the component’s [can-component::view view]. The constructor function
8-
is initialized with values specified by the component element’s [can-stache-bindings data bindings].
8+
is initialized with values specified by the component element’s
9+
[can-stache-bindings data bindings].
910

1011
@type {Object} An object that will be passed to [can-define/map/map.extend DefineMap.extend] and
1112
used to create a new observable instance accessible by the component’s [can-component::view].
1213

13-
For example, every time `<my-tag>` is found, a new [can-define/map/map DefineMap] instance
14-
will be created:
14+
For example, every time `<my-tag>` is found, a new [can-define/map/map DefineMap] instance
15+
will be created:
1516

16-
```js
17-
import Component from "can-component";
17+
```html
18+
<my-tag></my-tag>
1819

19-
Component.extend( {
20+
<script type="module">
21+
import {Component} from "can";
22+
23+
Component.extend({
2024
tag: "my-tag",
2125
ViewModel: {
22-
message: "string"
26+
message: {default: "Hello there!"}
2327
},
24-
view: "<h1>{{message}}</h1>"
25-
} );
26-
```
28+
view: `<h1>{{message}}</h1>`
29+
});
2730
28-
@type {function} A constructor function (usually defined by [can-define/map/map.extend DefineMap.extend] or
29-
[can-map Map.extend]) that will be used to create a new observable instance accessible by
30-
the component’s [can-component::view].
31+
var viewModelInstance = document.querySelector("my-tag").viewModel;
32+
console.log(viewModelInstance) //-> MyTagVM{message: "Hello there!"}
33+
</script>
34+
```
35+
@codepen
3136

32-
For example, every time `<my-tag>` is found, a new instance of `MyTagViewModel` will
33-
be created:
37+
@type {function} A constructor function (usually defined by [can-define/map/map.extend DefineMap.extend],
38+
or [can-observe.Object observe.Object]) that will be used to create a new observable instance accessible by the component’s [can-component::view].
3439

35-
```js
36-
import Component from "can-component";
37-
import DefineMap from "can-define/map/map";
40+
For example, every time `<my-tag>` is found, a new instance of `MyTagViewModel` will
41+
be created:
3842

39-
const MyTagViewModel = DefineMap.extend( "MyTagViewModel", {
40-
message: "string"
41-
} );
43+
```html
44+
<my-tag></my-tag>
4245

43-
Component.extend( {
46+
<script type="module">
47+
import {Component, DefineMap} from "can";
48+
49+
const MyTagViewModel = DefineMap.extend( "MyTagViewModel", {
50+
message: {default: "Hello there!"}
51+
} );
52+
53+
Component.extend({
4454
tag: "my-tag",
4555
ViewModel: MyTagViewModel,
46-
view: "<h1>{{message}}</h1>"
47-
} );
48-
```
56+
view: `<h1>{{message}}</h1>`
57+
});
4958
50-
Use [can-view-model] to read a component’s view model instance.
59+
var viewModelInstance = document.querySelector("my-tag").viewModel;
60+
console.log(viewModelInstance) //-> MyTagViewModel{message: "Hello there!"}
61+
</script>
62+
```
63+
@codepen
64+
65+
66+
Use `element.viewModel` to read a component’s view-model instance.
5167

5268
@param {Object} properties The initial properties that are passed by the [can-stache-bindings data bindings].
5369

@@ -65,14 +81,24 @@ added to the top of the [can-view-scope] the component’s [can-component::view]
6581

6682
@body
6783

84+
## Background
85+
86+
Before reading this documentation, it's useful to have read the [guides/technology-overview]
87+
and [guides/html] guides.
88+
6889
## Use
6990

7091
[can-component]’s ViewModel property is used to create an __object__, typically an instance
7192
of a [can-define/map/map], that will be used to render the component’s
7293
view. This is most easily understood with an example. The following
7394
component shows the current page number based off a `limit` and `offset` value:
7495

75-
```js
96+
```html
97+
<my-paginate></my-paginate>
98+
99+
<script type="module">
100+
import {DefineMap, Component} from "can";
101+
76102
const MyPaginateViewModel = DefineMap.extend( {
77103
offset: { default: 0 },
78104
limit: { default: 20 },
@@ -86,17 +112,11 @@ Component.extend( {
86112
ViewModel: MyPaginateViewModel,
87113
view: "Page {{page}}."
88114
} );
115+
</script>
89116
```
117+
@codepen
90118

91-
If this component HTML was inserted into the page like:
92-
93-
```js
94-
const renderer = stache( "<my-paginate/>" );
95-
const frag = renderer();
96-
document.body.appendChild( frag );
97-
```
98-
99-
It would result in:
119+
This will result in:
100120

101121
```html
102122
<my-paginate>Page 1</my-paginate>
@@ -127,7 +147,12 @@ that anonymous type as the view model.
127147

128148
The following does the same as above:
129149

130-
```js
150+
```html
151+
<my-paginate></my-paginate>
152+
153+
<script type="module">
154+
import {Component} from "can";
155+
131156
Component.extend( {
132157
tag: "my-paginate",
133158
ViewModel: {
@@ -139,7 +164,9 @@ Component.extend( {
139164
},
140165
view: "Page {{page}}."
141166
} );
167+
</script>
142168
```
169+
@codepen
143170

144171
## Values passed from attributes
145172

@@ -176,6 +203,7 @@ Component.extend( {
176203

177204
If `<my-paginate>` is used like:
178205

206+
179207
```js
180208
const renderer = stache( "<my-paginate offset:from='index' limit:from='size' />" );
181209

docs/connectedCallback.md

Lines changed: 64 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,73 @@
33

44
@description A lifecycle hook called after the component's element is inserted into the document.
55

6-
@signature `connectedCallback: function () { ... }`
6+
@signature `connectedCallback: function (element) { ... }`
77

8-
Mainly used as the context to orchestrate property bindings that would
9-
otherwise be a stream or an inappropriate side-effect during a getter.
8+
Use to orchestrate property bindings that would
9+
otherwise be a stream or an inappropriate side-effect during a getter.
1010

11-
For example, the following listens to changes on the `name` property
12-
and counts them in the `nameChanged` property:
11+
For example, the following listens to changes on the `name` property
12+
and counts them in the `nameChanged` property:
13+
14+
```html
15+
<my-component></my-component>
16+
17+
<script type="module">
18+
import {Component} from "can";
19+
20+
Component.extend({
21+
tag: "my-component",
22+
view: `
23+
<p>Name changed: {{nameChanged}}</p>
24+
<p>Name: <input value:bind="name"/></p>
25+
`,
26+
ViewModel: {
27+
nameChanged: {type: "number", default: 0},
28+
name: "string",
29+
connectedCallback( element ) {
30+
this.listenTo( "name", function() {
31+
this.nameChanged++;
32+
} );
33+
const disconnectedCallback = this.stopListening.bind( this );
34+
return disconnectedCallback;
35+
}
36+
}
37+
});
38+
</script>
39+
```
40+
@highlight 15-21
41+
@codepen
42+
43+
`connectedCallback` is named as such to match the [web components](https://developers.google.com/web/fundamentals/web-components/customelements#reactions) spec for the same concept.
44+
45+
@return {Function|undefined} The `disconnectedCallback` function to be called during teardown. Defined in the same closure scope as setup, it's used to tear down anything that was set up during the `connectedCallback` lifecycle hook. If `undefined` is returned, the default `disconnectedCallback` function will be the
46+
`viewModel`'s [can-event-queue/map/map.stopListening] function. So if you overwrite `disconnectedCallback`,
47+
you probably want to make sure [can-event-queue/map/map.stopListening] is called.
48+
49+
@body
50+
51+
## Use
52+
53+
Checkout the [guides/recipes/video-player] for a good example of using `connectedCallback` to create
54+
side effects. For example, it listens to the `viewModel`'s `playing` and `currentTime` and calls
55+
side-effectual DOM methods like `.play()`.
1356

1457
```js
15-
const Person = DefineMap.extend( {
16-
nameChanged: "number",
17-
name: "string",
18-
connectedCallback() {
19-
this.listenTo( "name", function() {
20-
this.nameChanged++;
21-
} );
22-
const disconnectedCallback = this.stopListening.bind( this );
23-
return disconnectedCallback;
24-
}
25-
} );
58+
connectedCallback(element) {
59+
this.listenTo("playing", function(event, isPlaying) {
60+
if (isPlaying) {
61+
element.querySelector("video").play();
62+
} else {
63+
element.querySelector("video").pause();
64+
}
65+
});
66+
this.listenTo("currentTime", function(event, currentTime) {
67+
const videoElement = element.querySelector("video");
68+
if (currentTime !== videoElement.currentTime) {
69+
videoElement.currentTime = currentTime;
70+
}
71+
});
72+
}
2673
```
2774

28-
`connectedCallback` is named as such to match the [web components](https://developers.google.com/web/fundamentals/web-components/customelements#reactions) spec for the same concept.
29-
30-
@return {Function|undefined} The `disconnectedCallback` function to be called during teardown. Defined in the same closure scope as setup, it's used to tear down anything that was set up during the `connectedCallback` lifecycle hook.
75+
As a reminder, event bindings bound with [can-event-queue/map/map.listenTo] (which is available on [can-define/map/map]) will automatically be torn down when the component is removed from the page.

test/component-viewmodel-test.js

Lines changed: 68 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -543,26 +543,77 @@ helpers.makeTests("can-component viewModels", function(){
543543

544544
});
545545

546-
QUnit.test("Can be called on an element using preventDataBindings (#183)", function(){
547-
Component.extend({
548-
tag: "prevent-data-bindings",
549-
ViewModel: {},
550-
view: stache("{{value}}")
551-
});
546+
QUnit.test("Can be called on an element using preventDataBindings (#183)", function(){
547+
Component.extend({
548+
tag: "prevent-data-bindings",
549+
ViewModel: {},
550+
view: stache("{{value}}")
551+
});
552552

553-
var document = this.document;
554-
var el = document.createElement("div");
555-
var callback = tag("prevent-data-bindings");
553+
var document = this.document;
554+
var el = document.createElement("div");
555+
var callback = tag("prevent-data-bindings");
556556

557-
var vm = new observe.Object({ value: "it worked" });
558-
el[canSymbol.for('can.viewModel')] = vm;
559-
canData.set.call(el, "preventDataBindings", true);
560-
callback(el, {
561-
scope: new Scope({ value: "it did not work" })
562-
});
563-
canData.set.call(el, "preventDataBindings", false);
557+
var vm = new observe.Object({ value: "it worked" });
558+
el[canSymbol.for('can.viewModel')] = vm;
559+
canData.set.call(el, "preventDataBindings", true);
560+
callback(el, {
561+
scope: new Scope({ value: "it did not work" })
562+
});
563+
canData.set.call(el, "preventDataBindings", false);
564+
565+
QUnit.equal(el.firstChild.nodeValue, "it worked");
566+
});
564567

565-
QUnit.equal(el.firstChild.nodeValue, "it worked");
568+
QUnit.test("viewModel available as viewModel property (#282)", function() {
569+
Component.extend({
570+
tag: "can-map-viewmodel",
571+
view: stache("{{name}}"),
572+
viewModel: {
573+
name: "Matthew"
574+
}
566575
});
567576

577+
var renderer = stache("<can-map-viewmodel></can-map-viewmodel>");
578+
579+
var fragOne = renderer();
580+
var vmOne = fragOne.firstChild.viewModel;
581+
582+
var fragTwo = renderer();
583+
584+
vmOne.set("name", "Wilbur");
585+
586+
equal(fragOne.firstChild.firstChild.nodeValue, "Wilbur", "The first map changed values");
587+
equal(fragTwo.firstChild.firstChild.nodeValue, "Matthew", "The second map did not change");
588+
});
589+
590+
QUnit.test("connectedCallback without a disconnect calls stopListening", 1, function(){
591+
QUnit.stop();
592+
593+
var map = new SimpleMap();
594+
595+
Component.extend({
596+
tag: "connected-component-listen",
597+
view: stache('rendered'),
598+
ViewModel: {
599+
connectedCallback: function(element) {
600+
this.listenTo(map,"foo", function(){});
601+
}
602+
}
603+
});
604+
var template = stache("<connected-component-listen/>");
605+
var frag = template();
606+
var first = frag.firstChild;
607+
domMutateNode.appendChild.call(this.fixture, frag);
608+
609+
helpers.afterMutation(function(){
610+
611+
domMutateNode.removeChild.call(first.parentNode, first);
612+
helpers.afterMutation(function(){
613+
QUnit.notOk( canReflect.isBound(map), "stopListening no matter what on vm");
614+
QUnit.start();
615+
});
616+
});
617+
});
618+
568619
});

0 commit comments

Comments
 (0)