Thứ Tư, 28 tháng 6, 2017

AUTOMATICALLY INSTALL PLUGINS WITH THEMES FOR WORDPRESS

Well, now that is completely possible with a new class that I have built on top of a lot of WordPress APIs. With some help from Gary Jones, the TGM_Plugin_Activation class is now available for WordPress theme authors and users everywhere!
***UPDATE***
Version 2.0 now has an API for theme authors to hook into to use the class, eliminating the need to change the code within the class. Check out the details below!

HOW IT WORKS

The TGM_Plugin_Activation class works by taking a set a key => value pairs from a multidimensional array and processes them to do one of two things:
  1. Automatically install a plugin that comes pre-packaged with the theme
  2. Automatically download the .zip file from the WordPress Plugin Repository and install it
In either case, this new class can handle multiple instances of both, which means that you can include/prompt to download as many plugins as needed!
On theme activation, users are prompted with admin notices that certain plugins are required for the theme. Users can then click the links that take them to a page where forms are prepared for them to submit.
The UI is very familiar and intuitive because it follows suit with the rest of the WordPress admin UI. Users can then hit the submit button that installs the plugin. You can even have plugins be downloaded and installed from the WordPress Plugin Repository!
The TGM_Plugin_Activation class uses the same methods as WordPress itself uses to download, install, upgrade and activate plugins.

HOW TO INCLUDE THE TGM_PLUGIN_ACTIVATION CLASS WITH YOUR THEME

Take a look at the following code (taken from the example.php file included with the class):
<?php
/**
* Include the TGM_Plugin_Activation class.
*/
require_once dirname( __FILE__ ) . '/class-tgm-plugin-activation.php';
add_action( 'tgmpa_register', 'my_theme_register_required_plugins' );
/**
* Register the required plugins for this theme.
*
* In this example, we register two plugins - one included with the TGMPA library
* and one from the .org repo.
*
* The variable passed to tgmpa_register_plugins() should be an array of plugin
* arrays.
*
* This function is hooked into tgmpa_init, which is fired within the
* TGM_Plugin_Activation class constructor.
*/
function my_theme_register_required_plugins() {
/**
* Array of plugin arrays. Required keys are name and slug.
* If the source is NOT from the .org repo, then source is also required.
*/
$plugins = array(
/** This is an example of how to include a plugin pre-packaged with a theme */
array(
'name' => 'TGM Example Plugin', // The plugin name
'slug' => 'tgm-example-plugin', // The plugin slug (typically the folder name)
'source' => get_stylesheet_directory() . '/lib/plugins/tgm-example-plugin.zip', // The plugin source
'required' => false,
),
/** This is an example of how to include a plugin from the WordPress Plugin Repository */
array(
'name' => 'Edit Howdy',
'slug' => 'edit-howdy',
),
);
/** Change this to your theme text domain, used for internationalising strings */
$theme_text_domain = 'tgmpa';
/**
* Array of configuration settings. Uncomment and amend each line as needed.
* If you want the default strings to be available under your own theme domain,
* uncomment the strings and domain.
* Some of the strings are added into a sprintf, so see the comments at the
* end of each line for what each argument will be.
*/
$config = array(
/*'domain' => $theme_text_domain, // Text domain - likely want to be the same as your theme. */
/*'default_path' => '', // Default absolute path to pre-packaged plugins */
/*'menu' => 'install-my-theme-plugins', // Menu slug */
'strings' => array(
/*'page_title' => __( 'Install Required Plugins', $theme_text_domain ), // */
/*'menu_title' => __( 'Install Plugins', $theme_text_domain ), // */
/*'instructions_install' => __( 'The %1$s plugin is required for this theme. Click on the big blue button below to install and activate %1$s.', $theme_text_domain ), // %1$s = plugin name */
/*'instructions_activate' => __( 'The %1$s is installed but currently inactive. Please go to the <a href="%2$s">plugin administration page</a> page to activate it.', $theme_text_domain ), // %1$s = plugin name, %2$s = plugins page URL */
/*'button' => __( 'Install %s Now', $theme_text_domain ), // %1$s = plugin name */
/*'installing' => __( 'Installing Plugin: %s', $theme_text_domain ), // %1$s = plugin name */
/*'oops' => __( 'Something went wrong with the plugin API.', $theme_text_domain ), // */
/*'notice_can_install' => __( 'This theme requires the %1$s plugin. <a href="%2$s"><strong>Click here to begin the installation process</strong></a>. You may be asked for FTP credentials based on your server setup.', $theme_text_domain ), // %1$s = plugin name, %2$s = TGMPA page URL */
/*'notice_cannot_install' => __( 'Sorry, but you do not have the correct permissions to install the %s plugin. Contact the administrator of this site for help on getting the plugin installed.', $theme_text_domain ), // %1$s = plugin name */
/*'notice_can_activate' => __( 'This theme requires the %1$s plugin. That plugin is currently inactive, so please go to the <a href="%2$s">plugin administration page</a> to activate it.', $theme_text_domain ), // %1$s = plugin name, %2$s = plugins page URL */
/*'notice_cannot_activate' => __( 'Sorry, but you do not have the correct permissions to activate the %s plugin. Contact the administrator of this site for help on getting the plugin activated.', $theme_text_domain ), // %1$s = plugin name */
/*'return' => __( 'Return to Required Plugins Installer', $theme_text_domain ), // */
),
);
tgmpa( $plugins, $config );
}
view rawgistfile1.php hosted with ❤ by GitHub
This may seem like a lot of code, but don’t let it scare you. Most of it is commenting to help you understand what each part does and how the API interacts with the class.
First you need to include the class for use within your functions.php file. This is done by the require_once line. You should amend the path so that it targets wherever you decide to place the class within your theme directory structure.
The crux of the API is the tgmpa_register() hook and the tgmpa() function. Your plugin information and configurations are added within a function that is added to the tgmpa_register hook. There are three variables for you to play with: $plugins, $config and $theme_text_domain.
The $plugins variables is required for you to fill out to include your plugins. The variable takes multiple arrays of arguments for your plugins. The keys ‘name’ and ‘slug’ are required for both pre-packaged and repo plugins. If you plugin is pre-packaged, the ‘source’ key is also required. The ‘required’ key is a boolean that will soon be used to determine whether your plugin is required or simply recommended. You can leave this out for now as it will not be active until a future update.
The $theme_text_domain is for localization. You set your own text domain here so that the strings used by the class can be localized along with your other theme text strings.
The $config variable allows you to set the theme text domain, the default absolute path to your pre-packaged plugins, the menu slug for the install plugins page, and an array of strings to customize admin notices and other messages to your theme users. Refer to the example.php file to learn how you can customize these for your theme.

A LOOK AT WHAT’S INSIDE

This automatic plugin installer class makes use of quite a few of WordPress’ own classes. Specifically, this class uses WP_Filesystem, Plugin_Upgrader, Plugin_Skin_Installer and WP_Error. This class also makes use of the plugins_api function as well for preparation for the plugin install and activation.
Unfortunately, none of these classes have extensive documentation (apart from WP_Error). Otto has a great tutorial on WP_Filesystem which I recommend you read get an understand of how to instantiate it. I will make a tutorial soon on how to use WP_Filesystem is correlation with other WordPress classes so you too can learn how to make applications like this one. 🙂
Questions? Comments? I’d love to have them! Please post any bugs you find or any questions that may arise when using this class because I would love to work them out. I believe this class has some great potential within the WordPress community, so please make sure you let everyone know about it!
Thomas

Thứ Ba, 14 tháng 2, 2017

Browser resize function with AngularJS

var app = angular.module('myApp',[]);

app.controller ('controllerName',function($scope, $window) {

  var appWindow = angular.element($window);

  appWindow.bind('resize', function () {
   console.log('Resized your browser')
  });
  
});

Thứ Năm, 5 tháng 1, 2017

Sử dụng dịch vụ $resource xây dựng ứng dụng CRUD trong AngularJS

Các ứng dụng sử dụng web service ngày càng phổ biến. AngularJS cung cấp dịch vụ $resource hỗ trợ tương tác với dịch vụ RESTful một cách dễ dàng. Trong bài viết này, chúng ta sẽ tìm hiểu cách thức sử dụng $resource trong ứng dụng web xây dựng bằng AngularJS.

Nội dung

$http vs $resource

Cả $http và $resource đều có thể làm việc tốt với REST. Vậy lý do là gì để chúng ta sử dụng $resource thay vì sử dụng $http? Sau khi google và theo quan điểm cá nhân thì dưới đây là các lý do được đưa ra:
  • $resource viết ít code hơn.
  • $resource cho phép chúng ta định nghĩa URL dưới dạng các template string (một chuỗi có chứa phần giữ chỗ (placeholder), ví dụ: /api/books/:id) với giá trị các tham số. $resource sẽ thay thế các placeholder bằng các giá trị tham số của các đối tượng cụ thể.
  • Việc xây dựng các service với $resource linh hoạt hơn với việc thiết lập các custom action cho các đối tượng cụ thể và có thể sử dụng nó linh hoạt trong bất kỳ controller bạn muốn.
  • $resource cho phép tạo ra một đối tượng javascript đại diện cho data model riêng biệt. Bằng cách này, với mỗi hoạt động được tính toán trên đối tượng được tạo ra thông qua $resource sẽ được thực hiện ở phía server. Ví dụ, nếu bạn muốn thực hiện update một book với $http, trước hết bạn phải lấy được đối tượng đó, sau đó thực hiện gán giá trị cho các thuộc tính rồi thực hiện save với đối tượng book đi kèm:
$http.get('http://yourserver/api/books/' + 3).success(function(book) {
 book.name = "My name updated"
 $http.put('http://yourserver/api/books/', book);
});
Với $resource, chúng ta gọi hàm $update() trên mỗi phần tử book:
BookService.get({id: 3}, function(book) {
 book.name = "My name edited";
 book.$update();
});

Sử dụng $resource

AngularJS cung cấp dịch vụ $resource thông qua module ngResource riêng biệt. Do đó, để có thể sử dụng dịch vụ này, bạn cần:
<script type="text/javascript" src="angular.js"></script>
<script type="text/javascript" src="angular-resource.js"></script>
  • Thêm nữa, trong module ứng dụng chính, bạn cần khai báo phụ thuộc vào module ngResource:
var app = angular.module('app', ['ngResource']);
  • Bơm (inject) $resource vào bất kỳ đâu (service, controller, …) bạn muốn sử dụng nó.

Các thức $resource làm việc

$resource làm việc với RESTful backend, do đó chúng ta cần có REST endpoint theo định dạng sau:

TẠO MỘT RESOURCE

Chúng ta có thể tạo một Resource bằng Resource factory: $resource(url, paramDefaults, actions, options)
Các tham số trong Resource factory:
  • url – {string} – là địa chỉ REST endpoint, đây là một URL template được tham số hóa với các tham số được bắt đầu với dấu :, chẳng hạn như /books/:bookId.
  • paramDefaults – {Object} – giả sử url template có dạng /books/:bookId và tham số {bookId: ‘12’, author: ‘Fukuzawa’} thì kết quả hiển thị trên URL là /books/12?author=Fukuzawa. Nếu giá trị của tham số có tiền tố @, ví dụ {bookId: ‘@id’} thì việc đặt tham số bookId thành @id có nghĩa là bất cứ khi nào chúng ta gọi phương thức$update() và $delete() trên một resource instance thì giá trị của :bookId sẽ được thiết lập thành thuộc tínhid của resource instance. Cách sử dụng này rất hữu ích cho các PUT request và DELETE request.
  • actions – {Object} – mặc định lời gọi hàm $resource trả về một resource class với 5 phương thức mặc định gồm get(), query(), save(), remove(), delete() (xem Đối tượng trả lại của hàm $resource). Tham số này cho phép chúng ta thêm các phuơng thức tùy chỉnh (custom method) vào resource constructor (hay resource class) với cú pháp:{ action: {method:?, params:?, isArray:?, headers:?, ...} };. Trong đó:
    • action – {string} – tên của action.
    • method – {string} – phương thức HTTP, ví như GET, POST, PUT, DELETE, JSONP, PATCH, …
    • params – {Object} – tập các tùy chọn của các tham số trước khi thực hiện action này.
    • isArray – {boolean} – quy định dữ liệu trả về cho phương thức này là Array hay Object.
    • Ngoài ra nó còn chứa đầy đủ các thuộc tính của $http.config như headers, transformRequest,transformResponse, cache, timeout, withCredentials,responseType, interceptor. Bạn có thể xem lại bài viết tìm hiểu về $http để nắm rõ cách thức sử dụng các thuộc tính đó.
  • options – {Object} – tham số này hiện tại chỉ hỗ trợ thiết lập thuộc tính stripTrailingSlashes. Mặc định, thuộc tính này được thiết lập giá trị true, nghĩa là dấu / trong URL sẽ bị bỏ qua khi truyền qua hàm $resource(), chúng ta có thể hủy bỏ tính năng này bằng cách thiết lập { stripTrailingSlashes: false }. Ví dụ:
angular.module('app', ['ngResource']).factory('Book', ['$resource', function($resource){
  return $resource('http://yourserver/books/:bookId', {bookId: '@id'},{ 
    update: {
      method: 'PUT'
    }
  }, {
   stripTrailingSlashes: false
  });
}]);

ĐỐI TƯỢNG TRẢ LẠI CỦA HÀM $RESOURCE

Default actions

Hàm Resource factory $resource() trả lại một resource class (hay Resource constructor) với 5 phương thức mặc định là:
{ 'get':  {method:'GET'},
'save':   {method:'POST'},
'query':  {method:'GET', isArray:true},
'remove': {method:'DELETE'},
'delete': {method:'DELETE'} };

Hãy xem chúng ta sử dụng hàm get(), query(), save() như thế nào trong controller:
var app = angular.module('app',['ngResource']);

app.factory('Book', function($resource){
 return $resource('http://yourserver/api/books/:id');
});
 
app.controller('ResourceController',function($scope, Book) {
  var book = Book.get({ id: $scope.id }, function() {
    console.log(book);
  }); // get() trả lại một book duy nhất
 
  var books = Book.query(function() {
    console.log(books);
  }); //query() trả về tất cả các books
 
  $scope.book = new Book(); //tạo mới một resource instance
 
  $scope.book.data = 'some data';
 
  Book.save($scope.book, function() {
    //dữ liệu đã được thêm mới, bạn có thể làm gì đó ở đây
  }); //thêm mới một book. Giả sử rằng $scope.book là một đối tượng Book
});
get() function trong kịch bản trên liên quan đến một GET request /api/books/:id. Tham số :id trong URL sẽ được thay thế với $scope.id. Tham số thứ 2 của function get() là hàm success callback, nó được thực thi khi dữ liệu từ server trả về thành công. Cần lưu ý rằng, function get() trả lại một đối tượng rỗng (empty object) và đối tượng này sẽ được tự động gắn dữ liệu khi có dữ liệu thật từ server trả về. Đây là một thủ thuật hữu ích vì thường các tài nguyên được gán vào model và sau đó sẽ được render bởi view, do đó bạn có thể thiết lập đối tượng rỗng đó vào trong $scope và hướng nó vào trong view. Khi dữ liệu thực tế đến và đối tượng đã được thiết lập sẵn trong view, dữ liệu được gắn kết vào các vị trí tương ứng và view cũng được cập nhật. Điều này có nghĩa là trong hầu hết các trường hợp, chúng ta không phải viết một hàm callback cho các action method này.
Function query() liên quan đến GET request /api/books (không có tham số :id) và trả lại một mảng rỗng. Khi dữ liệu từ server đến, mảng này đã có sẵn ở đó. Một lần nữa bạn có thể thiết lập mảng này vào trong $scope model và đề cập đến nó trong view sử dụng ng-repeat.
save() function liên quan đến POST request /api/books với tham số thứ nhất là POST body. Tham số thứ hai là hàm callback sẽ được gọi khi dữ liệu được lưu trữ thành công. $resource() function trả lại một resource class và lời gọi new Book() sẽ tạo ta một đối tượng cụ thể (resource instance), thiết lập các thuộc tính cho nó và lưu trữ đối tượng vào backend.

Các action method của Resource Class và Resource Instance

Các action method này dự kiến có 4 tham số, bao gồm: params object, data object, success function, error function.
  • HTTP GET action của Resource Class: Resource.action(paramsObj, successFn, errorFn). Ví dụ:
    • Book.get(paramsObj, successFn, errorFn);
    • Book.query(paramsObj, successFn, errorFn);
  • non-GET action của Resource Class: Resource.action(paramsObj, dataObj, successFn, errorFn). Ví dụ:
    • Book.save(paramsObj, dataObj, successFn, errorFn);
    • Book.remove(paramsObj, dataObj, successFn, errorFn);
    • Book.delete(paramsObj, dataObj, successFn, errorFn);
  • non-GET action của Resource Instance: instance.$action(paramsObj, successFn, errorFn). Ví dụ:
    • book.$save(paramsObj, successFn, errorFn);
    • book.$remove(paramsObj, successFn, errorFn);
    • book.$delete(paramsObj, successFn, errorFn);
Lưu ý rằng, các non-GET action method của resource instance bắt đầu với dấu $. Và cả class action và instance action có các chức năng tương đương:
Hàm success callback được gọi với tham số (value, responseHeaders). Hàm error callback được gọi với tham số(httpResponse)
Class actions trả lại một empty instance. Instance actions trả lại lời hứa (promise) của action.
Ngoài ra, các Resource instance và collection có thêm các thuộc tính:
  • $promise: việc thực hiện các request thông qua web service thường trả lại dữ liệu với độ trễ nhất định. Để đảm bảo dữ liệu đã sẵn sàng trước khi gán nó vào model hoặc render lên view, chúng ta có thể sử dụng promise. Ví dụ:
var Todo = $resource('/api/todó/:id');

Todo.get({id: 123}).$promise.then(function(todo) {
   // success
   $scope.todos = todos;
}, function(errResponse) {
   // fail
});

Todo.query().$promise.then(function(todos) {
   // success
   $scope.todos = todos;
}, function(errResponse) {
   // fail
});
Và cần nhớ rằng, thuộc tính $promise là một thuộc tính trên cùng giá trị được trả về ở trên, do đó ta có thể sử dụng với cách khác như sau:
var Todo = $resource('/api/todos/:id');

Todo.get({id: 123}).$promise.then(function(todo) {
   $scope.todo = todo;
});
//tương đương với cách viết sau:
var todo = Todo.get({id: 123}, function() {
   $scope.todo = todo;
});
//----------------
var todo = Todo.get({id: 123});
todo.$promise.then(function() {
   $scope.todo = todo;
});
//tương đương với cách viết sau:
Todo.get({id: 123}, function(todo) {
   $scope.todo = todo;
});
  • $resolved: ban đầu thuộc tính này có giá trị false, sau khi tương tác với server lần đầu tiên hoàn thành (dù success hay fail) nó được thiết lập giá trị true. Thuộc tính này hữu ích trong việc biết Resource đã được resolved hay chưa để thực hiện gắn kết dữ liệu (data-binding).

Unit testing

$resource được thực hiện ở tầng cao hơn $http. Nó tương tác với backend thông qua các HTTP request. Bạn có thể sử dụng $httpBackend mock để thực hiện unit test cho $resource.

Áp dụng vào ví dụ cụ thể

Trong ví dụ này, chúng ta sẽ xây dựng một ứng dụng CRUD (Create – Read – Update – Delete) Book sử dụng $resource trong AngularJS. Ví dụ được xây dựng trên Ubuntu nên việc cài đặt các công cụ sẽ được hướng dẫn trên Ubuntu. Với các hệ điều hành khác, bạn vui lòng xem hướng dẫn trong các link đính kèm công cụ.

XÂY DỰNG RESTFUL SERVICE

Trong ví dụ này, chúng ta cần tạo ra một dịch vụ web RESTful phía backend có thể trả về với các request tương ứng. Bạn có thể sử dụng bất cứ công cụ nào miễn sao có thể tạo ra RESTful resource để demo. Ở đây, tôi sử dụngdeployd – một công cụ giúp xây dựng các API đơn giản.
Để sử dụng công cụ này, bạn cần cài đặt deployd và mongodb.
Bạn chạy lệnh sau để cài đặt deployd:
$ npm install deployd -g
Sau đó, chạy lệnh dpd -V để kiểm tra phiên bản của deployd. Nếu bạn thấy phiên bản của deployd ví như 0.7.0 tức là bạn đã cài đặt thành công deployd.
Tiếp đến, bạn cần cài đặt mongodb nếu chưa có:
$ sudo apt-get install -y mongodb-org
Bạn kiểm tra phiên bản mongodb bằng lệnh mongod --version.
Tiếp đó, bạn có thể tải ví dụ cho phần Book web service theo địa chỉ cho git clone. Sau đó bạn di chuyển vào thư mục ứng dụng, tiếp đó chạy lệnh dpd -d để chạy ứng dụng và mở dashboard của ứng dụng trên trình duyệt
Khi đó, ứng dụng sẽ được mở trên trình duyệt với cổng mặc định là 2403 như hình dưới đây:
Với ví dụ đơn giản này, chúng ta tạo ra một Books Collection với các thuộc tính gồm: id, title, category, price. Bạn có thể dễ dàng thêm các bản ghi cho Books Collection. Giả sử tôi thêm 3 bản ghi, khi đó ứng dụng sẽ như sau:
Chuyển sang tab API, chúng ta sẽ thấy danh sách các API mà ứng dụng cung cấp:
Khi chạy ứng dụng phía client, ứng dụng này cần được chạy để cung cấp các REST endpoint và tương tác với client.

XÂY DỰNG ỨNG DỤNG PHÍA CLIENT BẰNG ANGULARJS

Bạn có thể tải demo tại địa chỉ git clone. Trong phần này, chúng ta sẽ sử dụng $resource để thực hiện các thao tác CRUD với Book. Dùng Terminal di chuyển vào thư mục example-angularjs-book-client. Bạn cần cài đặt các gói thư viện cần dùng cho HTTP Server:
$ git clone https://github.com/taly2808/example-angularjs-book-client.git
$ cd example-angularjs-book-client
$ npm install connect serve-static --save-dev
Sau khi npm cài đặt xong các gói thư viện trên, chạy HTTP Server bằng lệnh:
$ node server
Mở cửa sổ trình duyệt mới, nhập vào địa chỉ http://localhost:3000, ta sẽ được giao diện như sau:
Trong phần này, chúng ta tạo ra một service factory Book sử dụng $resource để request đến server và sử dụng nó trong controller để lấy dữ liệu:
var app = angular.module('bookApp', ['ngResource']);

// Book Service
app.factory('Book', ['$resource', function($resource){
  return $resource('http://localhost:2403/books/:id', {id: '@id'},{ 
    update: {
      method: 'PUT'
    }
  });
}]);

//popupService
app.service('popupService', ['$window', function($window){
  this.showPopup = function(msg) {
    return $window.confirm(msg);
  }
}])

// controller
app.controller('bookMgrCtrl', ['$scope', 'Book', 'popupService', function($scope, Book, popupService) {

  $scope.displayMode = "list";
  $scope.currentBook = null;

  $scope.listBooks = function () {
    $scope.books = Book.query();
  }

  $scope.loadEditForm = function (book) {
    $scope.currentBook = book ? Book.get({id: book.id}) : {};
    $scope.displayMode = "edit";
  }

  $scope.cancelEditForm = function () {
    $scope.currentBook = null;
    $scope.displayMode = "list";
  }

  $scope.saveEditForm = function (book) {
    if (angular.isDefined(book.id)) {
      $scope.updateBook(book);
    } else {
      $scope.createBook(book);
    }
  }

  $scope.createBook = function (book) {   
   $scope.currentBook = new Book(book);
   $scope.currentBook.$save(function(newBook){
      $scope.listBooks();
      $scope.displayMode = "list";
   });
    
  }

  $scope.updateBook = function (book) {
    $scope.currentBook.$update(function(){
      $scope.listBooks();
      $scope.displayMode = "list";
    });
  }
  
  $scope.deleteBook = function (book) {
    if (popupService.showPopup('Are you sure delete this book?')){
      book.$delete();
      $scope.listBooks();
    };
  }

  $scope.listBooks();

}]);