Skip to content

Commit

Permalink
fixes #282, needs docs, but improves docs on other parts of can-compo…
Browse files Browse the repository at this point in the history
…nent (#283)
  • Loading branch information
justinbmeyer authored Jul 19, 2018
1 parent cb8023a commit 382f66a
Show file tree
Hide file tree
Showing 4 changed files with 200 additions and 73 deletions.
3 changes: 3 additions & 0 deletions can-component.js
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,7 @@ var Component = Construct.extend(
this.viewModel = viewModel;

el[viewModelSymbol] = viewModel;
el.viewModel = viewModel;
domData.set.call(el, "preventDataBindings", true);

// ## Helpers
Expand Down Expand Up @@ -558,6 +559,8 @@ var Component = Construct.extend(
}
if(disconnectedCallback) {
disconnectedCallback(el);
} else if(typeof viewModel.stopListening === "function"){
viewModel.stopListening();
}
}, componentTagData.parentNodeList || true, false);
nodeList.expression = "<" + this.tag + ">";
Expand Down
102 changes: 65 additions & 37 deletions docs/ViewModel.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,49 +5,65 @@

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

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

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

```js
import Component from "can-component";
```html
<my-tag></my-tag>

Component.extend( {
<script type="module">
import {Component} from "can";
Component.extend({
tag: "my-tag",
ViewModel: {
message: "string"
message: {default: "Hello there!"}
},
view: "<h1>{{message}}</h1>"
} );
```
view: `<h1>{{message}}</h1>`
});
@type {function} A constructor function (usually defined by [can-define/map/map.extend DefineMap.extend] or
[can-map Map.extend]) that will be used to create a new observable instance accessible by
the component’s [can-component::view].
var viewModelInstance = document.querySelector("my-tag").viewModel;
console.log(viewModelInstance) //-> MyTagVM{message: "Hello there!"}
</script>
```
@codepen

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

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

const MyTagViewModel = DefineMap.extend( "MyTagViewModel", {
message: "string"
} );
```html
<my-tag></my-tag>

Component.extend( {
<script type="module">
import {Component, DefineMap} from "can";
const MyTagViewModel = DefineMap.extend( "MyTagViewModel", {
message: {default: "Hello there!"}
} );
Component.extend({
tag: "my-tag",
ViewModel: MyTagViewModel,
view: "<h1>{{message}}</h1>"
} );
```
view: `<h1>{{message}}</h1>`
});
Use [can-view-model] to read a component’s view model instance.
var viewModelInstance = document.querySelector("my-tag").viewModel;
console.log(viewModelInstance) //-> MyTagViewModel{message: "Hello there!"}
</script>
```
@codepen


Use `element.viewModel` to read a component’s view-model instance.

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

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

@body

## Background

Before reading this documentation, it's useful to have read the [guides/technology-overview]
and [guides/html] guides.

## Use

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

```js
```html
<my-paginate></my-paginate>

<script type="module">
import {DefineMap, Component} from "can";
const MyPaginateViewModel = DefineMap.extend( {
offset: { default: 0 },
limit: { default: 20 },
Expand All @@ -86,17 +112,11 @@ Component.extend( {
ViewModel: MyPaginateViewModel,
view: "Page {{page}}."
} );
</script>
```
@codepen

If this component HTML was inserted into the page like:

```js
const renderer = stache( "<my-paginate/>" );
const frag = renderer();
document.body.appendChild( frag );
```

It would result in:
This will result in:

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

The following does the same as above:

```js
```html
<my-paginate></my-paginate>

<script type="module">
import {Component} from "can";
Component.extend( {
tag: "my-paginate",
ViewModel: {
Expand All @@ -139,7 +164,9 @@ Component.extend( {
},
view: "Page {{page}}."
} );
</script>
```
@codepen

## Values passed from attributes

Expand Down Expand Up @@ -176,6 +203,7 @@ Component.extend( {

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


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

Expand Down
83 changes: 64 additions & 19 deletions docs/connectedCallback.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,73 @@

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

@signature `connectedCallback: function () { ... }`
@signature `connectedCallback: function (element) { ... }`

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

For example, the following listens to changes on the `name` property
and counts them in the `nameChanged` property:
For example, the following listens to changes on the `name` property
and counts them in the `nameChanged` property:

```html
<my-component></my-component>

<script type="module">
import {Component} from "can";
Component.extend({
tag: "my-component",
view: `
<p>Name changed: {{nameChanged}}</p>
<p>Name: <input value:bind="name"/></p>
`,
ViewModel: {
nameChanged: {type: "number", default: 0},
name: "string",
connectedCallback( element ) {
this.listenTo( "name", function() {
this.nameChanged++;
} );
const disconnectedCallback = this.stopListening.bind( this );
return disconnectedCallback;
}
}
});
</script>
```
@highlight 15-21
@codepen

`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.

@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
`viewModel`'s [can-event-queue/map/map.stopListening] function. So if you overwrite `disconnectedCallback`,
you probably want to make sure [can-event-queue/map/map.stopListening] is called.

@body

## Use

Checkout the [guides/recipes/video-player] for a good example of using `connectedCallback` to create
side effects. For example, it listens to the `viewModel`'s `playing` and `currentTime` and calls
side-effectual DOM methods like `.play()`.

```js
const Person = DefineMap.extend( {
nameChanged: "number",
name: "string",
connectedCallback() {
this.listenTo( "name", function() {
this.nameChanged++;
} );
const disconnectedCallback = this.stopListening.bind( this );
return disconnectedCallback;
}
} );
connectedCallback(element) {
this.listenTo("playing", function(event, isPlaying) {
if (isPlaying) {
element.querySelector("video").play();
} else {
element.querySelector("video").pause();
}
});
this.listenTo("currentTime", function(event, currentTime) {
const videoElement = element.querySelector("video");
if (currentTime !== videoElement.currentTime) {
videoElement.currentTime = currentTime;
}
});
}
```

`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.

@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.
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.
85 changes: 68 additions & 17 deletions test/component-viewmodel-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -543,26 +543,77 @@ helpers.makeTests("can-component viewModels", function(){

});

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

var document = this.document;
var el = document.createElement("div");
var callback = tag("prevent-data-bindings");
var document = this.document;
var el = document.createElement("div");
var callback = tag("prevent-data-bindings");

var vm = new observe.Object({ value: "it worked" });
el[canSymbol.for('can.viewModel')] = vm;
canData.set.call(el, "preventDataBindings", true);
callback(el, {
scope: new Scope({ value: "it did not work" })
});
canData.set.call(el, "preventDataBindings", false);
var vm = new observe.Object({ value: "it worked" });
el[canSymbol.for('can.viewModel')] = vm;
canData.set.call(el, "preventDataBindings", true);
callback(el, {
scope: new Scope({ value: "it did not work" })
});
canData.set.call(el, "preventDataBindings", false);

QUnit.equal(el.firstChild.nodeValue, "it worked");
});

QUnit.equal(el.firstChild.nodeValue, "it worked");
QUnit.test("viewModel available as viewModel property (#282)", function() {
Component.extend({
tag: "can-map-viewmodel",
view: stache("{{name}}"),
viewModel: {
name: "Matthew"
}
});

var renderer = stache("<can-map-viewmodel></can-map-viewmodel>");

var fragOne = renderer();
var vmOne = fragOne.firstChild.viewModel;

var fragTwo = renderer();

vmOne.set("name", "Wilbur");

equal(fragOne.firstChild.firstChild.nodeValue, "Wilbur", "The first map changed values");
equal(fragTwo.firstChild.firstChild.nodeValue, "Matthew", "The second map did not change");
});

QUnit.test("connectedCallback without a disconnect calls stopListening", 1, function(){
QUnit.stop();

var map = new SimpleMap();

Component.extend({
tag: "connected-component-listen",
view: stache('rendered'),
ViewModel: {
connectedCallback: function(element) {
this.listenTo(map,"foo", function(){});
}
}
});
var template = stache("<connected-component-listen/>");
var frag = template();
var first = frag.firstChild;
domMutateNode.appendChild.call(this.fixture, frag);

helpers.afterMutation(function(){

domMutateNode.removeChild.call(first.parentNode, first);
helpers.afterMutation(function(){
QUnit.notOk( canReflect.isBound(map), "stopListening no matter what on vm");
QUnit.start();
});
});
});

});

0 comments on commit 382f66a

Please sign in to comment.