extjs #extjs Tabla de contenido Acerca de 1 Capítulo 1: Empezando con extjs 2 Observaciones 2 Versiones 2 Examples 2 Creando una aplicación Hello World - Via Sencha Cmd 2 Instalación y configuración 3 Creación de una aplicación Hello World - Manualmente 4 Capítulo 2: Errores comunes y mejores prácticas Examples 9 9 Separación de intereses 9 Peor 9 Mejor 9 Explicación 10 Extender vs Override 10 Anulaciones: 10 Extensiones: 10 Explicación 11 Anulaciones separadas de correcciones de errores 11 Capítulo 3: ExtJS AJAX 13 Introducción 13 Examples 13 Solicitud basica Capítulo 4: Modelo de evento 13 14 Introducción 14 Examples 14 Controladores que escuchan los componentes Capítulo 5: MVC / MVVM - Arquitectura de aplicaciones Examples Introducción a los modelos. 14 16 16 16 Ejemplo de aplicación CRUD ExtJS 4 MVC Creditos 16 22 Acerca de You can share this PDF with anyone you feel could benefit from it, downloaded the latest version from: extjs It is an unofficial and free extjs ebook created for educational purposes. All the content is extracted from Stack Overflow Documentation, which is written by many hardworking individuals at Stack Overflow. It is neither affiliated with Stack Overflow nor official extjs. The content is released under Creative Commons BY-SA, and the list of contributors to each chapter are provided in the credits section at the end of this book. Images may be copyright of their respective owners unless otherwise specified. All trademarks and registered trademarks are the property of their respective company owners. Use the content presented in this book at your own risk; it is not guaranteed to be correct nor accurate, please send your feedback and corrections to [email protected] https://riptutorial.com/es/home 1 Capítulo 1: Empezando con extjs Observaciones ExtJS es un marco de JavaScript de Sencha para crear aplicaciones de Internet enriquecidas. Cuenta con una de las bibliotecas más grandes de componentes de UI modulares preconstruidos. Desde la versión 5.0, Sencha ha abogado por el uso de la arquitectura Model-View-ViewModel (MVVM) en su plataforma. También mantiene el soporte para la arquitectura Model-ViewController (MVC), que era el estilo de arquitectura principal soportado hasta la versión 4.x. Además, Sencha se ha centrado en equipar ExtJS con capacidades de aplicaciones web receptivas y centradas en dispositivos móviles. Su anterior marco Sencha Touch se ha integrado con ExtJS desde la versión 6.0 con esfuerzos para combinar las bases de clientes y consolidar las redundancias en el nuevo marco combinado. Versiones Versión Fecha de lanzamiento 6.2 2016-06-14 - 6.0 2015-08-28 5.0 2014-06-02 4.0 2011-04-26 3.0 2009-07-06 2.0 2007-12-04 1.1 2007-04-15 Examples Creando una aplicación Hello World - Via Sencha Cmd Este ejemplo demuestra la creación de una aplicación básica en ExtJS utilizando Sencha Cmd para iniciar el proceso. Este método generará automáticamente algún código y una estructura de esqueleto para el proyecto. Abra una ventana de la consola y cambie el directorio de trabajo a un espacio apropiado para trabajar. En la misma ventana y directorio, ejecute el siguiente comando para generar una nueva https://riptutorial.com/es/home 2 aplicación. > sencha -sdk /path/to/ext-sdk generate app HelloWorld ./HelloWorld Nota: el indicador -sdk especifica la ubicación del directorio extraído del archivo del marco. En ExtJS 6+, Sencha ha fusionado los frameworks ExtJS y Touch en una base de código única, diferenciada por los términos clásico y moderno respectivamente. Para simplificar, si no desea apuntar a dispositivos móviles, se puede especificar un indicador adicional en el comando para reducir el desorden en el área de trabajo. > sencha -sdk /path/to/ext-sdk generate app -classic HelloWorld ./HelloWorld Sin ninguna configuración adicional, una aplicación de demostración completamente funcional ahora debería residir en el directorio local. Ahora cambie el directorio de trabajo al nuevo directorio del proyecto HelloWorld y ejecute: > sencha app watch Al hacer esto, el proyecto se compila utilizando el perfil de compilación predeterminado y se inicia un servidor HTTP simple que permite ver la aplicación localmente a través de un navegador web. Por defecto en el puerto 1841 . Instalación y configuración El uso típico de ExtJS aprovecha el marco para construir aplicaciones enriquecidas de una sola página (RIA). La forma más sencilla de comenzar es hacer uso de Sencha Cmd , una herramienta de creación de CLI que cubre la mayoría de las preocupaciones generales en un ciclo de vida de implementación, principalmente: • gestión de paquetes y dependencias • compilación de código / agrupación y minificación • Gestión de estrategias de construcción para diferentes objetivos y plataformas. » Descargar Sencha Cmd El segundo paso es descargar el SDK, ExtJS es un producto comercial, para obtener una copia, uno de: • inicie sesión en Sencha Support para obtener la versión comercial de las licencias ( página del producto ) • Solicite una copia de evaluación que será válida por 30 días. • Solicite la versión GPL v3 disponible para proyectos de código abierto. (tenga en cuenta que es posible que no pueda acceder a la última versión con esta opción) Después de descargar el SDK, asegúrese de extraer el archivo antes de continuar. https://riptutorial.com/es/home 3 Nota : consulte la documentación oficial de Cómo comenzar para obtener una guía completa de los proyectos de ExtJS. Después de instalar Sencha Cmd, su disponibilidad se puede verificar abriendo una ventana de la consola y ejecutando: > sencha help Ahora tenemos las herramientas necesarias para crear e implementar aplicaciones ExtJS, tome nota de la ubicación del directorio donde se extrajo el SDK, ya que esto se requerirá en más ejemplos. Creación de una aplicación Hello World - Manualmente Vamos a empezar a usar ExtJS para construir una aplicación web simple. Crearemos una aplicación web simple que tendrá solo una página física (aspx / html). Como mínimo, cada aplicación ExtJS contendrá un archivo HTML y un archivo JavaScript, generalmente index.html y app.js. El archivo index.html o su página predeterminada incluirán las referencias al código CSS y JavaScript de ExtJS, junto con su archivo app.js que contiene el código para su aplicación (básicamente el punto de inicio de su aplicación web). Vamos a crear una aplicación web simple que utilizará los componentes de la biblioteca ExtJS: Paso 1: crear una aplicación web vacía Como se muestra en la captura de pantalla, he creado una aplicación web vacía. Para hacerlo simple, puede usar cualquier proyecto de aplicación web en el editor o IDE de su elección. https://riptutorial.com/es/home 4 Paso 2: Agregar una página web predeterminada Si ha creado una aplicación web vacía, debemos incluir una página web que sería la página de inicio de nuestra aplicación. Paso 3: Agregar referencias Ext Js a Default.aspx Este paso muestra cómo hacemos uso de la biblioteca extJS. Como se muestra en la captura de pantalla en Default.aspx, acabo de referir 3 archivos: • ext-all.js • ext-all.css • app.js Sencha se ha asociado con CacheFly, una red de contenido global, para proporcionar alojamiento CDN gratuito para el marco de trabajo de ExtJS. En este ejemplo, he usado la biblioteca CDN de https://riptutorial.com/es/home 5 Ext. Sin embargo, podríamos usar los mismos archivos (ext-all.js y ext-all.css) de nuestro directorio de proyectos en lugar de copias de seguridad en caso de que el CDN no estuviera disponible. Al referirse a app.js, se cargaría en el navegador y sería el punto de partida para nuestra aplicación. Aparte de estos archivos, tenemos un marcador de posición donde se representará la interfaz de usuario. En este ejemplo, tenemos un div con id "espacios en blanco" que usaremos más adelante para representar la interfaz de usuario. <script type="text/javascript" src="http://cdn.sencha.com/ext/trial/5.0.0/build/extall.js"></script> <link rel="stylesheet" type="text/css" href="http://cdn.sencha.com/ext/trial/5.0.0/build/packages/ext-themeneptune/build/resources/ext-theme-neptune-all.css"/> <script src="app/app.js"></script> Paso 4: Agregue la carpeta de aplicaciones y app.js en su proyecto web ExtJS nos proporciona una manera de administrar el código en un patrón MVC. Como se muestra en la captura de pantalla, tenemos una carpeta de contenedor para nuestra aplicación ExtJS, en este caso 'aplicación'. Esta carpeta contendrá todo nuestro código de aplicación dividido en varias carpetas, es decir, modelo, vista, controlador, tienda, etc. Actualmente, solo tiene el archivo app.js. https://riptutorial.com/es/home 6 Paso 5: Escribe tu código en app.js App.js es el punto de partida de nuestra aplicación; para esta muestra, acabo de usar la configuración mínima requerida para iniciar la aplicación. Ext.application representa una aplicación ExtJS que hace varias cosas. Crea una variable global ' SenchaApp ' proporcionada en la configuración del nombre y todas las clases de aplicaciones (modelos, vistas, controladores, tiendas) residirán en el espacio de nombres único. El lanzamiento es una función que se llama automáticamente cuando toda la aplicación está lista (todas las clases se cargan correctamente). En este ejemplo, estamos creando un Panel con alguna configuración y representándolo en el marcador de posición que proporcionamos en Default.aspx. Ext.application({ name: 'SenchaApp', launch: function () { Ext.create('Ext.panel.Panel', { title: 'Sencha App', width: 300, height: 300, bodyPadding:10, renderTo: 'whitespace', html:'Hello World' }); } }); Captura de pantalla de salida Cuando ejecute esta aplicación web con Default.aspx como página de inicio, aparecerá la siguiente ventana en el navegador. https://riptutorial.com/es/home 7 Lea Empezando con extjs en línea: https://riptutorial.com/es/extjs/topic/819/empezando-con-extjs https://riptutorial.com/es/home 8 Capítulo 2: Errores comunes y mejores prácticas Examples Separación de intereses Peor ViewController: // ... myMethod: function () { this.getView().lookup('myhappyfield').setValue(100); } //... Ver: //... items: [ { xtype: 'textfield', reference: 'myhappyfield' } ] //... Mejor ViewController: // ... myMethod: function () { this.getView().setHappiness(100); } //... Ver: //... items: [ { xtype: 'textfield', reference: 'myhappyfield' } ], setHappiness: function (happiness) { this.lookup('myhappyfield').setValue(happiness); https://riptutorial.com/es/home 9 } //... Explicación En este ejemplo, los dos fragmentos de código realizan la misma tarea. Sin embargo, en el caso de que la referencia a myhappyfield cambie o la metodología para myhappyfield cambios de "felicidad" de manera significativa, el enfoque anterior requiere cambios en cada lugar donde se usa la referencia. Con preocupaciones separadas (el último ejemplo), la vista proporciona una forma abstracta de modificar la "felicidad" que otras clases pueden usar. La consulta y la manipulación de componentes se guardan en un solo lugar (¡justo al lado de la definición de la vista en sí misma!) Y las llamadas al método abstraído no necesitan cambiar. Si bien es posible que un controlador permita realizar consultas a través de las capas de una vista, es muy recomendable abstraer ese comportamiento en los métodos de la vista. De esta manera, una vista puede proporcionar formas estandarizadas para que otras clases influyan en ella y minimicen o eliminen los cambios en otras clases cuando cambia la estructura de una vista. Extender vs Override Anulaciones: Reemplazar archivo: Ext.define('MyApp.override.CornField', override: 'Ext.form.field.Text', initComponent: function () { this.callParent(arguments); this.setValue('Corn!'); } ); Usar en la aplicación: { xtype: 'textfield' } Extensiones: Reemplazar archivo: Ext.define('MyApp.form.field.CornField', extend: 'Ext.form.field.Text', alias: 'widget.cornfield', initComponent: function () { this.callParent(arguments); this.setValue('Corn!'); https://riptutorial.com/es/home 10 } ); Usar en la aplicación: { xtype: 'cornfield' } Explicación ExtJS proporciona dos formas principales de cambiar el comportamiento de las clases existentes: extenderlas y anularlas. Cada uno tiene beneficios y dificultades que deben considerarse antes de usarlos. Extensiones Extender una clase crea una nueva clase que hereda su comportamiento y configuración de su padre. Al crear una nueva clase a través de la extensión, se pueden realizar cambios repetidos de configuración y comportamiento en una ubicación central y reutilizarlos en toda la aplicación. La mayor ventaja de la extensión es que la clase principal permanece intacta y disponible para casos de uso más simples donde no se desea el comportamiento extendido. Los ejemplos de casos de buen uso para extensiones incluyen campos de formulario personalizados con comportamiento especial, modales especializados y componentes personalizados en general. Anulaciones La anulación de una clase modifica el comportamiento de una clase existente en su lugar. La configuración y los métodos en los reemplazos reemplazan a sus contrapartes de la clase principal por completo, creando nuevas configuraciones y comportamientos predeterminados que llenan toda la aplicación. Las anulaciones deben usarse con moderación debido a la naturaleza destructiva de su uso; una clase extendida normalmente puede proporcionar los mismos beneficios mientras deja la clase principal sin ser molestada. Sin embargo, las anulaciones pueden proporcionar beneficios en algunas situaciones. Los casos de buen uso incluyen corregir errores en clases existentes, modificar el comportamiento del proxy para agregar información adicional a las solicitudes, como un token o datos de sesión, y generalmente obligar a un comportamiento específico a ser el comportamiento predeterminado en una aplicación. Anulaciones separadas de correcciones de errores En ExtJS, puede anular casi cualquier método del marco y reemplazarlo por el suyo. Esto le permite modificar las clases existentes sin modificar directamente el código fuente de ExtJS. A veces, es posible que desee mejorar una clase existente o proporcionar una propiedad predeterminada sana en una clase. https://riptutorial.com/es/home 11 Por ejemplo, es posible que desee que todos los campos de datos de su modelo permitan valores nulos. Ext.define('MyApp.override.DataField', { override: 'Ext.data.field.Field', allowNull: true }); En otros casos, tendrá que arreglar algo que está roto en el marco. Aquí hay un ejemplo de una corrección de errores con documentación. Tenga en cuenta que el nombre de clase contiene "corregir" en lugar de "anular". El nombre real no es importante, pero la separación sí lo es. Ext.define('MyApp.fix.FieldBase', { override: 'Ext.form.field.Base', /** * Add a description of what this fix does. * Be sure to add URLs to important reference information! * * You can also include some of your own tags to help identify * when the problem started and what Sencha bug ticket it relates to. * * @extversion 5.1.1 * @extbug EXTJS-15302 */ publishValue: function () { this.publishState('value', this.getValue()); } }); Ahora, cuando llega el momento de actualizar a la próxima versión de ExtJS, solo hay un lugar donde debe verificar para ver cuáles de sus correcciones de errores se pueden eliminar. Lea Errores comunes y mejores prácticas en línea: https://riptutorial.com/es/extjs/topic/5412/errores-comunes-y-mejores-practicas https://riptutorial.com/es/home 12 Capítulo 3: ExtJS AJAX Introducción Una instancia de singleton de una clase [ Ext.data.Connection ] [1]. Esta clase se utiliza para comunicarse con su servidor. [1]: http: //docs.sencha.com/extjs/6.0.1/classic/src/Connection.js.html#Ext.data.Connection Examples Solicitud basica Algunas de las propiedades de clase Ext.Data.Connection Propiedades Detalles url Dirección de la solicitud timeout Tiempo de espera en milisegundos success Retorno sobre el éxito failure Retorno en caso de fallo Ext.Ajax.on("beforerequest", function(conn , options , eOpts) { console.log("beforerequest"); }); Ext.Ajax.on("requestcomplete", function(conn , response , options , eOpts) { console.log("requestcomplete"); }); Ext.Ajax.on("requestexception", function(conn , response , options , eOpts) { console.log("requestexception"); }); Ext.Ajax.request({ url: 'mypath/sample.json', timeout: 60000, success: function(response, opts) { var obj = Ext.decode(response.responseText); console.log(obj); }, failure: function(response, opts) { console.log('server-side failure with status code ' + response.status); } }); Lea ExtJS AJAX en línea: https://riptutorial.com/es/extjs/topic/8134/extjs-ajax https://riptutorial.com/es/home 13 Capítulo 4: Modelo de evento Introducción ExtJS aboga por el uso de la activación y la escucha de eventos entre clases. Al desencadenar eventos y escucharlos, las clases no requieren un conocimiento 'sucio' de la estructura de clases de los demás y evitan el acoplamiento de código. Además, los eventos facilitan la escucha de varias instancias del mismo componente al permitir una escucha genérica para todos los objetos con el mismo selector. Finalmente, otras clases también pueden hacer uso de eventos que ya existen. Examples Controladores que escuchan los componentes Ext.define('App.Duck', { extend: 'Ext.Component', alias: 'widget.duck', initComponent: function () { this.callParent(arguments); this._quack(); }, _quack: function () { console.log('The duck says "Quack!"'); this.fireEvent('quack'); }, feed: function () { console.log('The duck looks content.'); }, poke: function () { this._quack(); } }); var feedController = Ext.create('Ext.app.Controller', { listen: { components: { duck: { quack: 'feedDuck' } } }, feedDuck: function (duck) { duck.feed(); } }); var countController = Ext.create('Ext.app.Controller', { listen: { components: { duck: { quack: 'addCount' } https://riptutorial.com/es/home 14 } }, quackCount: 0, addCount: function (duck) { this.quackCount++; console.log('There have been this many quacks: ' + this.quackCount); } }); var firstDuck = Ext.create('App.Duck'); // The duck says "Quack!" // The duck looks content. // There have been this many quacks: 1 var secondDuck = Ext.create('App.Duck'); // The duck says "Quack!" // The duck looks content. // There have been this many quacks: 2 firstDuck.poke(); // The duck says "Quack!" // The duck looks content. // There have been this many quacks: 3 Lea Modelo de evento en línea: https://riptutorial.com/es/extjs/topic/9314/modelo-de-evento https://riptutorial.com/es/home 15 Capítulo 5: MVC / MVVM - Arquitectura de aplicaciones Examples Introducción a los modelos. Un modelo representa algún objeto de datos en una aplicación. Por ejemplo, puede tener un modelo como: Fruta, Coche, Construcción, etc. en su aplicación. Los modelos son normalmente utilizados por las tiendas. Aquí está un ejemplo de cómo definiría una nueva clase de modelo. p.ej Ext.define('MyApp.model.Person', { extend: 'Ext.data.Model', fields: [ {name: 'name', type: 'string'}, {name: 'surname', type: 'string'}, {name: 'age', type: 'int'} ], getFullName: function() { return this.get('name') + " " + this.get('surname'); } }); Después de definir nuestra clase de modelo, posiblemente nos gustaría crear una instancia de ella y probablemente llamar a algunos métodos. Por ejemplo: // Create person instance var person = Ext.create('MyApp.model.Person', { name : 'Jon', surname: 'Doe', age : 24 }); alert(person.getFullName()); // Display person full name Ejemplo de aplicación CRUD ExtJS 4 MVC La demostración en línea está aquí: http://ext4all.com/post/extjs-4-mvc-applicationarchitecture.html Definir un modelo: // /scripts/app/model/User.js Ext.define('AM.model.User', { extend: 'Ext.data.Model', fields: ['id', 'name', 'email'] }); https://riptutorial.com/es/home 16 Define una tienda con proxy: // /scripts/app/store/Users.js Ext.define('AM.store.Users', { extend: 'Ext.data.Store', model: 'AM.model.User', autoLoad: true, autoSync: true, proxy: { type: 'ajax', limitParam: 'size', startParam: undefined, api: { create: '/user/add', read: '/user/list', update: '/user/update', destroy: '/user/delete' }, reader: { type: 'json', root: 'data', successProperty: 'success' }, writer: { type: 'json', writeAllFields: false } } }); Defina agregar vista de usuario, es una ventana con un formulario dentro: // /scripts/app/view/user/Add.js Ext.define('AM.view.user.Add', { extend: 'Ext.window.Window', alias: 'widget.useradd', title: 'Add User', layout: 'fit', autoShow: true, initComponent: function () { this.items = [ { xtype: 'form', bodyStyle: { background: 'none', padding: '10px', border: '0' }, items: [ { xtype: 'textfield', name: 'name', allowBlank: false, fieldLabel: 'Name' }, { xtype: 'textfield', name: 'email', allowBlank: false, https://riptutorial.com/es/home 17 vtype: 'email', fieldLabel: 'Email' } ] } ]; this.buttons = [ { text: 'Save', action: 'save' }, { text: 'Cancel', scope: this, handler: this.close } ]; this.callParent(arguments); } }); Defina la vista de edición de usuario, también es una ventana con un formulario dentro: // /scripts/app/view/user/Edit.js Ext.define('AM.view.user.Edit', { extend: 'Ext.window.Window', alias: 'widget.useredit', title: 'Edit User', layout: 'fit', autoShow: true, initComponent: function () { this.items = [ { xtype: 'form', bodyStyle: { background: 'none', padding: '10px', border: '0' }, items: [ { xtype: 'textfield', name: 'name', allowBlank: false, fieldLabel: 'Name' }, { xtype: 'textfield', name: 'email', allowBlank: false, vtype: 'email', fieldLabel: 'Email' } ] } ]; this.buttons = [ { text: 'Save', action: 'save' https://riptutorial.com/es/home 18 }, { text: 'Cancel', scope: this, handler: this.close } ]; this.callParent(arguments); } }); Defina una vista de lista de usuarios: es una cuadrícula con las columnas Id, Name, Email // /scripts/app/view/user/List.js Ext.define('AM.view.user.List', { extend: 'Ext.grid.Panel', alias: 'widget.userlist', title: 'All Users', store: 'Users', initComponent: function () { this.tbar = [{ text: 'Create User', action: 'create' }]; this.columns = [ { header: 'Id', dataIndex: 'id', width: 50 }, { header: 'Name', dataIndex: 'name', flex: 1 }, { header: 'Email', dataIndex: 'email', flex: 1 } ]; this.addEvents('removeitem'); this.actions = { removeitem: Ext.create('Ext.Action', { text: 'Remove User', handler: function () { this.fireEvent('removeitem', this.getSelected()) }, scope: this }) }; var contextMenu = Ext.create('Ext.menu.Menu', { items: [ this.actions.removeitem ] }); this.on({ itemcontextmenu: function (view, rec, node, index, e) { e.stopEvent(); contextMenu.showAt(e.getXY()); return false; } }); this.callParent(arguments); }, getSelected: function () { var sm = this.getSelectionModel(); var rs = sm.getSelection(); if (rs.length) { return rs[0]; } return null; } }); https://riptutorial.com/es/home 19 Defina un controlador para manejar eventos de vistas: // /scripts/app/controller/Users.js Ext.define('AM.controller.Users', { extend: 'Ext.app.Controller', stores: [ 'Users' ], models: [ 'User' ], views: [ 'user.List', 'user.Add', 'user.Edit' ], init: function () { this.control({ 'userlist': { itemdblclick: this.editUser, removeitem: this.removeUser }, 'userlist > toolbar > button[action=create]': { click: this.onCreateUser }, 'useradd button[action=save]': { click: this.doCreateUser }, 'useredit button[action=save]': { click: this.updateUser } }); }, editUser: function (grid, record) { var view = Ext.widget('useredit'); view.down('form').loadRecord(record); }, removeUser: function (user) { Ext.Msg.confirm('Remove User', 'Are you sure?', function (button) { if (button == 'yes') { this.getUsersStore().remove(user); } }, this); }, onCreateUser: function () { var view = Ext.widget('useradd'); }, doCreateUser: function (button) { var win = button.up('window'), form = win.down('form'), values = form.getValues(), store = this.getUsersStore(); if (form.getForm().isValid()) { store.add(values); win.close(); } }, updateUser: function (button) { var win = button.up('window'), form = win.down('form'), record = form.getRecord(), https://riptutorial.com/es/home 20 values = form.getValues(), store = this.getUsersStore(); if (form.getForm().isValid()) { record.set(values); win.close(); } } }); Define tu aplicación en app.js: // /scripts/app/app.js Ext.Loader.setConfig({ enabled: true }); Ext.application({ name: 'AM', appFolder: 'scripts/app', controllers: [ 'Users' ], launch: function () { Ext.create('Ext.container.Viewport', { layout: 'border', items: { xtype: 'userlist', region: 'center', margins: '5 5 5 5' } }); } }); La demostración en línea está aquí: http://ext4all.com/post/extjs-4-mvc-applicationarchitecture.html Lea MVC / MVVM - Arquitectura de aplicaciones en línea: https://riptutorial.com/es/extjs/topic/3854/mvc---mvvm---arquitectura-de-aplicaciones https://riptutorial.com/es/home 21 Creditos S. No Capítulos Contributors 1 Empezando con extjs Ben Rhys-Lewis, Community, David Millar, Dharmesh Hadiyal, Emissary, srinivasarao, UDID 2 Errores comunes y mejores prácticas David Millar, Emissary, John Krull 3 ExtJS AJAX Alexandre N. 4 Modelo de evento David Millar 5 MVC / MVVM Arquitectura de aplicaciones CD.., Emissary, Giorgi Moniava, khmurach https://riptutorial.com/es/home 22