add mp mvvm
This commit is contained in:
parent
7d2bd68e0d
commit
cbdd851695
|
@ -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)
|
|
@ -0,0 +1,6 @@
|
|||
//app.js
|
||||
App({
|
||||
onLaunch: function () {
|
||||
|
||||
}
|
||||
})
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"pages":[
|
||||
"pages/index/index"
|
||||
],
|
||||
"window":{
|
||||
"backgroundTextStyle":"light",
|
||||
"navigationBarBackgroundColor": "#fff",
|
||||
"navigationBarTitleText": "WeChat",
|
||||
"navigationBarTextStyle":"black"
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
})
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
|
@ -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>
|
|
@ -0,0 +1,8 @@
|
|||
.checkbox{
|
||||
display: block;
|
||||
}
|
||||
|
||||
.completed{
|
||||
color: #d9d9d9;
|
||||
text-decoration: line-through;
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import Todo from './todo'
|
||||
|
||||
const todo = new Todo()
|
||||
|
||||
export default todo
|
|
@ -0,0 +1,9 @@
|
|||
let id = 0
|
||||
|
||||
export default class TodoItem {
|
||||
constructor(text, completed) {
|
||||
this.id = id++
|
||||
this.text = text
|
||||
this.completed = completed || false
|
||||
}
|
||||
}
|
|
@ -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
|
||||
})
|
||||
}
|
|
@ -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()
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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:''
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
|
||||
"usingComponents": {
|
||||
"todo-list": "/components/todo-list/todo-list"
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
|
@ -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": []
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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]]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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]]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
Loading…
Reference in New Issue