Buscar..


Un control simple: calificación

Permítanos construir un control simple, un widget de calificación, destinado a ser utilizado como:

<rating min="0" max="5" nullifier="true" ng-model="data.rating"></rating>

No hay CSS de lujo por ahora; esto se traduciría como:

0 1 2 3 4 5 x

Al hacer clic en un número selecciona esa calificación; y al hacer clic en la "x" se establece la calificación en nulo.

app.directive('rating', function() {

    function RatingController() {
        this._ngModel = null;
        this.rating = null;
        this.options = null;
        this.min = typeof this.min === 'number' ? this.min : 1;
        this.max = typeof this.max === 'number' ? this.max : 5;
    }
    
    RatingController.prototype.setNgModel = function(ngModel) {
        this._ngModel = ngModel;
        
        if( ngModel ) {
            // KEY POINT 1
            ngModel.$render = this._render.bind(this);
        }
    };
    
    RatingController.prototype._render = function() {
        this.rating = this._ngModel.$viewValue != null ? this._ngModel.$viewValue : -Number.MAX_VALUE;
    };
    
    RatingController.prototype._calculateOptions = function() {
        if( this.min == null || this.max == null ) {
            this.options = [];
        }
        else {
            this.options = new Array(this.max - this.min + 1);
            for( var i=0; i < this.options.length; i++ ) {
                this.options[i] = this.min + i;
            }
        }
    };
    
    RatingController.prototype.setValue = function(val) {
        this.rating = val;
        // KEY POINT 2
        this._ngModel.$setViewValue(val);
    };
    
    // KEY POINT 3
    Object.defineProperty(RatingController.prototype, 'min', {
        get: function() {
            return this._min;
        },
        set: function(val) {
            this._min = val;
            this._calculateOptions();
        }
    });
    
    Object.defineProperty(RatingController.prototype, 'max', {
        get: function() {
            return this._max;
        },
        set: function(val) {
            this._max = val;
            this._calculateOptions();
        }
    });
    
    return {
        restrict: 'E',
        scope: {
            // KEY POINT 3
            min: '<?',
            max: '<?',
            nullifier: '<?'
        },
        bindToController: true,
        controllerAs: 'ctrl',
        controller: RatingController,
        require: ['rating', 'ngModel'],
        link: function(scope, elem, attrs, ctrls) {
            ctrls[0].setNgModel(ctrls[1]);
        },
        template:
            '<span ng-repeat="o in ctrl.options" href="#" class="rating-option" ng-class="{\'rating-option-active\': o <= ctrl.rating}" ng-click="ctrl.setValue(o)">{{ o }}</span>' +
            '<span ng-if="ctrl.nullifier" ng-click="ctrl.setValue(null)" class="rating-nullifier">&#10006;</span>'
    };
});

Puntos clave:

  1. Implemente ngModel.$render para transferir el valor de vista del modelo a su vista.
  2. Llame a ngModel.$setViewValue() siempre que sienta que el valor de vista debe actualizarse.
  3. El control puede, por supuesto, ser parametrizado; use '<' enlaces de alcance para los parámetros, si está en Angular> = 1.5 para indicar claramente la entrada - enlace de una vía. Si tiene que tomar medidas cada vez que cambia un parámetro, puede usar una propiedad de JavaScript (consulte Object.defineProperty() ) para guardar algunos relojes.

Nota 1: Para no complicar demasiado la implementación, los valores de calificación se insertan en una matriz: las ctrl.options . Esto no es necesario; una implementación más eficiente, pero también más compleja, podría utilizar la manipulación DOM para insertar / eliminar calificaciones cuando el cambio min / max .

Nota 2: Con la excepción de los enlaces de alcance '<' , este ejemplo se puede usar en Angular <1.5. Si está en Angular> = 1.5, sería una buena idea transformar esto en un componente y usar el gancho de ciclo de vida $onInit() para inicializar min y max , en lugar de hacerlo en el constructor del controlador.

Y un violín necesario: https://jsfiddle.net/h81mgxma/

Un par de controles complejos: editar un objeto completo

Un control personalizado no tiene que limitarse a cosas triviales como los primitivos; Puede editar cosas más interesantes. Aquí presentamos dos tipos de controles personalizados, uno para editar personas y otro para editar direcciones. El control de dirección se utiliza para editar la dirección de la persona. Un ejemplo de uso sería:

<input-person ng-model="data.thePerson"></input-person>
<input-address ng-model="data.thePerson.address"></input-address>

El modelo para este ejemplo es deliberadamente simplista:

function Person(data) {
  data = data || {};
  this.name = data.name;
  this.address = data.address ? new Address(data.address) : null;
}

function Address(data) {
  data = data || {};
  this.street = data.street;
  this.number = data.number;
}

El editor de direcciones:

app.directive('inputAddress', function() {

    InputAddressController.$inject = ['$scope'];
    function InputAddressController($scope) {
        this.$scope = $scope;
        this._ngModel = null;
        this.value = null;
        this._unwatch = angular.noop;
    }

    InputAddressController.prototype.setNgModel = function(ngModel) {
        this._ngModel = ngModel;
        
        if( ngModel ) {
            // KEY POINT 3
            ngModel.$render = this._render.bind(this);
        }
    };
    
    InputAddressController.prototype._makeWatch = function() {
        // KEY POINT 1
        this._unwatch = this.$scope.$watchCollection(
            (function() {
                return this.value;
            }).bind(this),
            (function(newval, oldval) {
                if( newval !== oldval ) { // skip the initial trigger
                    this._ngModel.$setViewValue(newval !== null ? new Address(newval) : null);
                }
            }).bind(this)
        );
    };
    
    InputAddressController.prototype._render = function() {
        // KEY POINT 2
        this._unwatch();
        this.value = this._ngModel.$viewValue ? new Address(this._ngModel.$viewValue) : null;
        this._makeWatch();
    };

    return {
        restrict: 'E',
        scope: {},
        bindToController: true,
        controllerAs: 'ctrl',
        controller: InputAddressController,
        require: ['inputAddress', 'ngModel'],
        link: function(scope, elem, attrs, ctrls) {
            ctrls[0].setNgModel(ctrls[1]);
        },
        template:
            '<div>' +
                '<label><span>Street:</span><input type="text" ng-model="ctrl.value.street" /></label>' +
                '<label><span>Number:</span><input type="text" ng-model="ctrl.value.number" /></label>' +
            '</div>'
    };
});

Puntos clave:

  1. Estamos editando un objeto; no queremos cambiar directamente el objeto que nos entregó nuestro padre (queremos que nuestro modelo sea compatible con el principio de inmutabilidad). Así que creamos una observación superficial sobre el objeto que se está editando y actualizamos el modelo con $setViewValue() cada vez que cambia una propiedad. Pasamos una copia a nuestros padres.
  2. Cuando el modelo cambia desde el exterior, lo copiamos y guardamos la copia en nuestro alcance. De nuevo los principios de la inmutabilidad, aunque la copia interna no es inmutable, la externa podría muy bien serlo. Además, reconstruimos el reloj ( this_unwatch();this._makeWatch(); ), para evitar activar al observador por los cambios que nos presenta el modelo. (Solo queremos que el reloj se active por los cambios realizados en la interfaz de usuario).
  3. Aparte de los puntos anteriores, implementamos ngModel.$render() y llamamos ngModel.$setViewValue() como lo haríamos con un control simple (consulte el ejemplo de calificación).

El código para el control personalizado de la persona es casi idéntico. La plantilla está utilizando la <input-address> . En una implementación más avanzada podríamos extraer los controladores en un módulo reutilizable.

app.directive('inputPerson', function() {

    InputPersonController.$inject = ['$scope'];
    function InputPersonController($scope) {
        this.$scope = $scope;
        this._ngModel = null;
        this.value = null;
        this._unwatch = angular.noop;
    }

    InputPersonController.prototype.setNgModel = function(ngModel) {
        this._ngModel = ngModel;
        
        if( ngModel ) {
            ngModel.$render = this._render.bind(this);
        }
    };
    
    InputPersonController.prototype._makeWatch = function() {
        this._unwatch = this.$scope.$watchCollection(
            (function() {
                return this.value;
            }).bind(this),
            (function(newval, oldval) {
                if( newval !== oldval ) { // skip the initial trigger
                    this._ngModel.$setViewValue(newval !== null ? new Person(newval) : null);
                }
            }).bind(this)
        );
    };
    
    InputPersonController.prototype._render = function() {
        this._unwatch();
        this.value = this._ngModel.$viewValue ? new Person(this._ngModel.$viewValue) : null;
        this._makeWatch();
    };

    return {
        restrict: 'E',
        scope: {},
        bindToController: true,
        controllerAs: 'ctrl',
        controller: InputPersonController,
        require: ['inputPerson', 'ngModel'],
        link: function(scope, elem, attrs, ctrls) {
            ctrls[0].setNgModel(ctrls[1]);
        },
        template:
            '<div>' +
                '<label><span>Name:</span><input type="text" ng-model="ctrl.value.name" /></label>' +
                '<input-address ng-model="ctrl.value.address"></input-address>' +
            '</div>'
    };
});

Nota: aquí se escriben los objetos, es decir, tienen constructores adecuados. Esto no es obligatorio; El modelo puede ser simple objetos JSON. En este caso, simplemente use angular.copy() lugar de los constructores. Una ventaja adicional es que el controlador se vuelve idéntico para los dos controles y se puede extraer fácilmente en algún módulo común.

El violín: https://jsfiddle.net/3tzyqfko/2/

Dos versiones del violín que extrajeron el código común de los controladores: https://jsfiddle.net/agj4cp0e/ y https://jsfiddle.net/ugb6Lw8b/



Modified text is an extract of the original Stack Overflow Documentation
Licenciado bajo CC BY-SA 3.0
No afiliado a Stack Overflow