Categories
Vue

vue-good-table — Events

Creating tables from scratch is a pain.

This is why there are many table plugins to let us add tables easily.

In this article, we’ll look at how to add tables to a Vue app with the vue-good-table plugin.

Table Events

Tables created with the vue-good-table component emits various kinds of events.

We can listen to them to run what we want when they’re emitted.

One of them is the on-row-click event.

To listen to it, we can write:

<template>
  <div>
    <vue-good-table :columns="columns" :rows="rows" @on-row-click="onRowClick"/>
  </div>
</template>

<script>
export default {
  data() {
    return {
      columns: [
        {
          label: "Name",
          field: "name"
        },
        {
          label: "Age",
          field: "age",
          type: "number"
        },
        {
          label: "Birthday",
          field: "birthDay",
          type: "date",
          dateInputFormat: "yyyy-MM-dd",
          dateOutputFormat: "MMM do yy"
        }
      ],
      rows: [
        { id: 1, name: "John", age: 20, birthDay: "2000-01-20" },
        { id: 2, name: "Jane", age: 24, birthDay: "1996-10-31" },
        { id: 3, name: "Susan", age: 16, birthDay: "2004-10-30" }
      ]
    };
  },
  methods: {
    onRowClick(params) {
      console.log(params);
    }
  }
};
</script>

We listen to the on-row-click event with the onRowClick method.

The params parameter is an object with various properties.

It has the row , pageIndex , selected , and event properties.

row is an object with a row.

pageIndex has the index of the row.

selected is a boolean to indicate whether the table row is selected.

event has the MouseEvent object that’s created from clicking the row.

We can detect the double clicks on a row with the on-row-dblclick event.

For example, we can listen to it by writing:

<template>
  <div>
    <vue-good-table :columns="columns" :rows="rows" @on-row-dblclick="onRowDoubleClick"/>
  </div>
</template>

<script>
export default {
  data() {
    return {
      columns: [
        {
          label: "Name",
          field: "name"
        },
        {
          label: "Age",
          field: "age",
          type: "number"
        },
        {
          label: "Birthday",
          field: "birthDay",
          type: "date",
          dateInputFormat: "yyyy-MM-dd",
          dateOutputFormat: "MMM do yy"
        }
      ],
      rows: [
        { id: 1, name: "John", age: 20, birthDay: "2000-01-20" },
        { id: 2, name: "Jane", age: 24, birthDay: "1996-10-31" },
        { id: 3, name: "Susan", age: 16, birthDay: "2004-10-30" }
      ]
    };
  },
  methods: {
    onRowDoubleClick(params) {
      console.log(params);
    }
  }
};
</script>

params has the same properties as the one in onRowClick method.

We can detect cell clicks by listening to the on-cell-click event.

For instance, we can listen to it by writing:

<template>
  <div>
    <vue-good-table :columns="columns" :rows="rows" @on-cell-click="onCellClick"/>
  </div>
</template>

<script>
export default {
  data() {
    return {
      columns: [
        {
          label: "Name",
          field: "name"
        },
        {
          label: "Age",
          field: "age",
          type: "number"
        },
        {
          label: "Birthday",
          field: "birthDay",
          type: "date",
          dateInputFormat: "yyyy-MM-dd",
          dateOutputFormat: "MMM do yy"
        }
      ],
      rows: [
        { id: 1, name: "John", age: 20, birthDay: "2000-01-20" },
        { id: 2, name: "Jane", age: 24, birthDay: "1996-10-31" },
        { id: 3, name: "Susan", age: 16, birthDay: "2004-10-30" }
      ]
    };
  },
  methods: {
    onCellClick(params) {
      console.log(params);
    }
  }
};
</script>

The params object is slightly different from the ones in the other handler methods.

It has the row , column , rowIndex , and event properties.

row and event are the same as the other params objects.

column is the column that’s clicked.

rowIndex has the index of the row that’s clicked.

Conclusion

We can listen to various events triggered on the table with the vue-good-table plugin.

Categories
Vue

Add Tables in a Vue App with the vue-good-table Plugin

Creating tables from scratch is a pain.

This is why there are many table plugins to let us add tables easily.

In this article, we’ll look at how to add tables to a Vue app with the vue-good-table plugin.

Getting Started

We can get started by installing the package.

To do this, we run:

npm install --save vue-good-table

Then we can add the plugin to main.js by writing:

import Vue from "vue";
import App from "./App.vue";
import VueGoodTablePlugin from "vue-good-table";
import "vue-good-table/dist/vue-good-table.css";

Vue.use(VueGoodTablePlugin);

Vue.config.productionTip = false;

new Vue({
  render: (h) => h(App)
}).$mount("#app");

to add the styles and the plugin.

We can also import it into our component with:

import 'vue-good-table/dist/vue-good-table.css'
import { VueGoodTable } from 'vue-good-table';

//...
components: {
  VueGoodTable,
}

Now we can add a simple table by writing:

<template>
  <div>
    <vue-good-table :columns="columns" :rows="rows"/>
  </div>
</template>

<script>
export default {
  data() {
    return {
      columns: [
        {
          label: "Name",
          field: "name"
        },
        {
          label: "Age",
          field: "age",
          type: "number"
        },
        {
          label: "Birthday",
          field: "birthDay",
          type: "date",
          dateInputFormat: "yyyy-MM-dd",
          dateOutputFormat: "MMM do yy"
        }
      ],
      rows: [
        { id: 1, name: "John", age: 20, birthDay: "2000-01-20" },
        { id: 2, name: "Jane", age: 24, birthDay: "1996-10-31" },
        { id: 3, name: "Susan", age: 16, birthDay: "2004-10-30" }
      ]
    };
  }
};
</script>

We add the column definitions with the columns pro[.

label is the heading that’s displayed.

field is the [property name of the field to display.

dateInputFormat is the format of the date in the table row objects.

And dateOutputFormat is the date output format that’s displayed.

The rows prop has an array of objects which are displayed as rows.

Table Options

We can change many options with vue-good-table.

One of them is the max height.

This can be changed with the max-height prop:

<template>
  <div>
    <vue-good-table :columns="columns" :rows="rows" max-height="300px"/>
  </div>
</template>

<script>
export default {
  data() {
    return {
      columns: [
        {
          label: "Name",
          field: "name"
        },
        {
          label: "Age",
          field: "age",
          type: "number"
        },
        {
          label: "Birthday",
          field: "birthDay",
          type: "date",
          dateInputFormat: "yyyy-MM-dd",
          dateOutputFormat: "MMM do yy"
        }
      ],
      rows: [
        { id: 1, name: "John", age: 20, birthDay: "2000-01-20" },
        { id: 2, name: "Jane", age: 24, birthDay: "1996-10-31" },
        { id: 3, name: "Susan", age: 16, birthDay: "2004-10-30" }
      ]
    };
  }
};
</script>

We can keep the header fixed with the fixed-header prop:

<template>
  <div>
    <vue-good-table :columns="columns" :rows="rows" fixed-header/>
  </div>
</template>

<script>
export default {
  data() {
    return {
      columns: [
        {
          label: "Name",
          field: "name"
        },
        {
          label: "Age",
          field: "age",
          type: "number"
        },
        {
          label: "Birthday",
          field: "birthDay",
          type: "date",
          dateInputFormat: "yyyy-MM-dd",
          dateOutputFormat: "MMM do yy"
        }
      ],
      rows: [
        { id: 1, name: "John", age: 20, birthDay: "2000-01-20" },
        { id: 2, name: "Jane", age: 24, birthDay: "2011-10-31" },
        { id: 3, name: "Susan", age: 16, birthDay: "2011-10-30" }
      ]
    };
  }
};
</script>

Conclusion

We can add tables easily into a Vue app with the vue-good-table plugin.

Categories
Mobx

Reacting to Observables with MobX-React

We can use MobX with MobX-React to manage the state by watching MobX observables and changing the observable data.

In this article, we’ll look at how to create an Observable and use it in React components directly.

Creating Observables and Using Them in React Components

We have to install the mobx and mobx-react packages to create Observables and use them within React components.

To do this, we can write the following:

npm i mobx mobx-react

Then we can create our Observable object and a React component that uses it as follows:

import React from "react";
import ReactDOM from "react-dom";
import { observer } from "mobx-react";
import { observable } from "mobx";

const countData = observable({
  count: 0
});

const App = observer(({ countData }) => (
  <>
    <button onClick={() => countData.count++}>Increment</button>
    <p>{countData.count}</p>
  </>
));
const rootElement = document.getElementById("root");
ReactDOM.render(<App countData={countData} />, rootElement);

In the code above, we created the countData Observable object by writing:

const countData = observable({
  count: 0
});

We’ll be able to get the latest state within our React component and then pass it in as a prop for our React component.

To make our React component watch for the latest value from our Observable object and let us change its value from within the component, we write:

const App = observer(({ countData }) => (
  <>
    <button onClick={() => countData.count++}>Increment</button>
    <p>{countData.count}</p>
  </>
));
const rootElement = document.getElementById("root");
ReactDOM.render(<App countData={countData} />, rootElement);

The code above gets the countData prop from the countData Observable. Then in the onClick handler, we just pass in the function to change its value.

Then when we reference the App component in the last line, we just pass in the countData Observable object as the value of the countData prop.

countData.count++ will change the state of the Observable, which then the latest value will be obtained from the prop.

We can use the countData Observable object with class components as follows:

import React from "react";
import ReactDOM from "react-dom";
import { observer } from "mobx-react";
import { observable } from "mobx";

const countData = observable({
  count: 0
});

@observer
class App extends React.Component {
  render() {
    return (
      <>
        <button onClick={() => countData.count++}>Increment</button>
        <p>{countData.count}</p>
      </>
    );
  }
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App countData={countData} />, rootElement);

The only difference is that it’s a class component with a render method and that we use the observer decorator instead of the observer function.

Using Context to Pass Observables Around

We can use the React Context API to pass the values of an Observable to another component.

For instance, we can write the following code:

import React, { useContext } from "react";
import ReactDOM from "react-dom";
import { observer } from "mobx-react";
import { observable } from "mobx";

const countData = observable({
  count: 0
});

const CountContext = React.createContext();

const Counter = observer(() => {
  const countData = useContext(CountContext);
  return (
    <>
      <button onClick={() => countData.count++}>Increment</button>
      <p>{countData.count}</p>
    </>
  );
});

const App = ({ countData }) => (
  <CountContext.Provider value={countData}>
    <Counter />
  </CountContext.Provider>
);

const rootElement = document.getElementById("root");
ReactDOM.render(<App countData={countData} />, rootElement);

In the code above, we have the CountContext . We create it by writing:

const CountContext = React.createContext();

Then in App , we included the CountContext component, so that we can get the countData Observable value by passing in the countData prop with the value set to the countData Observable.

In the Counter component, we call the observer function with a function component inside.

Inside the component, we use the useContext hook to get the value from CountContext .

Then in the handler function, we passed into the onClick prop of the button, we changed the count ‘s value, which will automatically change the value in the countData Observable object and set as the value of countData.count .

Therefore, when we click the Increment button, the number below it will go up.

Storing Observables in Local Component State

We can also use Observables as a local state in a React component.

To do this, we can use the useState hook by passing in a function that returns an Observable object as follows:

import React, { useState } from "react";
import ReactDOM from "react-dom";
import { observer } from "mobx-react";
import { observable } from "mobx";

const App = observer(() => {
  const [countData] = useState(() =>
    observable({
      count: 0
    })
  );

  return (
    <>
      <button onClick={() => countData.count++}>Increment</button>
      <p>{countData.count}</p>
    </>
  );
});
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

In the code above, we used the useState hook by passing in a function that returns an Observable object with the count property. We then assigned that to the countData variable.

Once we did that, countData.count will be updated by the onClick handler and when we click the Increment button, we’ll see the countData.count value update automatically.

If we click the Increment button, then the value will go up.

We don’t really need to use MobX Observable objects to store local state unless complex computations are involved, since MobX will optimize for those.

Likewise, we can use MobX to store local state with class components as follows:

import React, { useState } from "react";
import ReactDOM from "react-dom";
import { observer } from "mobx-react";
import { observable } from "mobx";

@observer
class App extends React.Component {
  @observable count = 0;

  render() {
    return (
      <>
        <button onClick={() => this.count++}>Increment</button>
        <p>{this.count}</p>
      </>
    );
  }
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

In the code above, we have this.count , which is an Observable field. In the onClick listener, we just modify this.count directly, and then it’ll be reflected in the this.count in between the p tag.

@observer implements memo orshouldComponentUpdate automatically so that there won’t be any unnecessary re-renders.

Conclusion

We can use MobX Observable to store a React app’s state or a React component’s state.

To use it for storing React app’s state, we can either pass an Observable to a component as a prop or we can use the Context API to send the data to different components.

We can also use it to store local state by either using the useState hook in function components and using the observable decorator within a class component.

Categories
Mobx

Watching MobX Observables with Intercept and Observe

We can watch MobX detect and modify mutations with the intercept function and watches observable value changes with the observe function.

In this article, we’ll look at how to use intercept to monitor changes and detect and modify mutations before they’re applied to an observable and use observe to watch for changes.

Intercept

We can call the intercept function as follows:

intercept(target, propertyName?, interceptor)

In the signature above, target is the observable to guard, propertyName is an optional parameter to specify specific properties to intercept.

intercept(user.name, interceptor) is different from intercept(user, "name", interceptor) . The first tries to add an interceptor to the current value inside user.name and the latter intercept changes to the name property of user .

The interceptor is a callback that’ll be run for each change that’s made to the observable.

The intercept function tells MobX what needs to happen with the current change.

It should do one of the following things;

  • Return the received change object as-is from the function
  • Modify the change object and return it
  • Return null to indicate the change shouldn’t be applied
  • Throw an exception

intercept returns a disposer function that can be used to cancel the interceptor when it’s invoked.

It’s possible to register multiple interceptors to the same observable. They’ll be chained in the registration order.

If one of the interceptors returns null or throw an exception, then the other interceptors won’t be evaluated anymore.

It’s also possible to register an interceptor both on a parent object and an individual property.

For example, we can use it as follows:

import { observable, intercept } from "mobx";

const theme = observable({
  color: "#ffffff"
});

const disposer = intercept(theme, "color", change => {
  if (!change.newValue) {
    return null;
  }
  if (change.newValue.length === 6) {
    change.newValue = `#${change.newValue}`;
    return change;
  }
  if (change.newValue.length === 7) {
    return change;
  }
  if (change.newValue.length > 10) {
    disposer();
  }
  throw new Error(`Invalid color`);
});

In the code above, we defined an interceptor for the color property of the theme observable object.

The interceptor callback takes a change parameter with the value passed in. Then it runs through some checks and makes some changes to the newValue property.

Observe

The observe function is used to watch for changes of an observable.

It can be called as follows:

observe(target, propertyName?, listener, invokeImmediately?)

In the signature above, the target is observable to watch.

propertyName is an optional parameter to specify a property to observe. observe(user.name, listener) is different from observe(user, "name", listener) . The first observes the current value in user.name and the latter observes the name property of user.

listener is a callback that’ll be invoked for each change that’s made to the observable. It receives a single change object describing the mutation except for boxed observables, which will invoke a listener with the newValue and oldValue parameters.

invokeImmediately is a boolean that’s false by default. Set it to true if we want observe to invoke listener directly with the state of the observable instead of waiting for the first change.

For example, we can use it as follows:

import { observable, observe } from "mobx";

const person = observable({
  firstName: "John",
  lastName: "Smith"
});

const disposer = observe(person, change => {
  console.log(
    `${change.type} ${change.name} from ${change.oldValue} to ${
      change.object[change.name]
    }`
  );
});

person.firstName = "Jane";

In the code above, we have a callback with the change object that has the type of change, name of the change, the oldValue and the change.object[change.name] to get the new value.

Events

The callbacks of intercept and observe will receive an event object which has the object for the observable triggering the event and the type string for the type of the current event.

They also have additional fields depending on the type:

Object add Event

add event gives us the name and newValue properties for the name of the property being added and the new value being assigned respectively.

Object update Event

update event gives us the name and newValue properties for the name of the property being added and the new value being assigned respectively.

In addition, it gives us the oldValue property for the value that’s being replaced.

Array splice Event

This gives us index for the starting index of the splice. Splices are also fired by other array methods like push etc.

removedCount gives us the amount of items being removed.

added gives us an array of items being added.

removed gives us an array with items being added.

addedCount gives us the number of items that were added.

Array update Event

The update event gives us index which has the index of the single entry that’s being updated

newValue gives us the value that’s being assigned.

oldVale has the old value that’s being replaced.

Map add and delete Events

The Map add and delete events has the name and newValue properties for the name of the property being added and the new value being assigned respectively.

Map update Event

The delete event gives us the properties that are sent with add and delete events and also has the oldValue with the value that’s being replaced.

Boxed & computed observables create Events

The create event for boxed and computed observables has the newValue property to get us the value that we assigned during creation.

Boxed & computed observables update Events

The update event also gives us the oldValue with the value being replaced in addition to the newValue .

Conclusion

We can use the intercept function to do something before the mutation of the observable’s state is complete.

The observe function can be used to watch for value changes in a MobX observable. It takes a callback that gives us old and new values of the state and other information.

Categories
Mobx

List of MobX Modifiers

MobX has a set of decorators to change how observable properties will behave.

In this article, we’ll look at them one by one and see how we can use them.

Modifiers

MobX comes with the following decorators that defines how observable properties will behave:

  • observable — an alias for observable.deep
  • observable.deep — the default modifier used by any observable. It clones and converts any array, map or plain object into its observable counterpart
  • observable.ref — disables automatic observable conversion and creates an observable reference instead
  • observable.shallow — can only be used with collections. Turns any assigned collection into an observable, but the values will be treated as-is
  • observable.struct — like ref but ignore new values that are structurally equal to the current value
  • computed — creates a derived property
  • computed(options) — same as computed , but sets additional options
  • computed.struct — same as computed , but only notify any of is observers when the value produced is structurally different from the previous value.
  • action — creates an action
  • action(name) — creates action and overrides the name
  • action.bound — creates action and binds this to the instance.

Decorators can be used the MobX’s decorate , observable.object , extendObservable and observable to specify how object members should behave.

observable.deep is the default behavior for any key-value pair by default and computed for getters.

For example, we can define an observable as follows:

import { observable, action } from "mobx";

const person = observable(
  {
    firstName: "John",
    lastName: "Smith",
    age: 42,

  get fullName() {
      return `${this.firstName} ${this.lastName}`;
    },

  setAge(age) {
      this.age = age;
    }
  },
  {
    setAge: action
  }
);

In the code above, we have the default decorators for all members except for setAge , which we explicitly defined as an action .

Therefore, firstName , lastName , and age are observable s and fullName is a computed field.

We can use the decorate function as follows:

import { observable, action, decorate } from "mobx";

class Person {
  firstName = "John";
  lastName = "Smith";
  age = 42;

  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  }

  setAge(age) {
    this.age = age;
  }
}

decorate(Person, {
  firstName: observable,
  lastName: observable,
  age: observable,
  setAge: action
});

In the code above, we call decorate with the class as the first argument, and then an object with the fields we want to modify as the second argument.

We set firstName , lastName and age as observables and setAge as an action.

fullName is a computed field since it’s the default option for getters.

Deep Observability

When MobX creates an observable object using observable , observable.object or extendObservable , it introduces observable properties which use the deep modifier by default.

The deep modifier recursively calls observable(newValue) for any assigned value which uses the deep modifier until it gets to the bottom level of the object.

Reference Observability

In some cases, objects don’t need to be converted into observables. For example, we don’t want to do this for immutable objects or objects that are managed by an external library.

In this case, we can use the ref modifier.

For example, we can use it as follows:

class Person {
  firstName = "John";
  lastName = "Smith";
  @observable.ref age = 42;
}

In the code above, we added the observable.ref decorator to age so that MobX will only track its reference but doesn’t try to convert its value.

With ES5 syntax, we can write the following:

import { observable, extendObservable } from "mobx";

function Person() {
  extendObservable(
    this,
    {
      name: "Joe",
      age: 42
    },
    {
      age: observable.ref
    }
  );
}

Shallow Observability

We can use the observable.shallow modifier to apply observability one level deep. We need this to create a collection of observable references.

It won’t recursively apply observability like deep .

For example, we can use it as follows:

class Books {
  @observable.shallow authors = [];
}

{ deep: false } can be passed as an option to observable , observable.object , observable.array , observable.map , and extendObservable to create shallow collections.

Conclusion

We can use modifiers to change the way MobX watches the changes in the values.

The default is that it looks at a value recursively for changes.

There’re also modifiers for computed values, shallow watch, and more.