add mp mvvm

This commit is contained in:
dntzhang 2018-12-06 11:25:51 +08:00
parent 7d2bd68e0d
commit cbdd851695
24 changed files with 1271 additions and 0 deletions

7
packages/mp-mvvm/README.md Executable file
View File

@ -0,0 +1,7 @@
# 小程序的 MVVM 架构 mp-mvvm 正式发布
> 小程序插上 MVVM 的翅膀,和 [Omi MVVM](https://github.com/Tencent/omi/blob/master/tutorial/omi-mvvm.cn.md) 一样强大
## License
MIT [@dntzhang](https://github.com/dntzhang)

6
packages/mp-mvvm/app.js Executable file
View File

@ -0,0 +1,6 @@
//app.js
App({
onLaunch: function () {
}
})

11
packages/mp-mvvm/app.json Executable file
View File

@ -0,0 +1,11 @@
{
"pages":[
"pages/index/index"
],
"window":{
"backgroundTextStyle":"light",
"navigationBarBackgroundColor": "#fff",
"navigationBarTitleText": "WeChat",
"navigationBarTextStyle":"black"
}
}

10
packages/mp-mvvm/app.wxss Executable file
View File

@ -0,0 +1,10 @@
/**app.wxss**/
.container {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
padding: 200rpx 0;
box-sizing: border-box;
}

View File

@ -0,0 +1,20 @@
import vm from '../../view-model/todo'
Component({
properties: {
items:{
type: Array,
value:[]
}
},
ready: function () {
},
methods: {
checkboxChange: function(e) {
vm.toogleComplete(e.currentTarget.dataset.id)
}
}
})

View File

@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}

View File

@ -0,0 +1,5 @@
<view >
<label class="checkbox {{item.completed&&'completed'}}" wx:for="{{items}}">
<checkbox bindtap="checkboxChange" data-id="{{item.id}}" checked="{{item.completed}}"/>{{item.text}}
</label>
</view>

View File

@ -0,0 +1,8 @@
.checkbox{
display: block;
}
.completed{
color: #d9d9d9;
text-decoration: line-through;
}

View File

@ -0,0 +1,5 @@
import Todo from './todo'
const todo = new Todo()
export default todo

View File

@ -0,0 +1,9 @@
let id = 0
export default class TodoItem {
constructor(text, completed) {
this.id = id++
this.text = text
this.completed = completed || false
}
}

View File

@ -0,0 +1,19 @@
//mock
const list = [
{
text: 'Task One'
},
{
text: 'Task Two'
}
]
export function getAll(callback) {
callback(JSON.parse(JSON.stringify(list)))
}
export function add(item) {
list.push({
text: item.text
})
}

View File

@ -0,0 +1,86 @@
import TodoItem from './todo-item'
import { getAll, add } from './todo-server'
export default class Todo {
constructor() {
this.items = []
this.author = {
firstName: 'dnt',
lastName: 'zhang'
}
}
initItems(list) {
list.forEach(item => {
this.items.push(new TodoItem(item.text))
})
}
add(content) {
const item = new TodoItem(content)
this.items.push(item)
add(item)
}
updateContent(id, content) {
this.items.every(item => {
if (id === item.id) {
item.content = content
return false
}
return true
})
}
complete(id) {
this.items.every(item => {
if (id === item.id) {
item.completed = true
return false
}
return true
})
}
uncomplete(id) {
this.items.every(item => {
if (id === item.id) {
item.completed = false
return false
}
return true
})
}
toogleComplete(id) {
this.items.every(item => {
if (id === item.id) {
item.completed = !item.completed
return false
}
return true
})
}
remove(id) {
this.items.every((item, index) => {
if (id === item.id) {
this.items.splice(index, 1)
return false
}
return true
})
}
clear() {
this.items.length = 0
}
getAll(callback) {
getAll(list => {
this.initItems(list)
callback()
})
}
}

View File

@ -0,0 +1,19 @@
import vm from '../../view-model/todo'
import create from '../../utils/create'
create.Page(vm, {
data:{
value:''
},
onLoad: function () {
vm.getAll()
},
tapHandler:function(){
vm.add('abc')
this.setData({
value:''
})
}
})

View File

@ -0,0 +1,6 @@
{
"usingComponents": {
"todo-list": "/components/todo-list/todo-list"
}
}

View File

@ -0,0 +1,11 @@
<!-- index.wxml -->
<view class="container">
<view class="title">Hello MVVM</view>
<view>
<todo-list items="{{items}}" />
<view class="form">
<input class="input" type="text" placeholder="Input your task" on value="{{value}}" />
<button class="button" bindtap="tapHandler">Add{{items.length}}</button>
</view>
</view>
</view>

View File

@ -0,0 +1,38 @@
/**index.wxss**/
.title {
display: flex;
flex-direction: column;
align-items: center;
font-size: 20px;
margin-bottom:40px;
}
.userinfo-avatar {
width: 128rpx;
height: 128rpx;
margin: 20rpx;
border-radius: 50%;
}
.form{
margin-top: 5px;
}
.button{
margin-left:10rpx;
height: 30px;
line-height: 30px;
display: inline-block;
font-size: 30rpx;
}
.input{
height:30px;
width: 300rpx;
text-indent: 10px;
display: inline-block;
border: 1px solid #ccc;
font-size: 30rpx;
border-radius: 8rpx;
}

View File

@ -0,0 +1,36 @@
{
"description": "项目配置文件。",
"packOptions": {
"ignore": []
},
"setting": {
"urlCheck": true,
"es6": true,
"postcss": true,
"minified": true,
"newFeature": true
},
"compileType": "miniprogram",
"libVersion": "9.9.9",
"appid": "wxfaf6dad43f57c6bd",
"projectname": "mp-mvvm",
"isGameTourist": false,
"condition": {
"search": {
"current": -1,
"list": []
},
"conversation": {
"current": -1,
"list": []
},
"game": {
"currentL": -1,
"list": []
},
"miniprogram": {
"current": -1,
"list": []
}
}
}

View File

@ -0,0 +1,33 @@
import JSONProxy from './proxy'
const create = {}
create.Page = function (vm, options) {
options.data = vm.data
const onLoad = options.onLoad
options.onLoad = function (e) {
vm.data = new JSONProxy(vm.data).observe(false, info => {
this.setData(vm.data)
})
onLoad && onLoad.call(this, e)
}
Page(options)
}
create.Component = function () {
}
if (typeof exports == "object") {
module.exports = create
} else if (typeof define == "function" && define.amd) {
define([], function () { return create })
} else {
window.create = create
}

180
packages/mp-mvvm/utils/create3.js Executable file
View File

@ -0,0 +1,180 @@
/*
westore 2.0
*/
import JSONProxy from './proxy'
let globalStore = null
let currentData = null
let timeout = null
let patchs = {}
const fnMapping = {}
const fnPreResult = {}
const fnCurrentResult = {}
const handler = function (patch) {
clearTimeout(timeout)
if (patch.op === 'remove') {//fix arr splice
const kv = getArrayPatch(patch.path)
patchs[kv.k] = kv.v
timeout = setTimeout(function () {
_update(patchs)
patchs = {}
})
} else {
const key = fixPath(patch.path)
patchs[key] = patch.value
timeout = setTimeout(function () {
_update(patchs)
patchs = {}
})
}
}
export default function create(store, option) {
if (arguments.length === 2) {
if (option.data && Object.keys(option.data).length > 0) {
Object.assign(store.data, option.data)
}
if (!store.instances) {
store.instances = {}
store.update = update
}
getApp().globalData && (getApp().globalData.store = store)
globalStore = store
option.data = store.data
currentData = store.data
const jp = new JSONProxy(store.data, handler)
const onLoad = option.onLoad
setFnMapping(globalStore.data)
option.onLoad = function (e) {
this.update = update
this.store = store
this.store.data = jp.observe(true, handler)
store.instances[this.route] = []
store.instances[this.route].push(this)
onLoad && onLoad.call(this, e)
}
Page(option)
} else {
const ready = store.ready
store.ready = function () {
this.update = update
this.page = getCurrentPages()[getCurrentPages().length - 1]
this.store = this.page.store
Object.assign(this.store.data, store.data)
setFnMapping(store.data)
this.setData.call(this, this.store.data)
this.store.instances[this.page.route].push(this)
ready && ready.call(this)
}
Component(store)
}
}
function _update(kv) {
defineFnProp()
Object.keys(fnCurrentResult).forEach(key => {
const v = fnCurrentResult[key]
if(v !== fnPreResult[key]){
kv[key] = v
fnPreResult[key] = v
}
})
setFnMapping(globalStore.data)
for (let key in globalStore.instances) {
globalStore.instances[key].forEach(ins => {
ins.setData.call(ins, kv)
})
}
globalStore.onChange && globalStore.onChange(kv)
}
function update(patch) {
if (patch) {
for (let key in patch) {
updateByPath(globalStore.data, key, patch[key])
}
}
}
function setFnMapping(data) {
Object.keys(data).forEach(key => {
const fn = data[key]
if (typeof fn == 'function') {
fnMapping[key] = () => {
return fn.call(globalStore.data)
}
}
})
}
function defineFnProp(){
Object.keys(fnMapping).forEach(key => {
fnCurrentResult[key] = fnMapping[key]()
})
}
function getArrayPatch(path) {
const arr = path.replace('/', '').split('/')
let current = currentData[arr[0]]
for (let i = 1, len = arr.length; i < len - 1; i++) {
current = current[arr[i]]
}
return { k: fixArrPath(path), v: current }
}
function fixArrPath(path) {
let mpPath = ''
const arr = path.replace('/', '').split('/')
const len = arr.length
arr.forEach((item, index) => {
if (index < len - 1) {
if (index) {
if (isNaN(parseInt(item))) {
mpPath += '.' + item
} else {
mpPath += '[' + item + ']'
}
} else {
mpPath += item
}
}
})
return mpPath
}
function fixPath(path) {
let mpPath = ''
const arr = path.replace('/', '').split('/')
arr.forEach((item, index) => {
if (index) {
if (isNaN(parseInt(item))) {
mpPath += '.' + item
} else {
mpPath += '[' + item + ']'
}
} else {
mpPath += item
}
})
return mpPath
}
function updateByPath(origin, path, value) {
const arr = path.replace(/]/g,'').replace(/\[/g, '.').split('.')
let current = origin
for (let i = 0, len = arr.length; i < len; i++) {
if (i === len - 1) {
current[arr[i]] = value
} else {
current = current[arr[i]]
}
}
}

120
packages/mp-mvvm/utils/mapping.js Executable file
View File

@ -0,0 +1,120 @@
/**
* mappingjs v1.0.0 by dntzhang
* Objects mapping for javascript. Omi MVVM's best partner.
* @method mapping
* @param {Object} options {from: .., to: .., rule: .. }
* @return {Object} To Object
*/
var ARRAYTYPE = '[object Array]'
var OBJECTTYPE = '[object Object]'
var mapping = function (options) {
var from = options.from
var to = options.to
var rules = options.rule
var res = to || {}
Object.keys(from).forEach(function (key) {
res[key] = from[key]
})
rules &&
Object.keys(rules).forEach(function (key) {
var rule = rules[key]
var isPath = key.match(/\.|\[/)
if (typeof rule === 'function') {
if (isPath) {
setPathValue(res, key, rule.call(from))
} else {
res[key] = rule.call(from)
}
} else {
if (isPath) {
setPathValue(res, key, rule)
} else {
res[key] = rule
}
}
})
return res
}
function setPathValue(obj, path, value) {
var arr = path
.replace(/]/g, '')
.replace(/\[/g, '.')
.split('.')
var current = obj
for (var i = 0, len = arr.length; i < len; i++) {
var key = arr[i]
var temp = current[key]
if (i === len - 1) {
current[arr[len - 1]] = value
} else {
if (temp === undefined) {
if (isNaN(Number(arr[i + 1]))) {
current[key] = {}
} else {
current[key] = []
}
temp = current[key]
}
}
current = temp
}
}
mapping.auto = function (from, to) {
return objMapping(from, to)
}
function arrayMapping(from, to) {
from.forEach(function (item, index) {
if (isArray(item)) {
to[index] = to[index] || []
arrayMapping(item, to[index])
} else if (isObject(item)) {
to[index] = objMapping(item, to[index])
} else {
to[index] = item
}
})
}
function objMapping(from, to) {
var res = to || {}
Object.keys(from).forEach(key => {
var obj = from[key]
if (isArray(obj)) {
res[key] = res[key] || []
arrayMapping(obj, res[key])
} else if (isObject(obj)) {
res[key] = res[key] || {}
objMapping(obj, res[key])
} else {
res[key] = obj
}
})
return res
}
function isArray(obj) {
return Object.prototype.toString.call(obj) === ARRAYTYPE
}
function isObject(obj) {
return Object.prototype.toString.call(obj) === OBJECTTYPE
}
if (typeof exports == "object") {
module.exports = mapping
} else if (typeof define == "function" && define.amd) {
define([], function () { return mapping })
} else {
window.mapping = mapping
}

180
packages/mp-mvvm/utils/mvvm.js Executable file
View File

@ -0,0 +1,180 @@
/*
westore 2.0
*/
import JSONProxy from './proxy'
let globalStore = null
let currentData = null
let timeout = null
let patchs = {}
const fnMapping = {}
const fnPreResult = {}
const fnCurrentResult = {}
const handler = function (patch) {
clearTimeout(timeout)
if (patch.op === 'remove') {//fix arr splice
const kv = getArrayPatch(patch.path)
patchs[kv.k] = kv.v
timeout = setTimeout(function () {
_update(patchs)
patchs = {}
})
} else {
const key = fixPath(patch.path)
patchs[key] = patch.value
timeout = setTimeout(function () {
_update(patchs)
patchs = {}
})
}
}
export default function create(store, option) {
if (arguments.length === 2) {
if (option.data && Object.keys(option.data).length > 0) {
Object.assign(store.data, option.data)
}
if (!store.instances) {
store.instances = {}
store.update = update
}
getApp().globalData && (getApp().globalData.store = store)
globalStore = store
option.data = store.data
currentData = store.data
const jp = new JSONProxy(store.data, handler)
const onLoad = option.onLoad
setFnMapping(globalStore.data)
option.onLoad = function (e) {
this.update = update
this.store = store
this.store.data = jp.observe(true, handler)
store.instances[this.route] = []
store.instances[this.route].push(this)
onLoad && onLoad.call(this, e)
}
Page(option)
} else {
const ready = store.ready
store.ready = function () {
this.update = update
this.page = getCurrentPages()[getCurrentPages().length - 1]
this.store = this.page.store
Object.assign(this.store.data, store.data)
setFnMapping(store.data)
this.setData.call(this, this.store.data)
this.store.instances[this.page.route].push(this)
ready && ready.call(this)
}
Component(store)
}
}
function _update(kv) {
defineFnProp()
Object.keys(fnCurrentResult).forEach(key => {
const v = fnCurrentResult[key]
if(v !== fnPreResult[key]){
kv[key] = v
fnPreResult[key] = v
}
})
setFnMapping(globalStore.data)
for (let key in globalStore.instances) {
globalStore.instances[key].forEach(ins => {
ins.setData.call(ins, kv)
})
}
globalStore.onChange && globalStore.onChange(kv)
}
function update(patch) {
if (patch) {
for (let key in patch) {
updateByPath(globalStore.data, key, patch[key])
}
}
}
function setFnMapping(data) {
Object.keys(data).forEach(key => {
const fn = data[key]
if (typeof fn == 'function') {
fnMapping[key] = () => {
return fn.call(globalStore.data)
}
}
})
}
function defineFnProp(){
Object.keys(fnMapping).forEach(key => {
fnCurrentResult[key] = fnMapping[key]()
})
}
function getArrayPatch(path) {
const arr = path.replace('/', '').split('/')
let current = currentData[arr[0]]
for (let i = 1, len = arr.length; i < len - 1; i++) {
current = current[arr[i]]
}
return { k: fixArrPath(path), v: current }
}
function fixArrPath(path) {
let mpPath = ''
const arr = path.replace('/', '').split('/')
const len = arr.length
arr.forEach((item, index) => {
if (index < len - 1) {
if (index) {
if (isNaN(parseInt(item))) {
mpPath += '.' + item
} else {
mpPath += '[' + item + ']'
}
} else {
mpPath += item
}
}
})
return mpPath
}
function fixPath(path) {
let mpPath = ''
const arr = path.replace('/', '').split('/')
arr.forEach((item, index) => {
if (index) {
if (isNaN(parseInt(item))) {
mpPath += '.' + item
} else {
mpPath += '[' + item + ']'
}
} else {
mpPath += item
}
})
return mpPath
}
function updateByPath(origin, path, value) {
const arr = path.replace(/]/g,'').replace(/\[/g, '.').split('.')
let current = origin
for (let i = 0, len = arr.length; i < len; i++) {
if (i === len - 1) {
current[arr[i]] = value
} else {
current = current[arr[i]]
}
}
}

394
packages/mp-mvvm/utils/proxy.js Executable file
View File

@ -0,0 +1,394 @@
'use strict';
/*!
* https://github.com/Palindrom/JSONPatcherProxy
* (c) 2017 Starcounter
* MIT license
*/
/** Class representing a JS Object observer */
const JSONPatcherProxy = (function() {
/**
* Deep clones your object and returns a new object.
*/
function deepClone(obj) {
switch (typeof obj) {
case 'object':
return JSON.parse(JSON.stringify(obj)); //Faster than ES5 clone - http://jsperf.com/deep-cloning-of-objects/5
case 'undefined':
return null; //this is how JSON.stringify behaves for array items
default:
return obj; //no need to clone primitives
}
}
JSONPatcherProxy.deepClone = deepClone;
function escapePathComponent(str) {
if (str.indexOf('/') == -1 && str.indexOf('~') == -1) return str;
return str.replace(/~/g, '~0').replace(/\//g, '~1');
}
JSONPatcherProxy.escapePathComponent = escapePathComponent;
/**
* Walk up the parenthood tree to get the path
* @param {JSONPatcherProxy} instance
* @param {Object} obj the object you need to find its path
*/
function findObjectPath(instance, obj) {
const pathComponents = [];
let parentAndPath = instance.parenthoodMap.get(obj);
while (parentAndPath && parentAndPath.path) {
// because we're walking up-tree, we need to use the array as a stack
pathComponents.unshift(parentAndPath.path);
parentAndPath = instance.parenthoodMap.get(parentAndPath.parent);
}
if (pathComponents.length) {
const path = pathComponents.join('/');
return '/' + path;
}
return '';
}
/**
* A callback to be used as th proxy set trap callback.
* It updates parenthood map if needed, proxifies nested newly-added objects, calls default callbacks with the changes occurred.
* @param {JSONPatcherProxy} instance JSONPatcherProxy instance
* @param {Object} target the affected object
* @param {String} key the effect property's name
* @param {Any} newValue the value being set
*/
function setTrap(instance, target, key, newValue) {
const parentPath = findObjectPath(instance, target);
const destinationPropKey = parentPath + '/' + escapePathComponent(key);
if (instance.proxifiedObjectsMap.has(newValue)) {
const newValueOriginalObject = instance.proxifiedObjectsMap.get(newValue);
instance.parenthoodMap.set(newValueOriginalObject.originalObject, {
parent: target,
path: key
});
}
/*
mark already proxified values as inherited.
rationale: proxy.arr.shift()
will emit
{op: replace, path: '/arr/1', value: arr_2}
{op: remove, path: '/arr/2'}
by default, the second operation would revoke the proxy, and this renders arr revoked.
That's why we need to remember the proxies that are inherited.
*/
const revokableInstance = instance.proxifiedObjectsMap.get(newValue);
/*
Why do we need to check instance.isProxifyingTreeNow?
We need to make sure we mark revokables as inherited ONLY when we're observing,
because throughout the first proxification, a sub-object is proxified and then assigned to
its parent object. This assignment of a pre-proxified object can fool us into thinking
that it's a proxified object moved around, while in fact it's the first assignment ever.
Checking isProxifyingTreeNow ensures this is not happening in the first proxification,
but in fact is is a proxified object moved around the tree
*/
if (revokableInstance && !instance.isProxifyingTreeNow) {
revokableInstance.inherited = true;
}
// if the new value is an object, make sure to watch it
if (
newValue &&
typeof newValue == 'object' &&
!instance.proxifiedObjectsMap.has(newValue)
) {
instance.parenthoodMap.set(newValue, {
parent: target,
path: key
});
newValue = instance._proxifyObjectTreeRecursively(target, newValue, key);
}
// let's start with this operation, and may or may not update it later
const operation = {
op: 'remove',
path: destinationPropKey
};
if (typeof newValue == 'undefined') {
// applying De Morgan's laws would be a tad faster, but less readable
if (!Array.isArray(target) && !target.hasOwnProperty(key)) {
// `undefined` is being set to an already undefined value, keep silent
return Reflect.set(target, key, newValue);
} else {
// when array element is set to `undefined`, should generate replace to `null`
if (Array.isArray(target)) {
// undefined array elements are JSON.stringified to `null`
(operation.op = 'replace'), (operation.value = null);
}
const oldValue = instance.proxifiedObjectsMap.get(target[key]);
// was the deleted a proxified object?
if (oldValue) {
instance.parenthoodMap.delete(target[key]);
instance.disableTrapsForProxy(oldValue);
instance.proxifiedObjectsMap.delete(oldValue);
}
}
} else {
if (Array.isArray(target) && !Number.isInteger(+key.toString())) {
/* array props (as opposed to indices) don't emit any patches, to avoid needless `length` patches */
if(key != 'length') {
console.warn('JSONPatcherProxy noticed a non-integer prop was set for an array. This will not emit a patch');
}
return Reflect.set(target, key, newValue);
}
operation.op = 'add';
if (target.hasOwnProperty(key)) {
if (typeof target[key] !== 'undefined' || Array.isArray(target)) {
operation.op = 'replace'; // setting `undefined` array elements is a `replace` op
}
}
operation.value = newValue;
}
const reflectionResult = Reflect.set(target, key, newValue);
instance.defaultCallback(operation);
return reflectionResult;
}
/**
* A callback to be used as th proxy delete trap callback.
* It updates parenthood map if needed, calls default callbacks with the changes occurred.
* @param {JSONPatcherProxy} instance JSONPatcherProxy instance
* @param {Object} target the effected object
* @param {String} key the effected property's name
*/
function deleteTrap(instance, target, key) {
if (typeof target[key] !== 'undefined') {
const parentPath = findObjectPath(instance, target);
const destinationPropKey = parentPath + '/' + escapePathComponent(key);
const revokableProxyInstance = instance.proxifiedObjectsMap.get(
target[key]
);
if (revokableProxyInstance) {
if (revokableProxyInstance.inherited) {
/*
this is an inherited proxy (an already proxified object that was moved around),
we shouldn't revoke it, because even though it was removed from path1, it is still used in path2.
And we know that because we mark moved proxies with `inherited` flag when we move them
it is a good idea to remove this flag if we come across it here, in deleteProperty trap.
We DO want to revoke the proxy if it was removed again.
*/
revokableProxyInstance.inherited = false;
} else {
instance.parenthoodMap.delete(revokableProxyInstance.originalObject);
instance.disableTrapsForProxy(revokableProxyInstance);
instance.proxifiedObjectsMap.delete(target[key]);
}
}
const reflectionResult = Reflect.deleteProperty(target, key);
instance.defaultCallback({
op: 'remove',
path: destinationPropKey
});
return reflectionResult;
}
}
/* pre-define resume and pause functions to enhance constructors performance */
function resume() {
this.defaultCallback = operation => {
this.isRecording && this.patches.push(operation);
this.userCallback && this.userCallback(operation);
};
this.isObserving = true;
}
function pause() {
this.defaultCallback = () => {};
this.isObserving = false;
}
/**
* Creates an instance of JSONPatcherProxy around your object of interest `root`.
* @param {Object|Array} root - the object you want to wrap
* @param {Boolean} [showDetachedWarning = true] - whether to log a warning when a detached sub-object is modified @see {@link https://github.com/Palindrom/JSONPatcherProxy#detached-objects}
* @returns {JSONPatcherProxy}
* @constructor
*/
function JSONPatcherProxy(root, showDetachedWarning) {
this.isProxifyingTreeNow = false;
this.isObserving = false;
this.proxifiedObjectsMap = new Map();
this.parenthoodMap = new Map();
// default to true
if (typeof showDetachedWarning !== 'boolean') {
showDetachedWarning = true;
}
this.showDetachedWarning = showDetachedWarning;
this.originalObject = root;
this.cachedProxy = null;
this.isRecording = false;
this.userCallback;
/**
* @memberof JSONPatcherProxy
* Restores callback back to the original one provided to `observe`.
*/
this.resume = resume.bind(this);
/**
* @memberof JSONPatcherProxy
* Replaces your callback with a noop function.
*/
this.pause = pause.bind(this);
}
JSONPatcherProxy.prototype.generateProxyAtPath = function(parent, obj, path) {
if (!obj) {
return obj;
}
const traps = {
set: (target, key, value, receiver) =>
setTrap(this, target, key, value, receiver),
deleteProperty: (target, key) => deleteTrap(this, target, key)
};
const revocableInstance = Proxy.revocable(obj, traps);
// cache traps object to disable them later.
revocableInstance.trapsInstance = traps;
revocableInstance.originalObject = obj;
/* keeping track of object's parent and path */
this.parenthoodMap.set(obj, { parent, path });
/* keeping track of all the proxies to be able to revoke them later */
this.proxifiedObjectsMap.set(revocableInstance.proxy, revocableInstance);
return revocableInstance.proxy;
};
// grab tree's leaves one by one, encapsulate them into a proxy and return
JSONPatcherProxy.prototype._proxifyObjectTreeRecursively = function(
parent,
root,
path
) {
for (let key in root) {
if (root.hasOwnProperty(key)) {
if (root[key] instanceof Object) {
root[key] = this._proxifyObjectTreeRecursively(
root,
root[key],
escapePathComponent(key)
);
}
}
}
return this.generateProxyAtPath(parent, root, path);
};
// this function is for aesthetic purposes
JSONPatcherProxy.prototype.proxifyObjectTree = function(root) {
/*
while proxyifying object tree,
the proxyifying operation itself is being
recorded, which in an unwanted behavior,
that's why we disable recording through this
initial process;
*/
this.pause();
this.isProxifyingTreeNow = true;
const proxifiedObject = this._proxifyObjectTreeRecursively(
undefined,
root,
''
);
/* OK you can record now */
this.isProxifyingTreeNow = false;
this.resume();
return proxifiedObject;
};
/**
* Turns a proxified object into a forward-proxy object; doesn't emit any patches anymore, like a normal object
* @param {Proxy} proxy - The target proxy object
*/
JSONPatcherProxy.prototype.disableTrapsForProxy = function(
revokableProxyInstance
) {
if (this.showDetachedWarning) {
const message =
"You're accessing an object that is detached from the observedObject tree, see https://github.com/Palindrom/JSONPatcherProxy#detached-objects";
revokableProxyInstance.trapsInstance.set = (
targetObject,
propKey,
newValue
) => {
console.warn(message);
return Reflect.set(targetObject, propKey, newValue);
};
revokableProxyInstance.trapsInstance.set = (
targetObject,
propKey,
newValue
) => {
console.warn(message);
return Reflect.set(targetObject, propKey, newValue);
};
revokableProxyInstance.trapsInstance.deleteProperty = (
targetObject,
propKey
) => {
return Reflect.deleteProperty(targetObject, propKey);
};
} else {
delete revokableProxyInstance.trapsInstance.set;
delete revokableProxyInstance.trapsInstance.get;
delete revokableProxyInstance.trapsInstance.deleteProperty;
}
};
/**
* Proxifies the object that was passed in the constructor and returns a proxified mirror of it. Even though both parameters are options. You need to pass at least one of them.
* @param {Boolean} [record] - whether to record object changes to a later-retrievable patches array.
* @param {Function} [callback] - this will be synchronously called with every object change with a single `patch` as the only parameter.
*/
JSONPatcherProxy.prototype.observe = function(record, callback) {
if (!record && !callback) {
throw new Error('You need to either record changes or pass a callback');
}
this.isRecording = record;
this.userCallback = callback;
/*
I moved it here to remove it from `unobserve`,
this will also make the constructor faster, why initiate
the array before they decide to actually observe with recording?
They might need to use only a callback.
*/
if (record) this.patches = [];
this.cachedProxy = this.proxifyObjectTree(this.originalObject);
return this.cachedProxy;
};
/**
* If the observed is set to record, it will synchronously return all the patches and empties patches array.
*/
JSONPatcherProxy.prototype.generate = function() {
if (!this.isRecording) {
throw new Error('You should set record to true to get patches later');
}
return this.patches.splice(0, this.patches.length);
};
/**
* Revokes all proxies rendering the observed object useless and good for garbage collection @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/revocable}
*/
JSONPatcherProxy.prototype.revoke = function() {
this.proxifiedObjectsMap.forEach(el => {
el.revoke();
});
};
/**
* Disables all proxies' traps, turning the observed object into a forward-proxy object, like a normal object that you can modify silently.
*/
JSONPatcherProxy.prototype.disableTraps = function() {
this.proxifiedObjectsMap.forEach(this.disableTrapsForProxy, this);
};
return JSONPatcherProxy;
})();
if (typeof module !== 'undefined') {
module.exports = JSONPatcherProxy;
module.exports.default = JSONPatcherProxy;
}

19
packages/mp-mvvm/utils/util.js Executable file
View File

@ -0,0 +1,19 @@
const formatTime = date => {
const year = date.getFullYear()
const month = date.getMonth() + 1
const day = date.getDate()
const hour = date.getHours()
const minute = date.getMinutes()
const second = date.getSeconds()
return [year, month, day].map(formatNumber).join('/') + ' ' + [hour, minute, second].map(formatNumber).join(':')
}
const formatNumber = n => {
n = n.toString()
return n[1] ? n : '0' + n
}
module.exports = {
formatTime: formatTime
}

View File

@ -0,0 +1,45 @@
import mapping from '../utils/mapping'
import todo from '../model/todo/index'
class TodoViewModel {
constructor() {
this.data = {
items: []
}
}
update() {
//will automatically update the view!!!
mapping.auto(todo, this.data)
}
complete(id) {
todo.complete(id)
this.update()
}
uncomplete(id) {
todo.uncomplete(id)
this.update()
}
toogleComplete(id){
todo.toogleComplete(id)
this.update()
}
add(text) {
todo.add(text)
this.update()
}
getAll() {
todo.getAll(() => {
this.update()
})
}
}
const vd = new TodoViewModel()
export default vd