Related
Learning Laravel event broadcasting / Echo / Vue and playing around with this tutorial.
I keep getting 403 responses to authentication, and I suspect my lack of understanding on the channels.php routes is the issue. I am using Player model instead of User for Auth which works ok.
Event ChatMessageSend
class ChatMessageSent implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $channel;
public $player;
public $chatMessage;
/**
* Create a new event instance.
* GameChat constructor.
* #param $chatMessage
* #param $player
*/
public function __construct(ChatMessage $chatMessage, Player $player)
{
$this->channel = session()->get('chat.channel');
$this->chatMessage = $chatMessage;
$this->player = $player;
}
/**
* Get the channels the event should broadcast on.
*
* #return Channel|array
*/
public function broadcastOn()
{
return new PrivateChannel($this->channel);
}
}
Listener ChatMessageNotification (default / empty)
class ChatMessageNotification
{
/**
* ChatMessageNotification constructor.
*/
public function __construct()
{
//
}
/**
* Handle the event.
*
* #param ChatMessageSent $event
* #return void
*/
public function handle(ChatMessageSent $event)
{
//
}
}
Controller ChatController
class ChatController extends Controller
{
/**
* Send chat message
*
* #param Request $request
* #return \Illuminate\Database\Eloquent\Collection|static[]
*/
public function getMessages(Request $request)
{
return ChatMessage::with('player')
->where('progress_id', '=', session('game.progress.id'))
->orderBy('created_at', 'DESC')
->get();
}
/**
* Send chat message
*
* #param Request $request
* #return array|string
*/
public function sendMessage(Request $request)
{
$player = Auth::user();
$message = $request->input('message');
if ($message) {
$message = ChatMessage::create([
'player_id' => $player->id,
'progress_id' => session()->get('game.progress.id'),
'message' => $request->input('message')
]);
}
broadcast(new ChatMessageSent($player, $message))->toOthers();
return ['type' => 'success'];
}
}
Routes channels.php
Broadcast::channel(session()->get('chat.channel'), function ($player, $message) {
return $player->inRoom();
});
And in my Player class
/**
* A user can be in one chat channel
*/
public function inRoom()
{
if ((Auth::check()) and ($this->games()->where('progress_id', '=', session('game.progress.id'))->get())) {
return true;
}
return false;
}
When a player logs in, I store in session a chat room id which I would like to use as channel.
My vue chat instance is
Vue.component('chat-messages', require('./../generic/chat-messages.vue'));
Vue.component('chat-form', require('./../generic/chat-form.vue'));
const app = new Vue({
el: '#toolbar-chat',
data: {
messages: []
},
created() {
this.fetchMessages();
Echo.private(chat_channel)
.listen('chatmessagesent', (e) => {
this.messages.unshift({
message: e.data.message,
player: e.data.player.nickname
});
});
},
methods: {
fetchMessages() {
axios.get(chat_get_route)
.then(response => {
this.messages = response.data;
});
},
addMessage(message) {
this.messages.unshift(message);
this.$nextTick(() => {
this.$refs.toolbarChat.scrollTop = 0;
});
axios.post(chat_send_route, message)
.then(response => {
console.log(response.data);
});
}
}
});
But I keep getting
POST http://my-games.app/broadcasting/auth 403 (Forbidden)
Pusher : Couldn't get auth info from your webapp : 403
Error 403 /broadcasting/auth with Laravel version > 5.3 & Pusher, you need change your code in resources/assets/js/bootstrap.js with
window.Echo = new Echo({
broadcaster: 'pusher',
key: 'your key',
cluster: 'your cluster',
encrypted: true,
auth: {
headers: {
Authorization: 'Bearer ' + YourTokenLogin
},
},
});
And in app/Providers/BroadcastServiceProvider.php change by
Broadcast::routes()
with
Broadcast::routes(['middleware' => ['auth:api']]);
or
Broadcast::routes(['middleware' => ['jwt.auth']]); //if you use JWT
it worked for me, and hope it help you.
It may be an idea to figure out whether your routes/channels.php file is doing as you expect. Maybe add some logging to see if that route gets called at all, and that your inRoom function is returning what you expect.
In case anyone still needs an answer, this worked for me.
Add to your broadcastServiceProvider.php
Broadcast::routes([
'middleware' => 'auth:api']);
add to your channels.php
Broadcast::channel('chat', function () {
return Auth::check();
});
in laravel 5 i made a new request named ApiRequest.
class ApiRequest extends Request
{
public function authorize() {
return $this->isJson();
}
public function rules()
{
return [
//
];
}
}
As you can see i am accepting only json data. And i am receiving the json in controller like this
public function postDoitApi(ApiRequest $payload) {
$inputJson = json_decode($payload->getContent());
$name = $inputJson->name;
}
Which is working fine. I am getting data in $name. But now i need to validate the input json.
I need to set validation rule in ApiRequest for the name key like this
public function rules()
{
return [
'name' => 'required|min:6'
];
}
Help me to do this. Thanks.
Laravel validates AJAX requests the same way. Just make sure you're setting one of these request headers on your request:
'Accept': 'application/json'
'X-Requested-With': 'XMLHttpRequest'
Validating any headers can be done in clean way two steps:
Step 1: Prepare header data to request data in prepareForValidation method.
public function prepareForValidation()
{
$this->merge([
"content_type" => $this->headers->get("Content-type"),
]);
}
Step 2: Apply any validation rules that you want, (Here, you want your data exact to be application/json. so
public function rules(): array
{
return [
"content_type" => "required|in:application/json",
];
}
Complete Example looks like:
/**
* Class LoginRequest
*
* #package App\Requests
*/
class LoginRequest extends FormRequest
{
public function prepareForValidation()
{
$this->merge([
"content_type" => $this->headers->get("Content-type"),
]);
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules(): array
{
return [
"content_type" => "required|in:application/json",
];
}
}
You could use a validator method instead of rules method:
class ApiRequest extends Request
{
public function authorize() {
return $this->isJson();
}
public function validator(){
//$data = \Request::instance()->getContent();
$data = json_decode($this->instance()->getContent());
return \Validator::make($data, [
'name' => 'required|min:6'
], $this->messages(), $this->attributes());
}
//what happens if validation fails
public function validate(){
$instance = $this->getValidatorInstance();
if($this->passesAuthorization()){
$this->failedAuthorization();
}elseif(!$instance->passes()){
$this->failedValidation($instance);
}elseif( $instance->passes()){
if($this->ajax())
throw new HttpResponseException(response()->json(['success' => true]));
}
}
}
return $inputJson->toArray();
and then pass to validator
$name = ['name'=>'er'];
$rules = array('name' => 'required|min:4');
$validation = Validator::make($name,$rules);
you can put following function in your ApiRequest form request.
public function validator(){
return \Validator::make(json_decode($this->getContent(),true), $this->rules(), $this->messages(), $this->attributes());
}
I wan't to use FOSRestBundle to return json responses from api, but FOSRestBundle want's to return twig instead of json.
my controller method looks like this
/**
* #Rest\Post("/producers-from-points",name="get-producers-from-points",options={"expose"=true})
* #Rest\View(serializerGroups={"list"}, statusCode=200)
*
* #return \FOS\RestBundle\View\View
* #throws \Exception
*/
public function getProducersFromPointsAction()
{
$json = parent::getJsonFromRequest();
if($json===false){
throw new \Exception("");
}
/** #var EntityManager $em */
$em = $this->getDoctrine()->getManager();
$poss = $em->getRepository('AppBundle:PointOfSale')->getPointOfSalesByAroundPoints($json->points);
return ['pointsOfSales'=>$poss];
}
config.yml
fos_rest:
routing_loader:
default_format: json
include_format: false
param_fetcher_listener: true
body_listener: true
format_listener: false
view:
view_response_listener: 'force'
exception:
messages:
'Exception': true
Try to set the view_reponse_listener to "false" instead of "force".
Have a look here to understand better what view_reponse_listener is:
http://symfony.com/doc/current/bundles/FOSRestBundle/3-listener-support.html
I used the answer at : YUI 3 -Datatable with Paginator + Query Builder + Sort tried
to for the samples http://jsbin.com/iwijek/10 and http://jsfiddle.net/UwjUt/ which have inline json data. I wanted to parse the remote json file for which I used the .get() as per YUI 3 docs. But bit does not do anything. plz check these examples and help me in parsing the remote json file.
{ var url = "file:///e:/Documents/json-search.txt";
var modelList;
dataSource = new Y.DataSource.Get({ source: url });
// comment
// var modelList = new Y.ModelList();
modelList = new Y.ModelList.Get({
source: url });
modelList.add();
}
json-search.txt
[{
name : 'Joe Bloggs',
company : 'ABC Inc',
state : 'AZ',
cost : 100
},
{
name : 'Jane Mary',
company : 'Bits and Bobs Ltd',
state : 'TX',
cost : 1000
},
{
name : 'Paul Smith',
company : 'PS Clothing PLC',
state : 'TX',
cost :400
},
{
name : 'Jack Jones',
company : 'jackjones.com',
State : 'NY',
cost: 9999
},
{
name : 'Crazy Horse',
company : 'Wacky Places Ltd.',
state : 'MI',
cost : 632
}]
I also tried to pass the datasource to the ModelList
YUI.add(
'datatable-filter',
function(Y) {
/**
Adds support for filtering the table data by API methods
#module datatable
#submodule datatable-filter
#since 3.5.0
**/
var YLang = Y.Lang,
isBoolean = YLang.isBoolean,
isString = YLang.isString,
isArray = YLang.isArray,
isObject = YLang.isObject,
toArray = Y.Array,
sub = YLang.sub;
/**
<pre><code>
var table = new Y.DataTable({
columns: [ 'id', 'username', 'name', 'birthdate' ],
data: [ ... ],
filters: true // this will render a filter input box for every column
});
table.render('#table');
</code></pre>
Setting `filters` to `true` will enable UI filtering for all columns. To enable
UI filtering for certain columns only, set `filters` to an array of column keys,
or just add `filters: true` to the respective column configuration objects.
This uses the default setting of `filters: auto` for the DataTable instance.
<pre><code>
var table = new Y.DataTable({
columns: [
'id',
{ key: 'username', renderFilter: true },
{ key: 'name' },
{ key: 'birthdate', renderFilter: true, filter : "=123", filterFn : customFilterFn }
],
data: [ ... ]
// filters: 'auto' is the default
});
To disable UI filtering for all columns, set `filters` to `false`. This still
permits filtering via the API methods.
filter via api:
var filterBy =
{
username : "=student",
name : "%richard",
birthdate : ">01-01-1975"
}
table.set("filterBy",filterBy);
As new records are inserted into the table's `data` ModelList, they will be checked against the filter to determine if they will be rendered
The current filters are stored in the `filterBy` attribute. Assigning this value at instantiation will automatically filter your data.
Filtering is done by a simple value comparison using '=', '!=', '<', '<=', '>', '>=' on the field value.
If you need custom filtering, add a filter function in the column's `filterFn` property. // TODO...
<pre><code>
function applePiesFilter(a) {
return a.get("type") == 'apple' ? true : false;
}
var table = new Y.DataTable({
columns: [ 'id', 'username', { key: name, filterFn: nameFilter }, 'birthdate' ],
data: [ ... ],
filters: [ 'username', 'name', 'birthdate' ]
});
</code></pre>
See the user guide for more details.
#class DataTable.Filters
#for DataTable
#since 3.5.0
**/
function Filters() {}
Filters.ATTRS = {
// Which columns in the UI should suggest and respond to filtering interaction
// pass an empty array if no UI columns should show filters, but you want the
// table.filter(...) API
/**
Controls which columns can show a filtering input
Acceptable values are:
* "auto" - (default) looks for `renderFilter: true` in the column configurations
* "true" - all columns have a filter field rendered
* "false" - no UI filters are enabled
* {String[]} - array of key names to give filter fields
#attribute filters
#type {String|String[]|Boolean}
#default "auto"
#since 3.5.0
**/
filters: {
value: 'auto',
validator: '_validateFilters'
},
/**
The current filter configuration to maintain in the data.
Accepts column `key` objects with a single property the value of the filter
E.g. `{ username: '%student' }`
E.g. `[{ username : '%student' }, { age : '<40'}]
#attribute filterBy
#type {String|String[]|Object|Object[]}
#since 3.5.0
**/
filterBy: {
validator: '_validateFilterBy'
},
/**
Strings containing language for filtering tooltips.
#attribute strings
#type {Object}
#default (strings for current lang configured in the YUI instance config)
#since 3.5.0
**/
strings: {}
};
Y.mix(Filters.prototype, {
/**
Filter the data in the `data` ModelList and refresh the table with the new
order.
#method filter
#param {String|String[]|Object|Object[]} fields The field(s) to filter by
#param {Object} [payload] Extra `filter` event payload you want to send along
#return {DataTable}
#chainable
#since 3.5.0
**/
filter : function (fields, payload) {
/**
Notifies of an impending filter, either from changing the filter in the UI
header, or from a call to the `filter` method.
The requested filter is available in the `filterBy` property of the event.
The default behavior of this event sets the table's `filterBy` attribute.
#event filter
#param {String|String[]|Object|Object[]} filterBy The requested filter
#preventable _defFilterFn
**/
return this.fire('filter', Y.merge((payload || {}), {
filterBy: fields || this.get('filterBy')
}));
},
/**
Template for the row that will hold the filter inputs
#property FILTERS_HEADER_CELL_TEMPLATE
#type {HTML}
#value
**/
FILTERS_HEADER_ROW_TEMPLATE : '<tr class="{filterRowClassName}" tabindex="0"></tr>',
/**
Template for the row that will hold the filter inputs
#property FILTERS_HEADER_CELL_TEMPLATE
#type {HTML}
#value
//<select><option value="=">=</option><option value="%">%</option><option value="!=">!=</option><option value="!%">!%</option><option value=">">%gt</option><option value=">=">>=</option><option value="<"><</option><option value="<="><=</option></select>
**/
FILTERS_HEADER_CELL_TEMPLATE : '<th class="{className}" tabindex="0" rowspan="1" colspan="1" title="Filter by {colName}">' +
'<div class="{linerClass}" tabindex="0"><input type="text" data-yui3-col-key="{colKey}" class="{inputClass}"/></div></th>',
/**
Template for the row that will doesn't have filter inputs
#property FILTERS_HEADER_CELL_TEMPLATE_NONE
#type {HTML}
#value
**/
FILTERS_HEADER_CELL_TEMPLATE_NONE : '<th class="{className}" tabindex="0" rowspan="1" colspan="1" title="Filtering unavailable on this field"></th>',
//--------------------------------------------------------------------------
// Protected properties and methods
//--------------------------------------------------------------------------
/**
Filters the `data` ModelList based on the new `filterBy` configuration.
#method _afterFilterByChange
#param {EventFacade} e The `filterByChange` event
#protected
#since 3.5.0
**/
_afterFilterByChange: function (e) {
var filters;
// Can't use a setter because it's a chicken and egg problem. The
// columns need to be set up to translate, but columns are initialized
// from Core's initializer. So construction-time assignment would
// fail.
// WHAT DOES THIS MEAN??
this._setFilterBy();
if (this._filterBy.length) {
// build the filter function
this._buildFilterFn();
// get the filtered data
this._filteredData = this.data.filter( { asList : true }, this._filterFn);
} else {
this._filteredData = this.data;
}
// 'hide' the filtered rows
this._hideFilteredData();
},
/**
#description if the row is not in the filtered data hide it, otherwise show it
#method _hideFilteredData
#protected
#since 3.5.0
**/
_hideFilteredData: function () {
var i,len,clientId;
for(i=0, len = this.data.size(); this._filteredData.getById && i < len; ++i) {
clientId = this.data.item(i).get("clientId");
if(this._filteredData.getByClientId(clientId)) {
this.get("contentBox").one("tbody").one("[data-yui3-record=" + clientId + "]").setStyle("display","table-row");
} else {
this.get("contentBox").one("tbody").one("[data-yui3-record=" + clientId + "]").setStyle("display","none");
}
}
},
/**
Applies the filtering logic to the new ModelList if the `newVal` is a new
ModelList.
#method _afterFilterDataChange
#param {EventFacade} e the `dataChange` event
#protected
#since 3.5.0
**/
_afterFilterDataChange: function (e) {
// object values always trigger a change event, but we only want to
// call _initFilterFn if the value passed to the `data` attribute was a
// new ModelList, not a set of new data as an array, or even the same
// ModelList.
if (e.prevVal !== e.newVal || e.newVal.hasOwnProperty('_compare')) {
// this._initFilterFn();
}
},
/**
Checks if any of the fields in the modified record are fields that are
currently being filtered by, and if so, refilters the `data` ModelList.
#method _afterFilterRecordChange
#param {EventFacade} e The Model's `change` event
#protected
#since 3.5.0
**/
_afterFilterRecordChange: function (e) {
var i, len;
for (i = 0, len = this._filterBy.length; i < len; ++i) {
if (e.changed[this._filterBy[i].key]) {
this.data.filter();
break;
}
}
},
/**
Subscribes to state changes that warrant updating the UI, and adds the
click handler for triggering the filter operation from the UI.
#method _bindFilterUI
#protected
#since 3.5.0
**/
_bindFilterUI: function () {
var handles = this._eventHandles;
// 'filterByChange' -> need to update UI
if (!handles.filterAttrs) {
handles.filterAttrs = this.after(
['filtersChange', 'columnsChange'],
Y.bind('_uiSetFilters', this));
}
if (!handles.filterUITrigger && this._theadNode) {
handles.filterUITrigger = this.delegate(['keyup','blur'],
Y.rbind('_onUITriggerFilter', this),
'.' + this.getClassName('filter', 'input'));
}
},
/**
Sets the `filterBy` attribute from the `filter` event's `e.filterBy` value.
#method _defFilterFn
#param {EventFacade} e The `filter` event
#protected
#since 3.5.0
**/
_defFilterFn: function (e) {
this.set.apply(this, ['filterBy', e.filterBy].concat(e.details));
},
/**
Sets up the initial filter state and instance properties. Publishes events
and subscribes to attribute change events to maintain internal state.
#method initializer
#protected
#since 3.5.0
**/
initializer: function () {
var boundParseFilter = Y.bind('_parseFilter', this);
this._parseFilter();
this._setFilterBy();
this._initFilterStrings();
// dataChange : Y.bind('_afterFilterDataChange', this),
//
// filterChange : boundParseFilter
this.after({
'sort' : this._hideFilteredData,
'columnsChange' : boundParseFilter,
'filterByChange' : Y.bind('_afterFilterByChange', this),
'table:renderHeader': Y.bind('_renderFilters', this)});
// this.data.after(this.data.model.NAME + ":change",
// Y.bind('_afterFilterRecordChange', this));
// TODO: this event needs magic, allowing async remote filtering
// this.publish('filter', {
// defaultFn: Y.bind('_defFilterFn', this)
// });
},
/**
Add the filter related strings to the `strings` map.
#method _initFilterStrings
#protected
#since 3.5.0
**/
_initFilterStrings: function () {
// Not a valueFn because other class extensions will want to add to it
this.set('strings', Y.mix((this.get('strings') || {}),
Y.Intl.get('datatable-filter')));
},
/**
#description Fires the `filter` event in response to user changing the UI filters
#method _onUITriggerFilter
#param {DOMEventFacade} e The `mouseout` event
#protected
#since 3.5.0
**/
_onUITriggerFilter: function (e) {
var colKey = e.currentTarget.getAttribute('data-yui3-col-key'),
column = colKey && this.getColumn(colKey),
filterBy = this.get("filterBy") || {},
i, len;
e.halt(); // not doing anything anyway?
if (column) {
filterBy[colKey] = e.currentTarget.get("value");
}
this.set("filterBy",filterBy);
},
/**
#description Build the filter function from the column config, this function is passed to the model list fiter() method
#method _buildFilterFn
#protected
#since 3.5.0
**/
_buildFilterFn: function () {
var i,len,op1,op2, key, filter, filterFn;
filterFn = function(model,index,modelList) {
var key,filter,op1,op2,val,filter,passesFilter = true;
for(i=0,len = this._filterBy.length; i< len && passesFilter; ++i) {
key = this._filterBy[i].key;
filter = this._filterBy[i].filter;
val = model.get(key) || '';
op1 = filter.substr(0,1);
op2 = filter.substr(1,1);
if(op2 == '=') {
switch(op1) {
case '!':
// not equal
if(val.toLowerCase() == filter.substr(2).toLowerCase()) {
passesFilter = false;
}
break;
case '>':
// greater or equal
if(parseInt(val) < parseInt(filter.substr(2))) {
passesFilter = false;
}
break;
case '<':
// less than or equal
if(parseInt(val) > parseInt(filter.substr(2))) {
passesFilter = false;
}
break;
}
} else if (op2 == '%' && op1 =='!') {
// not like
if((val.toLowerCase().indexOf(filter.substr(2).toLowerCase()) > -1)) {
passesFilter = false;
}
break;
} else {
switch(op1) {
case '=':
// equal
if(val.toLowerCase() != filter.substr(1).toLowerCase()) {
passesFilter = false;
}
break;
case '>':
// greater than
if(parseInt(val) <= parseInt(filter.substr(1))) {
passesFilter = false;
}
break;
case '<':
// less than
if(parseInt(val) >= parseInt(filter.substr(1))) {
passesFilter = false;
}
break;
case '%':
// like
if((val.toLowerCase().indexOf(filter.substr(1).toLowerCase()) === -1)) {
passesFilter = false;
}
break;
default:
// consider to be like
if((String(val).toLowerCase().indexOf(String(filter).toLowerCase()) === -1)) {
passesFilter = false;
}
break;
}
}
}
return passesFilter;
};
this._filterFn = Y.bind(filterFn,this);
},
/**
#description Normalizes the possible input values for the `filter` attribute setting up the column config as appropriate
#method _parseFilter
#protected
#since 3.5.0
**/
_parseFilter: function () {
var filters = this.get('filters'),
columns = [],
i, len, col;
col = this._displayColumns.slice();
this._uiFilters = false;
if(filters === 'auto') {
// look for key on columns
col = this._displayColumns.slice();
for(i = 0; i < col.length; i++) {
if(col[i].renderFilter) {
this._uiFilters = true;
}
}
} else if(filters === true) {
// provide UI filters for all cols
col = this._displayColumns.slice();
for(i = 0; i < col.length; i++) {
this._uiFilters = true;
this._displayColumns[i].renderFilter = true;
}
} else if (isArray(filters)) {
// provide UI filters on the specified cols (plural)
for (i = 0, len=filters.length; i < len; ++i) {
if(col = this.getColumn(filters[i])) {
this._uiFilters = true;
col.renderFilter = true;
}
}
} else if (filters) {
// provide UI filter on the specifed 'COL' (singular)
for (i = 0, len = col.length; i < len; ++i) {
if (col[i].key === filters) {
this._uiFilters = true;
this._displayColumns[i].renderFilter = true;
i = len;
}
}
}
},
/**
Initial application of the filters UI.
#method _renderFilters
#protected
#since 3.5.0
**/
_renderFilters: function () {
this._uiSetFilters();
this._bindFilterUI();
},
/**
#description Parses the current `filterBy` attribute and updates the columns
#method _setFilterBy
#protected
#since 3.5.0
**/
_setFilterBy: function () {
var columns = this._displayColumns,
filterBy = this.get('filterBy') || {},
filteredClass = ' ' + this.getClassName('filtered'),
i, len, name, dir, field, column;
this._filterBy = [];
// Purge current filter state from column configs
for (i = 0, len = columns.length; i < len; ++i) {
column = columns[i];
delete columns[i].filter;
if (column.className) {
// TODO: be more thorough
column.className = column.className.replace(filteredClass, '');
}
}
for (key in filterBy) {
if(filterBy[key]!='') {
// Allow filtering of any model field and any column
column = this.getColumn(key) || { _id: key, key: key };
if (column) {
column.filter = filterBy[key];
if (!column.className) {
column.className = '';
}
column.className += filteredClass;
this._filterBy.push(column);
}
}
}
},
/**
Array of column configuration objects of those columns that need UI setup
for user interaction.
#property _filters
#type {Object[]}
#protected
#since 3.5.0
**/
//_filters: null,
/**
Array of column configuration objects for those columns that are currently
being used to filter the data. Fake column objects are used for fields that
are not rendered as columns.
#property _filterBy
#type {Object[]}
#protected
#since 3.5.0
**/
//_filterBy: null,
/**
Replacement `comparator` for the `data` ModelList that defers filtering logic
to the `_compare` method. The deferral is accomplished by returning `this`.
#method _filterComparator
#param {Model} item The record being evaluated for filter position
#return {Model} The record
#protected
#since 3.5.0
**/
_filterComparator: function (item) {
// Defer filtering to ModelList's _compare
return item;
},
/**
Applies the appropriate classes to the `boundingBox` and column headers to
indicate filter state and filterability.
Also currently wraps the header content of filters columns in a `<div>`
liner to give a CSS anchor for filter indicators.
#method _uiSetFilters
#protected
#since 3.5.0
**/
_uiSetFilters: function () {
var columns = this._displayColumns.slice(),
filtersClass = this.getClassName('filters', 'column'),
filtersHidden = this.getClassName("filters","hidden"),
filterRowClass = this.getClassName("filters","row"),
filteredClass = this.getClassName('filtered'),
linerClass = this.getClassName('filter', 'liner'),
i, len, col, node, liner, title, desc;
this.get('boundingBox').toggleClass(
this.getClassName('filters'),
columns.length);
/// NEED TO ADDRESS
if((node = this._theadNode.one("." + filterRowClass))) {
node.remove(true);
}
if(columns.length>0 && this._uiFilters) {
tr = this._theadNode.appendChild(Y.Lang.sub(
this.FILTERS_HEADER_ROW_TEMPLATE, {
filterRowClassName: filterRowClass }));
for (i = 0, len = columns.length; i < len; ++i) {
if(columns[i].renderFilter) {
tr.append(Y.Lang.sub(
this.FILTERS_HEADER_CELL_TEMPLATE, {
className: this.getClassName("filter","cell"),
colKey : columns[i].key,
colName : columns[i].label || columns[i].key,
inputClass : this.getClassName("filter","input"),
linerClass: linerClass
}));
} else {
tr.append(Y.Lang.sub(
this.FILTERS_HEADER_CELL_TEMPLATE_NONE, {
className: this.getClassName("no-filter")
}));
}
}
}
},
/**
Allows values `true`, `false`, "auto", or arrays of column names through.
#method _validateFilters
#param {Any} val The input value to `set("filters", VAL)`
#return {Boolean}
#protected
#since 3.5.0
**/
_validateFilters: function (val) {
return val === 'auto' || isBoolean(val) || isArray(val);
},
/**
Allows strings, arrays of strings, objects, or arrays of objects.
#method _validateFilterBy
#param {String|String[]|Object|Object[]} val The new `filterBy` value
#return {Boolean}
#protected
#since 3.5.0
**/
_validateFilterBy: function (val) {
return val;
return val === null ||
isString(val) ||
isObject(val, true) ||
(isArray(val) && (isString(val[0]) || isObject(val, true)));
}
}, true);
Y.DataTable.Filters = Filters;
Y.Base.mix(Y.DataTable, [Filters]);
},"0.1", {
requires : []});
YUI().use('datatable-sort','datatable-filter',function(Y) {
// comment
//var modelList = new Y.ModelList();
var url = "file:///e:/Documents/json-search.txt";
var modelList, datasource;
modelList = new Y.ModelList();
datasource = Y.DataSource.IO({ source: "file:///e:/Documents/json-search.txt" });
modelList.plug(Y.Plugin.ModelListDataSource, {
source : datasource, });
datasource.sendRequest();
var dataTable = new Y.DataTable (
{
columns : [ {key:"name",label:"Person",renderFilter:true,filter:"Joe"},"company",{key:"state",renderFilter:true},{key:"cost",renderFilter:true}],
data : modelList, sortable : true, filters : 'auto'
}).render("#table");
modelList.item(1).set("id",102);
Y.one("#showhidefilters").on("click",function(e) {e.halt();Y.one("#table").one(".yui3-datatable-filters-row").toggleClass("yui3-datatable-filters-hidden");});
});
I receive a JSON object from an AJAX call to a REST server. This object has property names that match my TypeScript class (this is a follow-on to this question).
What is the best way to initialize it? I don't think this will work because the class (& JSON object) have members that are lists of objects and members that are classes, and those classes have members that are lists and/or classes.
But I'd prefer an approach that looks up the member names and assigns them across, creating lists and instantiating classes as needed, so I don't have to write explicit code for every member in every class (there's a LOT!)
These are some quick shots at this to show a few different ways. They are by no means "complete" and as a disclaimer, I don't think it's a good idea to do it like this. Also the code isn't too clean since I just typed it together rather quickly.
Also as a note: Of course deserializable classes need to have default constructors as is the case in all other languages where I'm aware of deserialization of any kind. Of course, Javascript won't complain if you call a non-default constructor with no arguments, but the class better be prepared for it then (plus, it wouldn't really be the "typescripty way").
Option #1: No run-time information at all
The problem with this approach is mostly that the name of any member must match its class. Which automatically limits you to one member of same type per class and breaks several rules of good practice. I strongly advise against this, but just list it here because it was the first "draft" when I wrote this answer (which is also why the names are "Foo" etc.).
module Environment {
export class Sub {
id: number;
}
export class Foo {
baz: number;
Sub: Sub;
}
}
function deserialize(json, environment, clazz) {
var instance = new clazz();
for(var prop in json) {
if(!json.hasOwnProperty(prop)) {
continue;
}
if(typeof json[prop] === 'object') {
instance[prop] = deserialize(json[prop], environment, environment[prop]);
} else {
instance[prop] = json[prop];
}
}
return instance;
}
var json = {
baz: 42,
Sub: {
id: 1337
}
};
var instance = deserialize(json, Environment, Environment.Foo);
console.log(instance);
Option #2: The name property
To get rid of the problem in option #1, we need to have some kind of information of what type a node in the JSON object is. The problem is that in Typescript, these things are compile-time constructs and we need them at runtime – but runtime objects simply have no awareness of their properties until they are set.
One way to do it is by making classes aware of their names. You need this property in the JSON as well, though. Actually, you only need it in the json:
module Environment {
export class Member {
private __name__ = "Member";
id: number;
}
export class ExampleClass {
private __name__ = "ExampleClass";
mainId: number;
firstMember: Member;
secondMember: Member;
}
}
function deserialize(json, environment) {
var instance = new environment[json.__name__]();
for(var prop in json) {
if(!json.hasOwnProperty(prop)) {
continue;
}
if(typeof json[prop] === 'object') {
instance[prop] = deserialize(json[prop], environment);
} else {
instance[prop] = json[prop];
}
}
return instance;
}
var json = {
__name__: "ExampleClass",
mainId: 42,
firstMember: {
__name__: "Member",
id: 1337
},
secondMember: {
__name__: "Member",
id: -1
}
};
var instance = deserialize(json, Environment);
console.log(instance);
Option #3: Explicitly stating member types
As stated above, the type information of class members is not available at runtime – that is unless we make it available. We only need to do this for non-primitive members and we are good to go:
interface Deserializable {
getTypes(): Object;
}
class Member implements Deserializable {
id: number;
getTypes() {
// since the only member, id, is primitive, we don't need to
// return anything here
return {};
}
}
class ExampleClass implements Deserializable {
mainId: number;
firstMember: Member;
secondMember: Member;
getTypes() {
return {
// this is the duplication so that we have
// run-time type information :/
firstMember: Member,
secondMember: Member
};
}
}
function deserialize(json, clazz) {
var instance = new clazz(),
types = instance.getTypes();
for(var prop in json) {
if(!json.hasOwnProperty(prop)) {
continue;
}
if(typeof json[prop] === 'object') {
instance[prop] = deserialize(json[prop], types[prop]);
} else {
instance[prop] = json[prop];
}
}
return instance;
}
var json = {
mainId: 42,
firstMember: {
id: 1337
},
secondMember: {
id: -1
}
};
var instance = deserialize(json, ExampleClass);
console.log(instance);
Option #4: The verbose, but neat way
Update 01/03/2016: As #GameAlchemist pointed out in the comments (idea, implementation), as of Typescript 1.7, the solution described below can be written in a better way using class/property decorators.
Serialization is always a problem and in my opinion, the best way is a way that just isn't the shortest. Out of all the options, this is what I'd prefer because the author of the class has full control over the state of deserialized objects. If I had to guess, I'd say that all other options, sooner or later, will get you in trouble (unless Javascript comes up with a native way for dealing with this).
Really, the following example doesn't do the flexibility justice. It really does just copy the class's structure. The difference you have to keep in mind here, though, is that the class has full control to use any kind of JSON it wants to control the state of the entire class (you could calculate things etc.).
interface Serializable<T> {
deserialize(input: Object): T;
}
class Member implements Serializable<Member> {
id: number;
deserialize(input) {
this.id = input.id;
return this;
}
}
class ExampleClass implements Serializable<ExampleClass> {
mainId: number;
firstMember: Member;
secondMember: Member;
deserialize(input) {
this.mainId = input.mainId;
this.firstMember = new Member().deserialize(input.firstMember);
this.secondMember = new Member().deserialize(input.secondMember);
return this;
}
}
var json = {
mainId: 42,
firstMember: {
id: 1337
},
secondMember: {
id: -1
}
};
var instance = new ExampleClass().deserialize(json);
console.log(instance);
you can use Object.assign I don't know when this was added, I'm currently using Typescript 2.0.2, and this appears to be an ES6 feature.
client.fetch( '' ).then( response => {
return response.json();
} ).then( json => {
let hal : HalJson = Object.assign( new HalJson(), json );
log.debug( "json", hal );
here's HalJson
export class HalJson {
_links: HalLinks;
}
export class HalLinks implements Links {
}
export interface Links {
readonly [text: string]: Link;
}
export interface Link {
readonly href: URL;
}
here's what chrome says it is
HalJson {_links: Object}
_links
:
Object
public
:
Object
href
:
"http://localhost:9000/v0/public
so you can see it doesn't do the assign recursively
TLDR: TypedJSON (working proof of concept)
The root of the complexity of this problem is that we need to deserialize JSON at runtime using type information that only exists at compile time. This requires that type-information is somehow made available at runtime.
Fortunately, this can be solved in a very elegant and robust way with decorators and ReflectDecorators:
Use property decorators on properties which are subject to serialization, to record metadata information and store that information somewhere, for example on the class prototype
Feed this metadata information to a recursive initializer (deserializer)
Recording Type-Information
With a combination of ReflectDecorators and property decorators, type information can be easily recorded about a property. A rudimentary implementation of this approach would be:
function JsonMember(target: any, propertyKey: string) {
var metadataFieldKey = "__propertyTypes__";
// Get the already recorded type-information from target, or create
// empty object if this is the first property.
var propertyTypes = target[metadataFieldKey] || (target[metadataFieldKey] = {});
// Get the constructor reference of the current property.
// This is provided by TypeScript, built-in (make sure to enable emit
// decorator metadata).
propertyTypes[propertyKey] = Reflect.getMetadata("design:type", target, propertyKey);
}
For any given property, the above snippet will add a reference of the constructor function of the property to the hidden __propertyTypes__ property on the class prototype. For example:
class Language {
#JsonMember // String
name: string;
#JsonMember// Number
level: number;
}
class Person {
#JsonMember // String
name: string;
#JsonMember// Language
language: Language;
}
And that's it, we have the required type-information at runtime, which can now be processed.
Processing Type-Information
We first need to obtain an Object instance using JSON.parse -- after that, we can iterate over the entires in __propertyTypes__ (collected above) and instantiate the required properties accordingly. The type of the root object must be specified, so that the deserializer has a starting-point.
Again, a dead simple implementation of this approach would be:
function deserialize<T>(jsonObject: any, Constructor: { new (): T }): T {
if (!Constructor || !Constructor.prototype.__propertyTypes__ || !jsonObject || typeof jsonObject !== "object") {
// No root-type with usable type-information is available.
return jsonObject;
}
// Create an instance of root-type.
var instance: any = new Constructor();
// For each property marked with #JsonMember, do...
Object.keys(Constructor.prototype.__propertyTypes__).forEach(propertyKey => {
var PropertyType = Constructor.prototype.__propertyTypes__[propertyKey];
// Deserialize recursively, treat property type as root-type.
instance[propertyKey] = deserialize(jsonObject[propertyKey], PropertyType);
});
return instance;
}
var json = '{ "name": "John Doe", "language": { "name": "en", "level": 5 } }';
var person: Person = deserialize(JSON.parse(json), Person);
The above idea has a big advantage of deserializing by expected types (for complex/object values), instead of what is present in the JSON. If a Person is expected, then it is a Person instance that is created. With some additional security measures in place for primitive types and arrays, this approach can be made secure, that resists any malicious JSON.
Edge Cases
However, if you are now happy that the solution is that simple, I have some bad news: there is a vast number of edge cases that need to be taken care of. Only some of which are:
Arrays and array elements (especially in nested arrays)
Polymorphism
Abstract classes and interfaces
...
If you don't want to fiddle around with all of these (I bet you don't), I'd be glad to recommend a working experimental version of a proof-of-concept utilizing this approach, TypedJSON -- which I created to tackle this exact problem, a problem I face myself daily.
Due to how decorators are still being considered experimental, I wouldn't recommend using it for production use, but so far it served me well.
I've created a tool that generates TypeScript interfaces and a runtime "type map" for performing runtime typechecking against the results of JSON.parse: ts.quicktype.io
For example, given this JSON:
{
"name": "David",
"pets": [
{
"name": "Smoochie",
"species": "rhino"
}
]
}
quicktype produces the following TypeScript interface and type map:
export interface Person {
name: string;
pets: Pet[];
}
export interface Pet {
name: string;
species: string;
}
const typeMap: any = {
Person: {
name: "string",
pets: array(object("Pet")),
},
Pet: {
name: "string",
species: "string",
},
};
Then we check the result of JSON.parse against the type map:
export function fromJson(json: string): Person {
return cast(JSON.parse(json), object("Person"));
}
I've left out some code, but you can try quicktype for the details.
I've been using this guy to do the job: https://github.com/weichx/cerialize
It's very simple yet powerful. It supports:
Serialization & deserialization of a whole tree of objects.
Persistent & transient properties on the same object.
Hooks to customize the (de)serialization logic.
It can (de)serialize into an existing instance (great for Angular) or generate new instances.
etc.
Example:
class Tree {
#deserialize public species : string;
#deserializeAs(Leaf) public leafs : Array<Leaf>; //arrays do not need extra specifications, just a type.
#deserializeAs(Bark, 'barkType') public bark : Bark; //using custom type and custom key name
#deserializeIndexable(Leaf) public leafMap : {[idx : string] : Leaf}; //use an object as a map
}
class Leaf {
#deserialize public color : string;
#deserialize public blooming : boolean;
#deserializeAs(Date) public bloomedAt : Date;
}
class Bark {
#deserialize roughness : number;
}
var json = {
species: 'Oak',
barkType: { roughness: 1 },
leafs: [ {color: 'red', blooming: false, bloomedAt: 'Mon Dec 07 2015 11:48:20 GMT-0500 (EST)' } ],
leafMap: { type1: { some leaf data }, type2: { some leaf data } }
}
var tree: Tree = Deserialize(json, Tree);
For simple objects, I like this method:
class Person {
constructor(
public id: String,
public name: String,
public title: String) {};
static deserialize(input:any): Person {
return new Person(input.id, input.name, input.title);
}
}
var person = Person.deserialize({id: 'P123', name: 'Bob', title: 'Mr'});
Leveraging the ability to define properties in the constructor lets it be concise.
This gets you a typed object (vs all the answers that use Object.assign or some variant, which give you an Object) and doesn't require external libraries or decorators.
This is my approach (very simple):
const jsonObj: { [key: string]: any } = JSON.parse(jsonStr);
for (const key in jsonObj) {
if (!jsonObj.hasOwnProperty(key)) {
continue;
}
console.log(key); // Key
console.log(jsonObj[key]); // Value
// Your logic...
}
if you want type safety and don't like decorators
abstract class IPerson{
name?: string;
age?: number;
}
class Person extends IPerson{
constructor({name, age}: IPerson){
super();
this.name = name;
this.age = age;
}
}
const json = {name: "ali", age: 80};
const person = new Person(json);
or this which I prefer
class Person {
constructor(init?: Partial<Person>){
Object.assign(this, init);
}
name?: string;
age?: number;
}
const json = {name: "ali", age: 80};
const person = new Person(json);
Option #5: Using Typescript constructors and jQuery.extend
This seems to be the most maintainable method: add a constructor that takes as parameter the json structure, and extend the json object. That way you can parse a json structure into the whole application model.
There is no need to create interfaces, or listing properties in constructor.
export class Company
{
Employees : Employee[];
constructor( jsonData: any )
{
jQuery.extend( this, jsonData);
// apply the same principle to linked objects:
if ( jsonData.Employees )
this.Employees = jQuery.map( jsonData.Employees , (emp) => {
return new Employee ( emp ); });
}
calculateSalaries() : void { .... }
}
export class Employee
{
name: string;
salary: number;
city: string;
constructor( jsonData: any )
{
jQuery.extend( this, jsonData);
// case where your object's property does not match the json's:
this.city = jsonData.town;
}
}
In your ajax callback where you receive a company to calculate salaries:
onReceiveCompany( jsonCompany : any )
{
let newCompany = new Company( jsonCompany );
// call the methods on your newCompany object ...
newCompany.calculateSalaries()
}
The best I found for this purpose is the class-transformer
That's how you use it:
Some class:
export class Foo {
name: string;
#Type(() => Bar)
bar: Bar;
public someFunction = (test: string): boolean => {
...
}
}
// the docs say "import [this shim] in a global place, like app.ts"
import 'reflect-metadata';
// import this function where you need to use it
import { plainToClass } from 'class-transformer';
export class SomeService {
anyFunction() {
u = plainToClass(Foo, JSONobj);
}
}
If you use the #Type decorator nested properties will be created, too.
The 4th option described above is a simple and nice way to do it, which has to be combined with the 2nd option in the case where you have to handle a class hierarchy like for instance a member list which is any of a occurences of subclasses of a Member super class, eg Director extends Member or Student extends Member. In that case you have to give the subclass type in the json format
JQuery .extend does this for you:
var mytsobject = new mytsobject();
var newObj = {a:1,b:2};
$.extend(mytsobject, newObj); //mytsobject will now contain a & b
Another option using factories
export class A {
id: number;
date: Date;
bId: number;
readonly b: B;
}
export class B {
id: number;
}
export class AFactory {
constructor(
private readonly createB: BFactory
) { }
create(data: any): A {
const createB = this.createB.create;
return Object.assign(new A(),
data,
{
get b(): B {
return createB({ id: data.bId });
},
date: new Date(data.date)
});
}
}
export class BFactory {
create(data: any): B {
return Object.assign(new B(), data);
}
}
https://github.com/MrAntix/ts-deserialize
use like this
import { A, B, AFactory, BFactory } from "./deserialize";
// create a factory, simplified by DI
const aFactory = new AFactory(new BFactory());
// get an anon js object like you'd get from the http call
const data = { bId: 1, date: '2017-1-1' };
// create a real model from the anon js object
const a = aFactory.create(data);
// confirm instances e.g. dates are Dates
console.log('a.date is instanceof Date', a.date instanceof Date);
console.log('a.b is instanceof B', a.b instanceof B);
keeps your classes simple
injection available to the factories for flexibility
I personally prefer option #3 of #Ingo Bürk.
And I improved his codes to support an array of complex data and Array of primitive data.
interface IDeserializable {
getTypes(): Object;
}
class Utility {
static deserializeJson<T>(jsonObj: object, classType: any): T {
let instanceObj = new classType();
let types: IDeserializable;
if (instanceObj && instanceObj.getTypes) {
types = instanceObj.getTypes();
}
for (var prop in jsonObj) {
if (!(prop in instanceObj)) {
continue;
}
let jsonProp = jsonObj[prop];
if (this.isObject(jsonProp)) {
instanceObj[prop] =
types && types[prop]
? this.deserializeJson(jsonProp, types[prop])
: jsonProp;
} else if (this.isArray(jsonProp)) {
instanceObj[prop] = [];
for (let index = 0; index < jsonProp.length; index++) {
const elem = jsonProp[index];
if (this.isObject(elem) && types && types[prop]) {
instanceObj[prop].push(this.deserializeJson(elem, types[prop]));
} else {
instanceObj[prop].push(elem);
}
}
} else {
instanceObj[prop] = jsonProp;
}
}
return instanceObj;
}
//#region ### get types ###
/**
* check type of value be string
* #param {*} value
*/
static isString(value: any) {
return typeof value === "string" || value instanceof String;
}
/**
* check type of value be array
* #param {*} value
*/
static isNumber(value: any) {
return typeof value === "number" && isFinite(value);
}
/**
* check type of value be array
* #param {*} value
*/
static isArray(value: any) {
return value && typeof value === "object" && value.constructor === Array;
}
/**
* check type of value be object
* #param {*} value
*/
static isObject(value: any) {
return value && typeof value === "object" && value.constructor === Object;
}
/**
* check type of value be boolean
* #param {*} value
*/
static isBoolean(value: any) {
return typeof value === "boolean";
}
//#endregion
}
// #region ### Models ###
class Hotel implements IDeserializable {
id: number = 0;
name: string = "";
address: string = "";
city: City = new City(); // complex data
roomTypes: Array<RoomType> = []; // array of complex data
facilities: Array<string> = []; // array of primitive data
// getter example
get nameAndAddress() {
return `${this.name} ${this.address}`;
}
// function example
checkRoom() {
return true;
}
// this function will be use for getting run-time type information
getTypes() {
return {
city: City,
roomTypes: RoomType
};
}
}
class RoomType implements IDeserializable {
id: number = 0;
name: string = "";
roomPrices: Array<RoomPrice> = [];
// getter example
get totalPrice() {
return this.roomPrices.map(x => x.price).reduce((a, b) => a + b, 0);
}
getTypes() {
return {
roomPrices: RoomPrice
};
}
}
class RoomPrice {
price: number = 0;
date: string = "";
}
class City {
id: number = 0;
name: string = "";
}
// #endregion
// #region ### test code ###
var jsonObj = {
id: 1,
name: "hotel1",
address: "address1",
city: {
id: 1,
name: "city1"
},
roomTypes: [
{
id: 1,
name: "single",
roomPrices: [
{
price: 1000,
date: "2020-02-20"
},
{
price: 1500,
date: "2020-02-21"
}
]
},
{
id: 2,
name: "double",
roomPrices: [
{
price: 2000,
date: "2020-02-20"
},
{
price: 2500,
date: "2020-02-21"
}
]
}
],
facilities: ["facility1", "facility2"]
};
var hotelInstance = Utility.deserializeJson<Hotel>(jsonObj, Hotel);
console.log(hotelInstance.city.name);
console.log(hotelInstance.nameAndAddress); // getter
console.log(hotelInstance.checkRoom()); // function
console.log(hotelInstance.roomTypes[0].totalPrice); // getter
// #endregion
Maybe not actual, but simple solution:
interface Bar{
x:number;
y?:string;
}
var baz:Bar = JSON.parse(jsonString);
alert(baz.y);
work for difficult dependencies too!!!
you can do like below
export interface Instance {
id?:string;
name?:string;
type:string;
}
and
var instance: Instance = <Instance>({
id: null,
name: '',
type: ''
});
My approach is slightly different. I do not copy properties into new instances, I just change the prototype of existing POJOs (may not work well on older browsers). Each class is responsible for providing a SetPrototypes method to set the prototoypes of any child objects, which in turn provide their own SetPrototypes methods.
(I also use a _Type property to get the class name of unknown objects but that can be ignored here)
class ParentClass
{
public ID?: Guid;
public Child?: ChildClass;
public ListOfChildren?: ChildClass[];
/**
* Set the prototypes of all objects in the graph.
* Used for recursive prototype assignment on a graph via ObjectUtils.SetPrototypeOf.
* #param pojo Plain object received from API/JSON to be given the class prototype.
*/
private static SetPrototypes(pojo: ParentClass): void
{
ObjectUtils.SetPrototypeOf(pojo.Child, ChildClass);
ObjectUtils.SetPrototypeOfAll(pojo.ListOfChildren, ChildClass);
}
}
class ChildClass
{
public ID?: Guid;
public GrandChild?: GrandChildClass;
/**
* Set the prototypes of all objects in the graph.
* Used for recursive prototype assignment on a graph via ObjectUtils.SetPrototypeOf.
* #param pojo Plain object received from API/JSON to be given the class prototype.
*/
private static SetPrototypes(pojo: ChildClass): void
{
ObjectUtils.SetPrototypeOf(pojo.GrandChild, GrandChildClass);
}
}
Here is ObjectUtils.ts:
/**
* ClassType lets us specify arguments as class variables.
* (where ClassType == window[ClassName])
*/
type ClassType = { new(...args: any[]): any; };
/**
* The name of a class as opposed to the class itself.
* (where ClassType == window[ClassName])
*/
type ClassName = string & {};
abstract class ObjectUtils
{
/**
* Set the prototype of an object to the specified class.
*
* Does nothing if source or type are null.
* Throws an exception if type is not a known class type.
*
* If type has the SetPrototypes method then that is called on the source
* to perform recursive prototype assignment on an object graph.
*
* SetPrototypes is declared private on types because it should only be called
* by this method. It does not (and must not) set the prototype of the object
* itself - only the protoypes of child properties, otherwise it would cause a
* loop. Thus a public method would be misleading and not useful on its own.
*
* https://stackoverflow.com/questions/9959727/proto-vs-prototype-in-javascript
*/
public static SetPrototypeOf(source: any, type: ClassType | ClassName): any
{
let classType = (typeof type === "string") ? window[type] : type;
if (!source || !classType)
{
return source;
}
// Guard/contract utility
ExGuard.IsValid(classType.prototype, "type", <any>type);
if ((<any>Object).setPrototypeOf)
{
(<any>Object).setPrototypeOf(source, classType.prototype);
}
else if (source.__proto__)
{
source.__proto__ = classType.prototype.__proto__;
}
if (typeof classType["SetPrototypes"] === "function")
{
classType["SetPrototypes"](source);
}
return source;
}
/**
* Set the prototype of a list of objects to the specified class.
*
* Throws an exception if type is not a known class type.
*/
public static SetPrototypeOfAll(source: any[], type: ClassType): void
{
if (!source)
{
return;
}
for (var i = 0; i < source.length; i++)
{
this.SetPrototypeOf(source[i], type);
}
}
}
Usage:
let pojo = SomePlainOldJavascriptObjectReceivedViaAjax;
let parentObject = ObjectUtils.SetPrototypeOf(pojo, ParentClass);
// parentObject is now a proper ParentClass instance
**model.ts**
export class Item {
private key: JSON;
constructor(jsonItem: any) {
this.key = jsonItem;
}
}
**service.ts**
import { Item } from '../model/items';
export class ItemService {
items: Item;
constructor() {
this.items = new Item({
'logo': 'Logo',
'home': 'Home',
'about': 'About',
'contact': 'Contact',
});
}
getItems(): Item {
return this.items;
}
}