Skip to content

Commit

Permalink
Add Knockout component and Redux integration
Browse files Browse the repository at this point in the history
Add knockout-loader, that allows you to import styles and a ViewModel
script from a knockout component template, which registers itself as
a knockout component.

Add a `knockout-redux-connect` function that connects knockout with
redux, converting the returned redux state to observables, and updating
them when the store changes.

Added documentation for the above; how to set up a knockout and
redux 'block' in your project.

re #27
  • Loading branch information
ThaNarie committed Nov 22, 2017
1 parent 327dfd0 commit 03bfdeb
Show file tree
Hide file tree
Showing 12 changed files with 500 additions and 38 deletions.
4 changes: 4 additions & 0 deletions build-tools/config/webpack/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ module.exports = merge(require('./webpack.config.base'), {
enforce: 'pre',
loader: 'source-map-loader'
},
{
test: /\.ko/,
loader: 'knockout-loader'
},
]
},
devServer: {
Expand Down
83 changes: 83 additions & 0 deletions build-tools/loaders/knockout-loader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
const path = require('path');
const loaderUtils = require('loader-utils');

/**
* Processes knockout templates to import script and style files.
* Registers itself as knockout component based on its filename.
*
* For scripts:
* - Changes the html script include to a js file require
* - Also registers the class to be initialized
* - Has support for hot reloading
*
* For styles:
* - Changes the html style link to a css file require
*/
module.exports = function(content) {
const loaderContext = this;
const done = this.async();
this.cacheable();

const options = loaderUtils.getOptions(this) || {};

const componentName = path.basename(loaderContext.resourcePath, '.ko');

const hot = typeof options.hot === 'undefined' ? true : options.hot;

const scripts = [];
const styles = [];

content = content.replace(/<script src=["']([^"']+)["']><\/script>[\\r\\n]*/ig, (res, match) => {
scripts.push(match);
return '';
});

content = content.replace(/<link rel=["']stylesheet["'] href=["']([^"']+)["']>[\\r\\n]*/ig, (res, match) => {
styles.push(match);
return '';
});

let newContent = '';

// if (scripts.length) {
// newContent = `
// ${scripts.map(script => `
// var component = require(${loaderUtils.stringifyRequest(loaderContext, script)}).default;
// var registerComponent = require(${loaderUtils.stringifyRequest(loaderContext, 'app/muban/componentUtils.ts')}).registerComponent;
// registerComponent(component);
// ${hot ? `var updateComponent = require(${loaderUtils.stringifyRequest(loaderContext, 'app/muban/componentUtils.ts')}).updateComponent;
//
// // Hot Module Replacement API
// if (module.hot) {
// module.hot.accept(${loaderUtils.stringifyRequest(loaderContext, script)}, function() {
// var component = require(${loaderUtils.stringifyRequest(loaderContext, script)}).default;
// updateComponent(component);
// });
// }` : ''}
// `).join("\n")}
//
// ` + newContent;
// }

if (styles.length) {
newContent = `
${styles.map(style =>
`require(${loaderUtils.stringifyRequest(loaderContext, style)});`
).join("\n")}
` + newContent;
}

newContent = newContent + `
var ko = require('knockout');
if (ko.components.isRegistered('ko-${componentName}')) {
ko.components.unregister('ko-${componentName}');
}
ko.components.register('ko-${componentName}', {${
scripts.length ? `viewModel: require(${loaderUtils.stringifyRequest(loaderContext, scripts[0])}).default,` : ''}
template: '${content.replace(/'/g, "\\'").replace(/\n/g, '\\n')}'
});
console.log('registered: ko-${componentName}');
`;

done(null, newContent);
};
8 changes: 4 additions & 4 deletions docs/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,20 @@ To make things interactive it can also contain a script file.

A simple component could look like this:

```
```html
<button class="component-button">{{text}}</button>
```

To make the button look nice, and handle some logic, you could link to a style and script file:

```
```html
<link rel="stylesheet" href="./button.scss">
<script src="./Button.ts"></script>

<button data-component="button">{{text}}</button>
```

```
```scss
[data-component="button"] {
border: 1px solid #ddd;
background-color: #eee;
Expand All @@ -37,7 +37,7 @@ To make the button look nice, and handle some logic, you could link to a style a
}
```

```
```typescript
import AbstractComponent from "app/component/AbstractComponent";

export default class Button extends AbstractComponent {
Expand Down
31 changes: 15 additions & 16 deletions docs/dynamic-data.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@ yarn add whatwg-fetch
```

Import in the file in `dev.js` and `dist.js`:
```
```js
import 'whatwg-fetch';
```


##### Getting HTML
```
```js
fetch('/users.html')
.then(response => response.text())
.then(body => {
Expand All @@ -40,7 +40,7 @@ fetch('/users.html')
```

##### Getting JSON
```
```js
fetch('/users.json')
.then(response => response.json())
.then(json => {
Expand All @@ -51,7 +51,7 @@ fetch('/users.json')
```

##### Post form
```
```js
var form = document.querySelector('form')

fetch('/users', {
Expand All @@ -61,7 +61,7 @@ fetch('/users', {
```

##### Post JSON
```
```js
fetch('/users', {
method: 'POST',
headers: {
Expand All @@ -75,7 +75,7 @@ fetch('/users', {
```

##### File Upload
```
```js
const input = document.querySelector('input[type="file"]')

const data = new FormData()
Expand Down Expand Up @@ -103,7 +103,7 @@ snippet for that section. In that case we should:
3. replace the HTML on the page
4. initialize new component instances for that section and nested components

```
```typescript
// code is located a component, where this.element points to HTML element for that section

import { cleanElement, initComponents } from '../../../muban/componentUtils';
Expand Down Expand Up @@ -131,7 +131,7 @@ fetch(`/api/section/${id}`)

Luckily there is a utility function for this:

```
```js
// code is located a component, where this.element points to HTML element for that section

import { updateElement } from '../../../muban/componentUtils';
Expand All @@ -153,7 +153,7 @@ especially when dealing with animation/transitions.
This one might be a bit more work compared to just replacing HTML, but gives you way more control
over what happens on the page. The big benefit is that the state doesn't reset, allowing you to
make nice transitions while the new data is updated on the page.
```
```js
fetch(`/api/section/${id}`)
.then(response => response.json())
.then(json => {
Expand All @@ -168,7 +168,7 @@ fetch(`/api/section/${id}`)
```

Or when using knockout to update your HTML:
```
```typescript
import { initTextBinding } from '../../../muban/knockoutUtils';
import ko from 'knockout';

Expand Down Expand Up @@ -199,7 +199,7 @@ on the page, it's not that difficult.
We can just query all the items, and retrieve the information we need to execute our logic, and
add them back to the page.

```
```typescript
constructor() {
this.initItems();
this.updateItems();
Expand Down Expand Up @@ -245,7 +245,6 @@ private sortOnTitle(itemData, ascending:boolean = false) {
private filterOnTags(itemData, filter:string) {
return itemData.filter(item => item.tags.some(tag => tag.includes(filter.toLowerCase())));
}
```

### Load more items to the page
Expand All @@ -265,7 +264,7 @@ There are two options we can choose from.
For smaller items, we could just clone the first element of the list, and create a function that
updates all the data in that item, so we can append it to the DOM.

```
```typescript
// get the template node to clone later
const template = <HTMLELement>this.element.querySelector('.item');
// create a documentFragment for better performance when adding items
Expand All @@ -289,7 +288,7 @@ This option works best when only used on the client, but when having server-rend
DOM you would first need to convert them to data to properly render them.

Handlebars template:
```
```html
<!--
List item template, keep in HTML since it will be used by javascript.
The HTML in the script-template is similar to the html in the handlebars list below.
Expand Down Expand Up @@ -322,7 +321,7 @@ be used by knockout to render the list client-side (when new data comes in).
```

Script:
```
```js
// 1. transform old items to data
// get all DOM nodes
const items = Array.from(this.element.querySelectorAll('.item'));
Expand All @@ -349,7 +348,7 @@ itemData.push(...newData);

The above can be simplified by using a util.
The 3rd parameter can also be `oldData` extract above instead of the passed config for more control.
```
```js
import { initListBinding } from '../../../muban/knockoutUtils';

// 1+2+3. extract data, create observable and apply bindings
Expand Down
16 changes: 8 additions & 8 deletions docs/knockout.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ your view when the data updates.

Some examples on how to use this:

```
```typescript
// initiate the observable
private searchOpened:KnockoutObservable<boolean> = ko.observable(false);

Expand Down Expand Up @@ -55,11 +55,11 @@ Within the data-bind values you can pass observables, but you have to do so with
the `()`. If you do so, it will just return that value, and the changes won't be tracked.
By supplying the observable itself, changes can be tracked to update the binding.

```
```js
ko.applyBindingsToNode(element, object);
```

```
```js
ko.applyBindingsToNode(this.element.querySelector('.search-results'), {
'css' : { 'opened': this.searchOpened }
});
Expand All @@ -79,15 +79,15 @@ observables will be tracked, just like in normal computeds.
If one of the data-bind properties for an element needs to be a function, you have to switch to
this method, and all of the properties have to be a function.

```
```js
ko.applyBindingsToNode(element, object, viewModel);
```

Below, the `style` property has to be a function because we are using to observables to return
a custom value. Because of this, the `css` property also has to be a function, but that one
will just reference the observable (calling it would also work here).

```
```js
ko.applyBindingAccessorsToNode(this.content, {
'style' : () => ({
maxWidth: model.deviceEmulateEnabled() ? model.viewportWidth() + 'px' : '100%',
Expand All @@ -100,12 +100,12 @@ The following example applies a binding to a list of elements, where each eleme
computed by introducing some logic. For better performance, the reading of the attributes
should be done only once.

```
```js
$('.bar').toArray().forEach((bar) => {
ko.applyBindingAccessorsToNode(bar, {
'css' : () => {
let min:any = bar.getAttribute('data-size-min');
let max:any = bar.getAttribute('data-size-max');
let min = bar.getAttribute('data-size-min');
let max = bar.getAttribute('data-size-max');
min = min === '*' ? min : parseInt(min, 10);
max = max === '*' ? max : parseInt(max, 10);

Expand Down
Loading

0 comments on commit 03bfdeb

Please sign in to comment.